Skip to content
Commits on Source (10)
......@@ -5,6 +5,21 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](<http://keepachangelog.com/en/1.0.0/>)
and this project adheres to [Semantic Versioning](<http://semver.org/spec/v2.0.0.html>).
## [unchanged]
## [1.2.3]
### Added
- script to check overlapping and incompatibility of unique roles in local administrations
- persons endpoint in API now shows classifications for list and detailed view;
classifications may be passed in the json for POST (creation) and PUT (update) operations;
- PopoloPersonWithRelatedDetailsLoader handles json of persons with classifications, same syntax of organizations
### Fixed
- histadmin update logic improved: overwriting existing values is avoided; less duplicates are found
- typo in ``script_check_overlapping_unique_roles`` management task
## [1.2.2]
### Changed
......@@ -528,6 +543,8 @@ in the test stage.
[atoka]: https://atoka.io
[1.2.3]: https://gitlab.depp.it/openpolis/opdm/opdm-service/compare/v1.2.2...v1.2.3
[1.2.2]: https://gitlab.depp.it/openpolis/opdm/opdm-service/compare/v1.1.18...v1.2.2
[1.1.18]: https://gitlab.depp.it/openpolis/opdm/opdm-service/compare/v1.1.17...v1.1.18
[1.1.17]: https://gitlab.depp.it/openpolis/opdm/opdm-service/compare/v1.1.16...v1.1.17
[1.1.16]: https://gitlab.depp.it/openpolis/opdm/opdm-service/compare/v1.1.15...v1.1.16
......
......@@ -3,7 +3,7 @@ Openpolis Data Manager service package (backend)
"""
from typing import Optional
__version__ = (1, 2, 2)
__version__ = (1, 2, 3)
def get_version_str() -> str:
......
......@@ -96,6 +96,15 @@ class PersonFilterSet(
help_text=_("Get persons by Fiscal Code"),
)
classification_id = NumberFilter(
field_name="classifications__classification_id",
label=_("Classification"),
help_text=_(
"Filter persons by a known Classification object (ID). For example:"
" 2605 (OPDM_PERSON_LABEL=Politico locale)."
),
)
@staticmethod
def cfid_filter(queryset, name, value):
return queryset.filter(identifiers__scheme="CF", identifiers__identifier=value)
......@@ -178,7 +187,7 @@ class OrganizationFilterSet(
)
classification = CharFilter(
field_name="classification", lookup_expr="iexact", label=_("Classification")
field_name="classification", lookup_expr="iexact", label=_("Main classification")
)
forma_giuridica = CharFilter(
......
......@@ -378,6 +378,7 @@ class PersonListResultSerializer(serializers.HyperlinkedModelSerializer):
identifiers = IdentifierInlineSerializer(many=True)
other_names = OtherNameInlineSerializer(many=True)
contact_details = ContactDetailInlineSerializer(many=True)
classifications = ClassificationRelInlineSerializer(many=True)
birth_location_area = AreaInlineSerializer()
class Meta:
......@@ -398,6 +399,7 @@ class PersonListResultSerializer(serializers.HyperlinkedModelSerializer):
"birth_location_area",
"email",
"identifiers",
"classifications",
"other_names",
"contact_details",
"created_at",
......
......@@ -578,6 +578,7 @@ class PersonSerializer(serializers.HyperlinkedModelSerializer):
sources = SourceRelSerializer(many=True)
memberships = MembershipInlineSerializer(many=True)
ownerships = PersonOwnershipInlineSerializer(many=True)
classifications = ClassificationRelInlineSerializer(many=True)
related_persons = PersonInlineSerializer(many=True)
birth_location_area = AreaInlineSerializer(read_only=True)
original_profession = OriginalProfessionSerializer(read_only=True, allow_null=True)
......@@ -623,6 +624,7 @@ class PersonSerializer(serializers.HyperlinkedModelSerializer):
"sources",
"memberships",
"ownerships",
"classifications",
"related_persons",
"created_at",
"updated_at",
......@@ -666,6 +668,9 @@ class PersonWriteSerializer(
contact_details = ContactDetailSerializer(
many=True, required=False, allow_empty=True
)
classifications = ClassificationInlineSerializer(
many=True, required=False, allow_null=True
)
other_names = OtherNameSerializer(many=True, required=False, allow_empty=True)
links = LinkRelSerializer(many=True, required=False, allow_empty=True)
sources = SourceRelSerializer(many=True, required=False, allow_empty=True)
......@@ -676,6 +681,7 @@ class PersonWriteSerializer(
"other_names",
"identifiers",
"contact_details",
"classifications",
]
class Meta:
......@@ -706,6 +712,7 @@ class PersonWriteSerializer(
"email",
"contact_details",
"identifiers",
"classifications",
"other_names",
"links",
"sources",
......
......@@ -716,6 +716,103 @@ class PersonTestCase(
content = json.loads(response.content)
self.assertEqual(content["count"], 1)
def test_create_person_with_classifications(self):
""" Test creation of a person with three different classifications
:return:
"""
classification_a = ClassificationFactory()
classification_b = ClassificationFactory()
# test various methods to add a classification
classifications = [
{"classification": classification_a.id}, # existing classification id
{
"scheme": faker.word(),
"code": faker.ssn(),
"descr": faker.word(),
}, # non-existing classification by dict
{
"scheme": classification_b.scheme,
"code": classification_b.code,
}, # existing classification by dict
{
"scheme": classification_b.scheme,
"code": faker.word(),
}, # no duplication of same-scheme-class
]
person = self.get_basic_data()
person["classifications"] = classifications
response = self.client.post(self.endpoint, person, format="json")
self.assertEquals(
response.status_code, status.HTTP_201_CREATED, response.content
)
r = json.loads(response.content)
self.assertEquals(len(r["classifications"]), 3)
response = self.client.get(self.endpoint + "/classifications", format="json")
r = json.loads(response.content)
self.assertEquals(response.status_code, status.HTTP_200_OK, response.content)
self.assertEquals(r["count"], 3)
response = self.client.get(self.endpoint + "/classification_types", format="json")
r = json.loads(response.content)
self.assertEquals(response.status_code, status.HTTP_200_OK, response.content)
self.assertEquals(r["count"], 3)
def test_create_persons_classifications_donot_repeat(self):
""" Test creation of 2 organizations with the same 2 classifications.
The total amount of classifications is 2.
:return:
"""
classification_a = ClassificationFactory()
classification_b = ClassificationFactory()
classifications = [
{"classification": classification_a.id},
{"classification": classification_b.id},
]
person = self.get_basic_data()
person["classifications"] = classifications
response = self.client.post(self.endpoint, person, format="json")
self.assertEquals(
response.status_code, status.HTTP_201_CREATED, response.content
)
r = json.loads(response.content)
self.assertEquals(len(r["classifications"]), 2)
response = self.client.get(self.endpoint + "/classifications", format="json")
r = json.loads(response.content)
self.assertEquals(response.status_code, status.HTTP_200_OK, response.content)
self.assertEquals(r["count"], 2)
person_b = self.get_basic_data()
person_b["classifications"] = classifications
response = self.client.post(self.endpoint, person_b, format="json")
self.assertEquals(
response.status_code, status.HTTP_201_CREATED, response.content
)
r = json.loads(response.content)
self.assertEquals(len(r["classifications"]), 2)
response = self.client.get(self.endpoint + "/classifications", format="json")
r = json.loads(response.content)
self.assertEquals(response.status_code, status.HTTP_200_OK, response.content)
self.assertEquals(r["count"], 2)
response = self.client.get(self.endpoint + "/classification_types", format="json")
r = json.loads(response.content)
self.assertEquals(response.status_code, status.HTTP_200_OK, response.content)
self.assertEquals(r["count"], 2)
class ProfessionsTestCase(
mixins.APIResourceWithIdentifiersTestCase,
......
......@@ -734,7 +734,7 @@ class OrganizationViewSet(
return Response(response)
class PersonViewSet(IdentifierTypesMixin, rw_viewsets.ModelViewSet):
class PersonViewSet(IdentifierTypesMixin, ClassificationTypesMixin, rw_viewsets.ModelViewSet):
"""
A ViewsSet for viewing and editing Person resources.
......@@ -775,6 +775,7 @@ class PersonViewSet(IdentifierTypesMixin, rw_viewsets.ModelViewSet):
"sources",
"memberships",
"ownerships",
"classifications__classification",
)
)
search_fields = ("name",)
......@@ -837,6 +838,38 @@ class PersonViewSet(IdentifierTypesMixin, rw_viewsets.ModelViewSet):
# TODO: implement endpoint returning a list of all possible education_levels
return Response()
@action(
detail=False,
queryset=Classification.objects.all(),
serializer_class=ClassificationInlineSerializer,
ordering=("scheme", "code"),
ordering_fields=("scheme", "code", "descr"),
search_fields=("scheme", "code", "descr"),
filter_backends=(
filters.SearchFilter,
NullsLastOrderingFilter,
extra_filters.DjangoFilterBackend,
),
filter_class=ClassificationFilterSet,
)
def classifications(self, request, *args, **kwargs):
""" All Persons' classifications """
ct = ContentType.objects.get(model="person")
self.queryset = (
Classification.objects.filter(related_objects__content_type_id=ct.id)
.values("scheme", "code", "descr", "id")
.distinct()
)
page = self.paginate_queryset(self.filter_queryset(self.get_queryset()))
if page is not None:
serializer = self.get_serializer(page, many=True)
return self.get_paginated_response(serializer.data)
serializer = self.get_serializer(
self.filter_queryset(self.get_queryset()), many=True
)
return Response(serializer.data)
@action(
detail=False, serializer_class=PersonSearchResultSerializer, filter_backends=()
)
......
......@@ -107,6 +107,7 @@ def update_or_create_person_from_item(item, person_id=0, update_strategy="keep_o
identifiers = item.get("identifiers", [])
sources = item.get("sources", [])
links = item.get("links", [])
classifications = item.get("classifications", [])
if person_id < 0:
raise ValueError("person_id must be greater than 0")
......@@ -138,6 +139,7 @@ def update_or_create_person_from_item(item, person_id=0, update_strategy="keep_o
p.add_identifiers(identifiers)
p.add_sources(sources)
p.add_links(links)
p.add_classifications(classifications)
return p, created
......@@ -278,13 +280,13 @@ def update_or_create_role(
or "sconosciut" in v.lower()
or "non not" in v.lower()
)
) or (
"overwrite" in update_strategy and
k == "end_date" and v is not None
) or (
"overwrite" in update_strategy and
k == "end_reason" and v is not None
)
or "overwrite" in update_strategy
and k == "end_date"
and v is not None
or "overwrite" in update_strategy
and k == "end_reason"
and v is not None
):
to_save = True
setattr(role, k, v)
......
......@@ -198,13 +198,13 @@ class OPDMLoaderPipeline(object):
def remove_duplicates(minint_memberships, opdm_memberships):
# filter out duplicated person + role
minint_names_roles = [(m['person__name'], m['role']) for m in minint_memberships]
opdm_names_roles = [(m['person__name'], m['role']) for m in opdm_memberships]
minint_names_roles_sd = [(m['person__name'], m['role'], m['start_date']) for m in minint_memberships]
opdm_names_roles_sd = [(m['person__name'], m['role'], m['start_date']) for m in opdm_memberships]
d = {
x: minint_names_roles.count(x)
for x in minint_names_roles
if minint_names_roles.count(x) > 1 or opdm_names_roles.count(x) > 1
x: minint_names_roles_sd.count(x)
for x in minint_names_roles_sd
if minint_names_roles_sd.count(x) > 1 or opdm_names_roles_sd.count(x) > 1
}
if d != {}:
minint_memberships = list(filter(
......@@ -219,13 +219,13 @@ class OPDMLoaderPipeline(object):
spider.logger.warning(" name: {0}, role: {1}".format(k[0], k[1]))
d = {
x: opdm_names_roles.count(x)
for x in opdm_names_roles
if opdm_names_roles.count(x) > 1 or minint_names_roles.count(x) > 1
x: opdm_names_roles_sd.count(x)
for x in opdm_names_roles_sd
if opdm_names_roles_sd.count(x) > 1 or minint_names_roles_sd.count(x) > 1
}
if d != {}:
opdm_memberships = list(filter(
lambda x: (x['person__name'], x['role']) not in d,
lambda x: (x['person__name'], x['role'], x['start_date']) not in d,
opdm_memberships
))
spider.logger.warning(
......@@ -233,7 +233,7 @@ class OPDMLoaderPipeline(object):
"May point to multiple charges in the same context, needs manual check."
)
for k in d.keys():
spider.logger.warning(" name: {0}, role: {1}".format(k[0], k[1]))
spider.logger.warning(" name: {0}, role: {1}, start_date: {2}".format(k[0], k[1], k[2]))
return minint_memberships, opdm_memberships
......@@ -333,12 +333,14 @@ class OPDMLoaderPipeline(object):
# person = Person.objects.get(id=person_id)
membership = Membership.objects.get(id=membership_id)
# will have the URL where the information has been read
update_source = None
# copy likely updates into r_update, removing non-updateable fields
r_update = deepcopy(l_update)
r_update.pop(opdm_identifier_field)
r_update.pop(membership_id_field)
source_added_msg = ''
idx = "-"
for k, v in l_update.items():
......@@ -358,36 +360,49 @@ class OPDMLoaderPipeline(object):
if field in ['opdm_identifier', 'membership_id']:
continue
# set start or end dates for memberships, avoiding overwriting existing values
if field in ['start_date', 'end_date'] and getattr(membership, field, None) is None:
# skip null or undefined values for end date
# avoid overwriting existing values (valid for all fields)
if getattr(membership, field, None) is not None:
r_update.pop(fieldnames.index(field))
continue
# set start or end dates for memberships
if field in ['start_date', 'end_date']:
# avoid writing empty or undefined values, leaving it to None
if field == 'end_date' and (v_new == '-' or v_new.strip() == ''):
r_update.pop(fieldnames.index(field))
continue
setattr(membership, field, v_new)
# set electoral list, if not null
if field == 'electoral_list_descr_tmp' and membership.electoral_list_descr_tmp is None:
if v_new != '-' and v_new.strip() != '':
membership.electoral_list_descr_tmp = v_new
else:
r_update.pop(fieldnames.index('electoral_list_descr_tmp'))
# set end_reason if different from proclamazione (it's a status in the minint page)
if field == 'end_reason':
# avoid writing proclamazione as an end_reason
if v_new == 'proclamazione':
r_update.pop(fieldnames.index(field))
continue
# set end_reason if different from proclamazione (it's a status in the minint page)
if field == 'end_reason' and membership.end_reason is None:
if v_new != 'proclamazione':
membership.end_reason = v_new
else:
r_update.pop(fieldnames.index('end_reason'))
membership.end_reason = v_new
# set electoral list, if not null
if field == 'electoral_list_descr_tmp':
# avoid writing empty or undefined values, leaving it to None
if v_new == '-' or v_new.strip() == '':
r_update.pop(fieldnames.index(field))
continue
# add minint_identifier to the record
membership.electoral_list_descr_tmp = v_new
# set the update_source apart, to use it in case of a real update
if field == 'minint_detail_url':
membership.add_source(v_new.replace(" ", "+"), note='amministratori.interno.gov.it')
source_added_msg = "minint source added for this membership"
update_source = v_new.replace(" ", "+")
r_update.pop(fieldnames.index(field))
# add electoral event
if field == 'election_date':
membership.electoral_event = electoral_event
# log real updates
......@@ -397,8 +412,13 @@ class OPDMLoaderPipeline(object):
for k, v in r_update.items()
if fieldnames[k] != 'minint_detail_url'
]
if source_added_msg:
messages.append(source_added_msg)
if update_source:
messages.append("histadmin source added for this membership")
membership.add_source(
update_source,
note="Storia amministrativa dell'Ente, da amministratori.interno.gov.it"
)
changes_str = "; ".join(messages)
# person.save()
......@@ -484,7 +504,7 @@ class OPDMLoaderPipeline(object):
minint_io,
sorted(
el_group['minint_administrators'],
key=lambda x: ",".join([x['person__name'], x['role']])
key=lambda x: ",".join([x['person__name'], x['role'], x['start_date']])
),
fieldnames
)
......@@ -495,7 +515,7 @@ class OPDMLoaderPipeline(object):
sorted(
[membership_transform(administrator, memberships_minint_detail_urls)
for administrator in el_group['opdm_administrators']],
key=lambda x: ",".join([x['person__name'], x['role']])
key=lambda x: ",".join([x['person__name'], x['role'], x['start_date']])
),
fieldnames,
persons_other_names=persons_other_names
......@@ -504,6 +524,7 @@ class OPDMLoaderPipeline(object):
index_columns = (
fieldnames.index('person__name'),
fieldnames.index('role'),
fieldnames.index('start_date'),
)
adds, removes, updates = DiffHelper.csv_streams_diff(
new=opdm_io, old=minint_io, sep=",",
......
# coding=utf-8
import itertools
import datetime as dt
from django.db.models import F
from popolo.models import Organization
from taskmanager.management.base import LoggingBaseCommand
class Command(LoggingBaseCommand):
"""
Loop over all municipalities or regions and show those with overlapping unique roles:
- presidenti, sindaci, presidenti del consiglio
"""
help = "Check overlapping unique roles"
epoch = dt.datetime(1970, 1, 1)
def add_arguments(self, parser):
parser.add_argument(
"--context",
dest="context",
help="The context to check [com|prov|reg].",
)
parser.add_argument(
"--max-ab",
dest="max_ab", type=int,
help="The maximum inhabitants limit.",
)
parser.add_argument(
"--min-ab",
dest="min_ab", type=int,
help="The minimum inhabitants limit.",
)
@classmethod
def check_overlapping_roles_in_org(cls, org, classification, role):
"""
:param org:
:param classification:
:param role:
:return:
"""
roles = []
org = org.children.get(classification=classification)
ms = org.memberships.filter(
end_date__isnull=False,
role=role
)
_l = list(itertools.chain(*ms.values_list('start_date', 'end_date').order_by('start_date')))
if sorted(_l) != _l:
overlapping_roles = list(
ms.values('person__name', 'label', 'start_date', 'end_date').order_by('start_date')
)
for k in range(0, len(overlapping_roles)-1):
role = overlapping_roles[k]
role_next = overlapping_roles[k+1]
if role_next['start_date'] < role['end_date']:
roles.append((role, role_next))
return roles
@classmethod
def check_incompatible_roles(cls, org, classification, roles):
"""Check overlapping roles for single person with roles as
Presidente del consiglio and Consigliere or
Vicesindaco and Assessore
:param org:
:param classification:
:param roles:
:return:
"""
_roles = []
org = org.children.get(classification=classification)
ms = org.memberships.filter(
end_date__isnull=False,
role__in=roles
)
for k, g in itertools.groupby(
ms.values_list(
'person_id', 'role', 'start_date', 'end_date'
).order_by('person_id', 'start_date'),
lambda x: x[0]
):
g1 = list(g)
if roles[0] not in {i[1] for i in g1}:
continue
_l = list(itertools.chain(*[[i[2], i[3]] for i in g1]))
if _l != sorted(_l):
overlapping_roles = list(
ms.filter(person_id=k).values(
'person__name', 'label', 'start_date', 'end_date'
).order_by('start_date')
)
for i in range(0, len(overlapping_roles)-1):
role = overlapping_roles[i]
role_next = overlapping_roles[i+1]
if role_next['start_date'] < role['end_date']:
_roles.append((role, role_next))
return _roles
def handle(self, *args, **options):
super(Command, self).handle(__name__, *args, formatter_key="simple", **options)
context = options["context"]
if context.lower() not in ["com", "reg", "prov"]:
raise Exception("Need to use a context among 'com', 'prov', 'reg'")
if context.lower() == 'reg':
orgs = Organization.objects.regioni()
cons_classification = "Consiglio regionale"
cons_role = "Presidente di consiglio regionale"
cons_incompatible_roles = [cons_role, "Consigliere regionale"]
cons_incompatible_roles_vice = [f"Vice{cons_role.lower()}", "Consigliere regionale"]
giunta_classification = "Giunta regionale"
giunta_role = "Presidente di Regione"
giunta_incompatible_roles = ["Vicepresidente di Regione", "Assessore regionale"]
elif context.lower() == 'prov':
orgs = Organization.objects.province()
cons_classification = "Consiglio provinciale"
cons_role = "Presidente di consiglio provinciale"
cons_incompatible_roles = [cons_role, "Consigliere provinciale"]
cons_incompatible_roles_vice = [f"Vice{cons_role.lower()}", "Consigliere provinciale"]
giunta_classification = "Giunta provinciale"
giunta_role = "Presidente di Provincia"
giunta_incompatible_roles = ["Vicepresidente di Provincia", "Assessore provinciale"]
else: # com is the only possibility left at this point
orgs = Organization.objects.comuni()
cons_classification = "Consiglio comunale"
cons_role = "Presidente di consiglio comunale"
cons_incompatible_roles = [cons_role, "Consigliere comunale"]
cons_incompatible_roles_vice = [f"Vice{cons_role.lower()}", "Consigliere comunale"]
giunta_classification = "Giunta comunale"
giunta_role = "Sindaco"
giunta_incompatible_roles = ["Vicesindaco", "Assessore comunale"]
min_ab = options.get('min_ab', None)
max_ab = options.get('max_ab', None)
self.logger.info("Start of process")
if min_ab:
orgs = orgs.filter(area__inhabitants__gte=min_ab, area__inhabitants__isnull=False)
if max_ab:
orgs = orgs.filter(area__inhabitants__lte=max_ab, area__inhabitants__isnull=False)
orgs = orgs.order_by(F('area__inhabitants').desc(nulls_last=True))
n_orgs = orgs.count()
self.logger.info(f"Processing {n_orgs} organizations")
for k, org in enumerate(orgs, start=1):
# overlap presidenti consiglio
cons_ov = self.check_overlapping_roles_in_org(org, cons_classification, cons_role)
# overlap apicali giunta
giunta_ov = self.check_overlapping_roles_in_org(org, giunta_classification, giunta_role)
# incompatibilità consiglio
cons_inc = self.check_incompatible_roles(org, cons_classification, cons_incompatible_roles)
cons_inc_vice = self.check_incompatible_roles(org, cons_classification, cons_incompatible_roles_vice)
# incompatibilità giunta
giunta_inc = self.check_incompatible_roles(org, giunta_classification, giunta_incompatible_roles)
# loop su tutti i casi: mostra etichetta comune di X solo se ci sono segnalazioni
_types = {
'cons_ov': {'label': "Consiglio", 'value': cons_ov},
'giunta_ov': {'label': "Giunta", 'value': giunta_ov},
'cons_inc': {'label': "Presidente del consiglio/Consigliere", 'value': cons_inc},
'cons_inc_vice': {'label': "Vicepresidente del consiglio/Consigliere", 'value': cons_inc_vice},
'giunta_inc': {'label': "Vicepresidente - Vicesindaco/Assessore", 'value': giunta_inc},
}
if any([i['value'] for i in _types.values()]):
self.logger.info(f"{k} - {org}")
for _, v in _types.items():
if v['value']:
self.logger.info(v['label'])
for r in v['value']:
self.logger.info(f" {r[0]['person__name']} {r[0]['label']}")
self.logger.info(f" dal {r[0]['start_date']} al {r[0]['end_date']}")
self.logger.info(f" dal {r[1]['start_date']} al {r[1]['end_date']}")
self.logger.info("End of process")
......@@ -14,7 +14,7 @@ class Command(LoggingBaseCommand):
Loop over all personal roles to see if some overlapping ones are found
"""
help = "Check overlapping roles"
help = "Check overlapping roles for persons"
epoch = dt.datetime(1970, 1, 1)
def add_arguments(self, parser):
......
......@@ -1827,3 +1827,46 @@ class Json2OpdmETLTest(SolrETLTest):
# the person in the similarity has now all roles
self.assertIsNotNone(matched_person.memberships.count(), 0)
def test_persons_with_classifications(self):
"""Test import of a person with classifications.
"""
from project.api_v1.tests.test_person import PersonTestCase
from popolo.models import Classification
classification_a = ClassificationFactory()
classification_b = ClassificationFactory()
classifications = [
{"classification": classification_a.id},
{"classification": classification_b.id},
]
n_classifications = len(classifications)
# create a person from the PersonTestCase get_basic_data method
# then add classifications and no memberships
p = PersonTestCase.get_basic_data()
p['classifications'] = classifications
p['memberships'] = []
self.setup_mock([p]) # setup_mock asks for a list of items
# define the instance and invoke the etl() method through __call__()
etl = ETL(
extractor=JsonArrayExtractor(
"https://s3.eu-central-1.amazonaws.com/opdm-service-data/parsers/test.json"
),
loader=PopoloPersonWithRelatedDetailsLoader(
context="ministeri", check_membership_label=True
),
log_level=0,
)
etl.extract().transform()
etl.load()
# total number of persons must be 1
self.assertEqual(Person.objects.count(), 1)
# the right number of classifications were imported
self.assertEqual(Classification.objects.count(), n_classifications)
self.assertEqual(Person.objects.first().classifications.count(), n_classifications)
......@@ -39,7 +39,7 @@ django-filter==2.2.0 # via -r requirements.in
django-haystack==2.8.1 # via -r requirements.in
django-model-utils==3.2.0 # via -r requirements.in, django-popolo
django-redis==4.11.0 # via -r requirements.in
django-uwsgi-taskmanager[notifications]==2.2.4 # via -r requirements.in
django-uwsgi-taskmanager[notifications]==2.2.5 # via -r requirements.in
django==2.2.13 # via -r requirements.in, django-braces, django-cors-headers, django-csv-export-view, django-debug-toolbar, django-filter, django-haystack, django-model-utils, django-popolo, django-redis, django-uwsgi-taskmanager, djangorestframework, djangorestframework-simplejwt, drf-rw-serializers, drf-yasg
djangorestframework-gis==0.15 # via -r requirements.in
djangorestframework-simplejwt==4.4.0 # via -r requirements.in
......
[bumpversion]
current_version = 1.2.2
current_version = 1.2.3
commit = True
tag = True
tag_name = v{new_version}
......