diff --git a/greg/admin.py b/greg/admin.py index abc671d7ef3355151b6bd76356ecce6164b1cd4a..296d00cebe0983521d33f559293e535c775e5996 100644 --- a/greg/admin.py +++ b/greg/admin.py @@ -4,8 +4,31 @@ from greg.models import ( Person, PersonRole, Role, + PersonIdentity, + Consent, + PersonConsent, + OrganizationalUnit, + Sponsor, + SponsorOrganizationalUnit, ) +admin.site.site_header = "Guest Registration Admin" + + +class RoleInline(admin.TabularInline): + model = PersonRole + extra = 1 + + +class PersonIdentityInline(admin.TabularInline): + model = PersonIdentity + extra = 1 + + +class ConsentInline(admin.TabularInline): + model = PersonConsent + extra = 1 + class PersonAdmin(admin.ModelAdmin): list_display = ( @@ -15,6 +38,7 @@ class PersonAdmin(admin.ModelAdmin): ) search_fields = ("first_name", "last_name") # TODO: "identities__value"? readonly_fields = ("id", "created", "updated") + inlines = (RoleInline, PersonIdentityInline, ConsentInline) def role_count(self, person): return str(person.roles.count()) @@ -37,6 +61,53 @@ class RoleAdmin(admin.ModelAdmin): readonly_fields = ("id", "created", "updated") +class PersonIdentityAdmin(admin.ModelAdmin): + list_display = ("id", "person", "type", "verified") + list_filter = ("verified",) + readonly_fields = ("id", "created", "updated") + + +class ConsentAdmin(admin.ModelAdmin): + list_display = ("id", "consent_name_en", "valid_from", "user_allowed_to_change") + readonly_fields = ("id", "created", "updated") + + +class PersonConsentAdmin(admin.ModelAdmin): + list_display = ("id", "person", "get_consent_name_en") + readonly_fields = ("id", "created", "updated") + + def get_consent_name_en(self, obj): + return obj.consent.consent_name_en + + get_consent_name_en.short_description = "Consent name" + + +class OrganizationalUnitAdmin(admin.ModelAdmin): + list_display = ("id", "name_en", "parent") + readonly_fields = ("id", "created", "updated") + + +class OrganizationalUnitInline(admin.TabularInline): + model = SponsorOrganizationalUnit + extra = 1 + + +class SponsorAdmin(admin.ModelAdmin): + list_display = ("id", "feide_id") + inlines = (OrganizationalUnitInline,) + readonly_fields = ("id", "created", "updated") + + +class SponsorOrganizationalUnitAdmin(admin.ModelAdmin): + readonly_fields = ("id", "created", "updated") + + admin.site.register(Person, PersonAdmin) admin.site.register(PersonRole, PersonRoleAdmin) admin.site.register(Role, RoleAdmin) +admin.site.register(PersonIdentity, PersonIdentityAdmin) +admin.site.register(Consent, ConsentAdmin) +admin.site.register(PersonConsent, PersonConsentAdmin) +admin.site.register(OrganizationalUnit, OrganizationalUnitAdmin) +admin.site.register(Sponsor, SponsorAdmin) +admin.site.register(SponsorOrganizationalUnit, SponsorOrganizationalUnitAdmin) diff --git a/greg/migrations/0001_initial.py b/greg/migrations/0001_initial.py index 4d782f4e1539199a6e98a9b67e8dacf118aad24d..7b7d22a9ab6213f16c893aeb695899daf2016709 100644 --- a/greg/migrations/0001_initial.py +++ b/greg/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 3.2.5 on 2021-07-13 06:47 +# Generated by Django 3.2.5 on 2021-07-14 12:28 import datetime import dirtyfields.dirtyfields @@ -75,10 +75,10 @@ class Migration(migrations.Migration): ('last_name', models.CharField(max_length=256)), ('date_of_birth', models.DateField()), ('email', models.EmailField(max_length=254)), - ('email_verified_date', models.DateField(blank=True, null=True)), + ('email_verified_date', models.DateField(null=True)), ('mobile_phone', models.CharField(max_length=15)), - ('mobile_phone_verified_date', models.DateField(blank=True, null=True)), - ('registration_completed_date', models.DateField(blank=True, null=True)), + ('mobile_phone_verified_date', models.DateField(null=True)), + ('registration_completed_date', models.DateField(null=True)), ('token', models.CharField(blank=True, max_length=32)), ], options={ @@ -159,7 +159,7 @@ class Migration(migrations.Migration): ('source', models.CharField(max_length=256)), ('value', models.CharField(max_length=256)), ('verified', models.CharField(blank=True, choices=[('AUTOMATIC', 'Automatic'), ('MANUAL', 'Manual')], max_length=9)), - ('verified_when', models.DateField(blank=True)), + ('verified_when', models.DateField(null=True)), ('person', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='person', to='greg.person')), ('verified_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='sponsor', to='greg.sponsor')), ], @@ -200,7 +200,7 @@ class Migration(migrations.Migration): ), migrations.AddConstraint( model_name='personrole', - constraint=models.UniqueConstraint(fields=('person', 'role'), name='personrole_person_role_unique'), + constraint=models.UniqueConstraint(fields=('person', 'role', 'unit'), name='personrole_person_role_unit_unique'), ), migrations.AddConstraint( model_name='personconsent', diff --git a/greg/models.py b/greg/models.py index 9e3a030443cd60a9471204eba7928a979fe44945..9e77c2626f97217679c15ca1a27711f413eca6bc 100644 --- a/greg/models.py +++ b/greg/models.py @@ -36,10 +36,10 @@ class Person(BaseModel): last_name = models.CharField(max_length=256) date_of_birth = models.DateField() email = models.EmailField() - email_verified_date = models.DateField(null=True, blank=True) + email_verified_date = models.DateField(null=True) mobile_phone = models.CharField(max_length=15) - mobile_phone_verified_date = models.DateField(null=True, blank=True) - registration_completed_date = models.DateField(null=True, blank=True) + mobile_phone_verified_date = models.DateField(null=True) + registration_completed_date = models.DateField(null=True) token = models.CharField(max_length=32, blank=True) roles = models.ManyToManyField("Role", through="PersonRole", related_name="persons") consents = models.ManyToManyField( @@ -106,7 +106,8 @@ class PersonRole(BaseModel): class Meta: constraints = [ models.UniqueConstraint( - fields=["person", "role"], name="personrole_person_role_unique" + fields=["person", "role", "unit"], + name="personrole_person_role_unit_unique", ) ] @@ -157,7 +158,7 @@ class PersonIdentity(BaseModel): verified_by = models.ForeignKey( "Sponsor", on_delete=models.PROTECT, related_name="sponsor", null=True ) - verified_when = models.DateField(blank=True) + verified_when = models.DateField(null=True) def __repr__(self): return ( diff --git a/greg/tests/populate_database.py b/greg/tests/populate_database.py new file mode 100644 index 0000000000000000000000000000000000000000..9d999aa55c216ca98504f144930b9f00d373ec01 --- /dev/null +++ b/greg/tests/populate_database.py @@ -0,0 +1,194 @@ +import random +from typing import List, TypeVar +from django.db import connection, IntegrityError + +from faker import Faker +from greg.models import ( + Person, + Role, + OrganizationalUnit, + Sponsor, + PersonRole, + Consent, + PersonIdentity, +) + +# Set seeds so that the generated data is always the same +random.seed(0) +Faker.seed(0) + +R = TypeVar("R") + + +def get_random_element_from_list(input_list: List[R]) -> R: + return input_list[random.randint(0, len(input_list) - 1)] + + +class DatabasePopulation: + """ + Helper class for populating database with random data. It can be useful to see how things look in the interface + for example. + + Run the file in the Django shell: exec(open('greg/tests/populate_database.py').read()) + """ + + faker: Faker + persons: List[Person] = [] + units: List[OrganizationalUnit] = [] + sponsors: List[Sponsor] = [] + role_types: List[Role] = [] + consents: List[Consent] = [] + + def __init__(self): + self.faker = Faker() + + def populate_database(self): + for i in range(10): + first_name = self.faker.first_name() + last_name = self.faker.last_name() + + self.persons.append( + Person.objects.create( + first_name=first_name, + last_name=last_name, + date_of_birth=self.faker.date_of_birth(maximum_age=50), + email=f"{first_name}.{last_name}@example.org", + mobile_phone=self.faker.phone_number(), + ) + ) + + for role_type in ("Visiting Professor", "Professor Emeritus", "Consultant"): + self.role_types.append( + Role.objects.create(type=role_type, name_en=role_type) + ) + + for i in range(10): + self.units.append( + OrganizationalUnit.objects.create( + orgreg_id=f"12345{i}", name_en=self.faker.company() + ) + ) + + for i in range(5): + self.sponsors.append( + Sponsor.objects.create( + feide_id=self.faker.bothify(text="???####@uio.no") + ) + ) + + for i in range(10): + self.consents.append( + Consent.objects.create( + type=self.faker.slug(), + consent_name_en=self.faker.sentence(nb_words=6), + consent_name_nb=self.faker.sentence(nb_words=6), + consent_description_en=self.faker.paragraph(nb_sentences=5), + consent_description_nb=self.faker.paragraph(nb_sentences=5), + consent_link_en=self.faker.url(), + user_allowed_to_change=random.random() > 0.5, + ) + ) + + self.__add_random_person_role_connections() + self.__add_random_person_consent_connections() + self.__add_random_sponsor_unit_connections() + self.__add_random_person_identification_connections() + + def __add_random_person_identification_connections(self, connections_to_create=5): + person_identifier_count = 0 + while person_identifier_count < connections_to_create: + person = get_random_element_from_list(self.persons) + identity_type = get_random_element_from_list( + PersonIdentity.IdentityType.choices + )[0] + + if random.random() > 0.5: + sponsor = get_random_element_from_list(self.sponsors) + verified_when = self.faker.date_this_year() + identity_type = get_random_element_from_list( + PersonIdentity.IdentityType.choices + )[0] + verified = self.faker.text(max_nb_chars=50) + else: + sponsor = None + verified_when = None + verified = "" + + PersonIdentity.objects.create( + person=person, + type=identity_type, + source=self.faker.text(max_nb_chars=50), + value=self.faker.numerify("##################"), + verified_by=sponsor, + verified=verified, + verified_when=verified_when, + ) + + person_identifier_count += 1 + + def __add_random_sponsor_unit_connections(self, connections_to_create=5): + sponsor_unit_count = 0 + while sponsor_unit_count < connections_to_create: + sponsor = get_random_element_from_list(self.sponsors) + unit = get_random_element_from_list(self.units) + + sponsor.units.add( + unit, through_defaults={"hierarchical_access": random.random() > 0.5} + ) + sponsor_unit_count += 1 + + def __add_random_person_role_connections(self, connections_to_create=5): + person_role_count = 0 + while person_role_count < connections_to_create: + try: + PersonRole.objects.create( + person=get_random_element_from_list(self.persons), + role=get_random_element_from_list(self.role_types), + unit=get_random_element_from_list(self.units), + start_date=self.faker.date_this_decade(), + end_date=self.faker.date_this_decade( + before_today=False, after_today=True + ), + contact_person_unit=self.faker.name(), + available_in_search=random.random() > 0.5, + registered_by=get_random_element_from_list(self.sponsors), + ) + person_role_count += 1 + except IntegrityError: + # This is probably caused by the same person added with the same role to the same unit. + # Try again and see if the randomly selected values will make it pass on the next attempt + pass + + def __add_random_person_consent_connections(self, number_of_connections_to_make=5): + person_consent_count = 0 + while person_consent_count < number_of_connections_to_make: + person = get_random_element_from_list(self.persons) + consent = get_random_element_from_list(self.consents) + + person.consents.add( + consent, + through_defaults={"consent_given_at": self.faker.date_this_decade()}, + ) + person_consent_count += 1 + + def truncate_tables(self): + with connection.cursor() as cursor: + for table in ( + "greg_personconsent", + "greg_consent", + "greg_notification", + "greg_personidentity", + "greg_personrole", + "greg_person", + "greg_sponsororganizationalunit", + "greg_sponsor", + "greg_organizationalunit", + "greg_role", + ): + cursor.execute(f"DELETE FROM {table}") + + +if __name__ == "__main__": + database_population = DatabasePopulation() + database_population.truncate_tables() + database_population.populate_database() diff --git a/pyproject.toml b/pyproject.toml index af1365272d20ec1f64cf33c3145721b8cbe86783..09d1747b14ba031d8a671a82f13fa700e273f369 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,6 +35,7 @@ pylint = "*" pylint-django = "*" rope = "*" autopep8 = "*" +Faker = "^8.10.1" [build-system] requires = ["poetry-core>=1.0.0"]