diff --git a/.gitignore b/.gitignore index 0bfe066ca2b05822fc5894d0fe721a8d99141b45..a6f1e7cba73e51fe9ea6c8c46071019bdeedc42f 100644 --- a/.gitignore +++ b/.gitignore @@ -10,8 +10,7 @@ .settings/ .venv/ .vscode/ -venv/ +venv*/ gregsite/db.sqlite3 gregsite/settings/local.py -gregsite/static/ diff --git a/.pylintrc b/.pylintrc index 5774872c0bb9c0b5ec06a055bdaee2760c318fc3..8bde7c7fa4876a5d976408de308cc62eb530c74a 100644 --- a/.pylintrc +++ b/.pylintrc @@ -5,6 +5,7 @@ django-settings-module=gregsite.settings [MESSAGES CONTROL] disable= + duplicate-code, fixme, import-outside-toplevel, invalid-name, @@ -13,6 +14,7 @@ disable= missing-function-docstring, missing-module-docstring, no-self-use, + redefined-outer-name, too-few-public-methods, too-many-ancestors, unused-argument, diff --git a/Makefile b/Makefile index 1172547917e9365efa0a0576cd694c6fe157e6ae..2b5bb24f46695e771b18d2afb1ff472dd92297b9 100644 --- a/Makefile +++ b/Makefile @@ -1,14 +1,18 @@ +DJANGO_SETTINGS_MODULE ?= gregsite.settings.dev + BLACK ?= black -q MYPY ?= mypy PIP ?= pip -q POETRY ?= poetry PYLINT ?= pylint -sn +PYTEST ?= pytest -v -s --no-header PYTHON ?= python3.9 VENV ?= venv mypy = $(MYPY) --config-file mypy.ini pip = python -m $(PIP) poetry = python -m $(POETRY) +pytest = DJANGO_SETTINGS_MODULE=$(DJANGO_SETTINGS_MODULE) python -m $(PYTEST) venv = . $(VENV)/bin/activate && PACKAGES = greg/ gregsite/ @@ -30,7 +34,7 @@ $(VENV)/touchfile: .PHONY: test test: $(VENV) $(venv) $(mypy) -p greg - $(venv) python manage.py test + $(venv) $(pytest) .PHONY: lint lint: $(VENV) diff --git a/greg/api/filters.py b/greg/api/filters.py index 226b3142277e532d7f54e475d4df83e331c1d97f..307fe3202146880b9c943e485356ac6e95558476 100644 --- a/greg/api/filters.py +++ b/greg/api/filters.py @@ -12,6 +12,10 @@ class PersonRoleFilter(filters.FilterSet): class PersonFilter(filters.FilterSet): + verified = filters.BooleanFilter( + field_name="person__verified_by_id", lookup_expr="isnull", exclude=True + ) + class Meta: model = Person - fields = ["first_name"] + fields = ["first_name", "last_name", "verified"] diff --git a/greg/api/serializers/consent.py b/greg/api/serializers/consent.py new file mode 100644 index 0000000000000000000000000000000000000000..5c1663f2da723d94f477a82e3fb97d9351892e35 --- /dev/null +++ b/greg/api/serializers/consent.py @@ -0,0 +1,9 @@ +from rest_framework.serializers import ModelSerializer + +from greg.models import Consent + + +class ConsentSerializer(ModelSerializer): + class Meta: + model = Consent + fields = "__all__" diff --git a/greg/api/serializers/organizational_unit.py b/greg/api/serializers/organizational_unit.py new file mode 100644 index 0000000000000000000000000000000000000000..12251bffff79f9119d76e9e1f7ddcc58c2984b68 --- /dev/null +++ b/greg/api/serializers/organizational_unit.py @@ -0,0 +1,9 @@ +from rest_framework.serializers import ModelSerializer + +from greg.models import OrganizationalUnit + + +class OrganizationalUnitSerializer(ModelSerializer): + class Meta: + model = OrganizationalUnit + fields = "__all__" diff --git a/greg/api/serializers/person.py b/greg/api/serializers/person.py index ba5b55ed0cfdaeb67930fa4b0e5b2e6ea5a34434..77eec13afec9c235552e84b503b085ee80c44a77 100644 --- a/greg/api/serializers/person.py +++ b/greg/api/serializers/person.py @@ -6,7 +6,14 @@ from greg.models import Person, PersonRole, Role class PersonSerializer(serializers.ModelSerializer): class Meta: model = Person - fields = ("id", "first_name", "last_name", "date_of_birth", "email", "mobile_phone") + fields = [ + "id", + "first_name", + "last_name", + "date_of_birth", + "email", + "mobile_phone", + ] class PersonRoleSerializer(serializers.ModelSerializer): @@ -16,6 +23,10 @@ class PersonRoleSerializer(serializers.ModelSerializer): model = PersonRole fields = [ "id", + "start_date", + "end_date", + "registered_by", + "unit", "created", "updated", "role", diff --git a/greg/api/serializers/sponsor.py b/greg/api/serializers/sponsor.py new file mode 100644 index 0000000000000000000000000000000000000000..57326558e9999d307412bed70d79665ae63c8821 --- /dev/null +++ b/greg/api/serializers/sponsor.py @@ -0,0 +1,9 @@ +from rest_framework import serializers + +from greg.models import Sponsor + + +class SponsorSerializer(serializers.ModelSerializer): + class Meta: + model = Sponsor + fields = ["id", "feide_id"] diff --git a/greg/api/urls.py b/greg/api/urls.py index 62ee2d499a1cc1ab3e866522c8dd7b8e6c1159f3..69743c28704421b8128bbe25b19f66d3e7412a42 100644 --- a/greg/api/urls.py +++ b/greg/api/urls.py @@ -1,15 +1,30 @@ -from django.conf.urls import url -from django.urls import path +from django.urls import ( + path, + re_path, +) from rest_framework.routers import DefaultRouter -from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView +from drf_spectacular.views import ( + SpectacularAPIView, + SpectacularSwaggerView, +) -from greg.api.views.person import PersonViewSet, PersonRoleViewSet +from greg.api.views.consent import ConsentViewSet +from greg.api.views.organizational_unit import OrganizationalUnitViewSet +from greg.api.views.person import ( + PersonRoleViewSet, + PersonViewSet, +) from greg.api.views.role import RoleViewSet from greg.api.views.health import Health +from greg.api.views.sponsor import SponsorViewSet router = DefaultRouter() router.register(r"persons", PersonViewSet, basename="person") router.register(r"roles", RoleViewSet, basename="role") +router.register(r"consents", ConsentViewSet, basename="consent") +router.register(r"sponsors", SponsorViewSet, basename="sponsor") +router.register(r"orgunit", OrganizationalUnitViewSet, basename="orgunit") + urlpatterns = router.urls @@ -21,12 +36,12 @@ urlpatterns += [ name="swagger-ui", ), path("health/", Health.as_view()), - url( + re_path( r"^persons/(?P<person_id>[0-9]+)/roles/$", - PersonRoleViewSet.as_view({"get": "list"}), + PersonRoleViewSet.as_view({"get": "list", "post": "create"}), name="person_role-list", ), - url( + re_path( r"^persons/(?P<person_id>[0-9]+)/roles/(?P<id>[0-9]+)/$", PersonRoleViewSet.as_view({"get": "retrieve"}), name="person_role-detail", diff --git a/greg/api/views/consent.py b/greg/api/views/consent.py new file mode 100644 index 0000000000000000000000000000000000000000..4996325e7cb9b40ffb64fde25a8d6b374e81d3e6 --- /dev/null +++ b/greg/api/views/consent.py @@ -0,0 +1,14 @@ +from rest_framework import viewsets + +from greg.api.pagination import PrimaryKeyCursorPagination +from greg.api.serializers.consent import ConsentSerializer +from greg.models import Consent + + +class ConsentViewSet(viewsets.ModelViewSet): + """Consent API""" + + queryset = Consent.objects.all().order_by("id") + serializer_class = ConsentSerializer + pagination_class = PrimaryKeyCursorPagination + lookup_field = "id" diff --git a/greg/api/views/organizational_unit.py b/greg/api/views/organizational_unit.py new file mode 100644 index 0000000000000000000000000000000000000000..c6d2236cd68f3318629a4595f4ee6d3f1755918e --- /dev/null +++ b/greg/api/views/organizational_unit.py @@ -0,0 +1,14 @@ +from rest_framework import viewsets + +from greg.api.pagination import PrimaryKeyCursorPagination +from greg.api.serializers.organizational_unit import OrganizationalUnitSerializer +from greg.models import OrganizationalUnit + + +class OrganizationalUnitViewSet(viewsets.ModelViewSet): + """OrganizationalUnit API""" + + queryset = OrganizationalUnit.objects.all().order_by("id") + serializer_class = OrganizationalUnitSerializer + pagination_class = PrimaryKeyCursorPagination + lookup_field = "id" diff --git a/greg/api/views/person.py b/greg/api/views/person.py index 556cc8ce2ade09badc0c386cd6c9da67869d4cee..3566e930e53ba46bc3f6682d0b59eddbc7e30632 100644 --- a/greg/api/views/person.py +++ b/greg/api/views/person.py @@ -1,10 +1,11 @@ +from django.core.exceptions import ValidationError from django_filters import rest_framework as filters - +from drf_spectacular.utils import extend_schema, OpenApiParameter from rest_framework import viewsets +from greg.api.filters import PersonFilter, PersonRoleFilter from greg.api.pagination import PrimaryKeyCursorPagination from greg.api.serializers.person import PersonSerializer, PersonRoleSerializer -from greg.api.filters import PersonFilter, PersonRoleFilter from greg.models import Person, PersonRole @@ -18,6 +19,20 @@ class PersonViewSet(viewsets.ModelViewSet): filter_backends = (filters.DjangoFilterBackend,) filterset_class = PersonFilter + @extend_schema( + parameters=[ + OpenApiParameter( + name="verified", + description="Only include verified or only not verified. When nothing is set, " + "both verified and not verified are returned", + required=False, + type=bool, + ) + ] + ) + def list(self, request, *args, **kwargs): + return super().list(request) + class PersonRoleViewSet(viewsets.ModelViewSet): """Person role API""" @@ -38,3 +53,12 @@ class PersonRoleViewSet(viewsets.ModelViewSet): if person_role_id: qs = qs.filter(id=person_role_id) return qs + + def perform_create(self, serializer): + person_id = self.kwargs["person_id"] + + if person_id is None: + # Should not happen, the person ID is part of the API path + raise ValidationError("No person id") + + serializer.save(person_id=person_id) diff --git a/greg/api/views/sponsor.py b/greg/api/views/sponsor.py new file mode 100644 index 0000000000000000000000000000000000000000..0251a2e86274d368d61508c2c937b1205e8ea6e3 --- /dev/null +++ b/greg/api/views/sponsor.py @@ -0,0 +1,14 @@ +from rest_framework.viewsets import ReadOnlyModelViewSet + +from greg.api.pagination import PrimaryKeyCursorPagination +from greg.api.serializers.sponsor import SponsorSerializer +from greg.models import Sponsor + + +class SponsorViewSet(ReadOnlyModelViewSet): + """Sponsor API""" + + queryset = Sponsor.objects.all().order_by("id") + serializer_class = SponsorSerializer + pagination_class = PrimaryKeyCursorPagination + lookup_field = "id" diff --git a/greg/apps.py b/greg/apps.py index d3307bd4c24714e32a3e648524e5a9724a5a8a5a..64d48ad212bcd358df71dbe338d43a234baa98fe 100644 --- a/greg/apps.py +++ b/greg/apps.py @@ -1,5 +1,5 @@ from django.apps import AppConfig -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ class GregAppConfig(AppConfig): diff --git a/greg/migrations/0001_initial.py b/greg/migrations/0001_initial.py index 7b7d22a9ab6213f16c893aeb695899daf2016709..45ff7241e90d4f660c2c1dccde38bcbddf44fd76 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-14 12:28 +# Generated by Django 3.2.5 on 2021-07-15 13:31 import datetime import dirtyfields.dirtyfields @@ -147,6 +147,9 @@ class Migration(migrations.Migration): ('role', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='person_roles', to='greg.role')), ('unit', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='unit_person_role', to='greg.organizationalunit')), ], + options={ + 'abstract': False, + }, bases=(dirtyfields.dirtyfields.DirtyFieldsMixin, models.Model), ), migrations.CreateModel( @@ -198,10 +201,6 @@ class Migration(migrations.Migration): model_name='sponsor', constraint=models.UniqueConstraint(fields=('feide_id',), name='unique_feide_id'), ), - migrations.AddConstraint( - model_name='personrole', - constraint=models.UniqueConstraint(fields=('person', 'role', 'unit'), name='personrole_person_role_unit_unique'), - ), migrations.AddConstraint( model_name='personconsent', constraint=models.UniqueConstraint(fields=('person', 'consent'), name='person_consent_unique'), diff --git a/greg/models.py b/greg/models.py index 4db81b57b4d6150a68a501e9ffb786ef927e3fbc..fc471b9133b4e04fb5d530d02d54441e70711bb3 100644 --- a/greg/models.py +++ b/greg/models.py @@ -103,14 +103,6 @@ class PersonRole(BaseModel): "Sponsor", on_delete=models.PROTECT, related_name="sponsor_role" ) - class Meta: - constraints = [ - models.UniqueConstraint( - fields=["person", "role", "unit"], - name="personrole_person_role_unit_unique", - ) - ] - def __repr__(self): return "{}(id={!r}, person={!r}, role={!r})".format( self.__class__.__name__, self.pk, self.person, self.role diff --git a/greg/tests/api/__init__.py b/greg/tests/api/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/greg/tests/api/test_consent.py b/greg/tests/api/test_consent.py new file mode 100644 index 0000000000000000000000000000000000000000..24ae8c837c73054d8edcad98abcefa1f39624e59 --- /dev/null +++ b/greg/tests/api/test_consent.py @@ -0,0 +1,30 @@ +import pytest +from rest_framework import status +from rest_framework.reverse import reverse + +from greg.models import Consent + + +@pytest.fixture +def consent_foo() -> Consent: + return Consent.objects.create( + type="test_consent", + consent_name_en="Test1", + consent_name_nb="Test2", + consent_description_en="Test description", + consent_description_nb="Test beskrivelse", + consent_link_en="https://example.org", + consent_link_nb="https://example.org", + valid_from="2018-01-20", + user_allowed_to_change=True, + ) + + +@pytest.mark.django_db +def test_get_consent(client, consent_foo): + resp = client.get(reverse("consent-detail", kwargs={"id": consent_foo.id})) + assert resp.status_code == status.HTTP_200_OK + data = resp.json() + assert data.get("id") == consent_foo.id + assert data.get("type") == consent_foo.type + assert data.get("consent_name_en") == consent_foo.consent_name_en diff --git a/greg/tests/api/test_person.py b/greg/tests/api/test_person.py new file mode 100644 index 0000000000000000000000000000000000000000..f79d9d26a2475b542cc22dda4c9dbb7ed7104ce4 --- /dev/null +++ b/greg/tests/api/test_person.py @@ -0,0 +1,96 @@ +import pytest + +from rest_framework import status +from rest_framework.reverse import reverse +from rest_framework.status import HTTP_200_OK + +from greg.models import Person + + +@pytest.fixture +def person_foo() -> Person: + return Person.objects.create( + first_name="Foo", + last_name="Foo", + date_of_birth="2001-01-27", + email="test@example.org", + mobile_phone="123456788", + ) + + +@pytest.fixture +def person_bar() -> Person: + return Person.objects.create( + first_name="Bar", + last_name="Bar", + date_of_birth="2000-07-01", + email="test2@example.org", + mobile_phone="123456789", + ) + + +@pytest.mark.django_db +def test_get_person(client, person_foo): + resp = client.get(reverse("person-detail", kwargs={"id": person_foo.id})) + assert resp.status_code == HTTP_200_OK + data = resp.json() + assert data.get("id") == person_foo.id + assert data.get("first_name") == person_foo.first_name + assert data.get("last_name") == person_foo.last_name + + +@pytest.mark.django_db +def test_persons(client, person_foo, person_bar): + resp = client.get(reverse("person-list")) + assert resp.status_code == HTTP_200_OK + data = resp.json() + assert len(data["results"]) == 2 + + +@pytest.mark.django_db +def test_persons_verified_filter_include(client, setup_db_test_data): + url = reverse("person-list") + response = client.get(url, {"verified": "true"}) + results = response.json()["results"] + assert len(results) == 1 + # The following person will have a verified identity set up for him + # in the test data + assert results[0]["first_name"] == "Christopher" + assert results[0]["last_name"] == "Flores" + + +@pytest.mark.django_db +def test_persons_verified_filter_exclude(client, setup_db_test_data): + url = reverse("person-list") + response = client.get(url, {"verified": "false"}) + results = response.json()["results"] + names = [(result["first_name"], result["last_name"]) for result in results] + assert len(results) == 9 + assert ("Christopher", "Flores") not in names + + +@pytest.mark.django_db +def test_add_role(client, person_foo): + url = reverse("person_role-list", kwargs={"person_id": person_foo.id}) + roles_for_person = client.get(url).json()["results"] + + # Check that there are no roles for the person, and then add one + assert len(roles_for_person) == 0 + + role_data = { + "role": "Visiting Professor", + "start_date": "2021-06-10", + "end_date": "2021-08-10", + "registered_by": "1", + "unit": "1", + } + response = client.post(url, role_data) + + assert response.status_code == status.HTTP_201_CREATED + + response_data = response.json() + roles_for_person = client.get(url).json()["results"] + + # Check that the role shows up when listing roles for the person now + assert len(roles_for_person) == 1 + assert roles_for_person[0]["id"] == response_data["id"] diff --git a/greg/tests/conftest.py b/greg/tests/conftest.py new file mode 100644 index 0000000000000000000000000000000000000000..bbdf24d3973f2d452e7e45e22ae1a81e2bcda7b5 --- /dev/null +++ b/greg/tests/conftest.py @@ -0,0 +1,34 @@ +import logging + +from rest_framework.authtoken.models import Token +from rest_framework.test import APIClient +from django.contrib.auth import get_user_model + + +# faker spams the logs with localisation warnings +# see https://github.com/joke2k/faker/issues/753 +import pytest + +from greg.tests.populate_database import DatabasePopulation + +logging.getLogger("faker").setLevel(logging.ERROR) + + +# The database is populated once when scope is session. +# If the scope is changed to function some additional +# logic is needed to make sure the old data is cleaned +# before the seeding is run again +@pytest.fixture(scope="session") +def setup_db_test_data(django_db_setup, django_db_blocker): + with django_db_blocker.unblock(): + database_seeder = DatabasePopulation() + database_seeder.populate_database() + + +@pytest.fixture +def client() -> APIClient: + user, _ = get_user_model().objects.get_or_create(username="test") + token, _ = Token.objects.get_or_create(user=user) + client = APIClient() + client.credentials(HTTP_AUTHORIZATION=f"Token {token.key}") + return client diff --git a/greg/tests/models/__init__.py b/greg/tests/models/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/greg/tests/models/test_consent.py b/greg/tests/models/test_consent.py new file mode 100644 index 0000000000000000000000000000000000000000..ad800bd596686e43520556a1acb2ec22aa2a76c6 --- /dev/null +++ b/greg/tests/models/test_consent.py @@ -0,0 +1,31 @@ +import pytest + +from greg.models import ( + Person, + Consent, +) + + +@pytest.fixture +def person() -> Person: + return Person.objects.create( + first_name="Test", + last_name="Tester", + date_of_birth="2000-01-27", + email="test@example.org", + mobile_phone="123456789", + ) + + +@pytest.mark.django_db +def test_add_consent_to_person(person): + consent = Consent.objects.create( + type="it_guidelines", + consent_name_en="IT Guidelines", + consent_name_nb="IT Regelverk", + consent_description_en="IT Guidelines description", + consent_description_nb="IT Regelverk beskrivelse", + consent_link_en="https://example.org/it_guidelines", + user_allowed_to_change=False, + ) + person.consents.add(consent, through_defaults={"consent_given_at": "2021-06-20"}) diff --git a/greg/tests/models/test_organizational_unit.py b/greg/tests/models/test_organizational_unit.py new file mode 100644 index 0000000000000000000000000000000000000000..dd61b87e016fc572805b2cb936c96f051a87f5d2 --- /dev/null +++ b/greg/tests/models/test_organizational_unit.py @@ -0,0 +1,10 @@ +import pytest + +from greg.models import OrganizationalUnit + + +@pytest.mark.django_db +def test_set_parent(): + parent = OrganizationalUnit.objects.create(orgreg_id="parent") + child = OrganizationalUnit.objects.create(orgreg_id="child", parent=parent) + assert list(OrganizationalUnit.objects.filter(parent__id=parent.id)) == [child] diff --git a/greg/tests/models/test_person.py b/greg/tests/models/test_person.py new file mode 100644 index 0000000000000000000000000000000000000000..fb851f06594a6aa9e9e5996c52cb794f9ff3622d --- /dev/null +++ b/greg/tests/models/test_person.py @@ -0,0 +1,76 @@ +from functools import partial + +import pytest + +from django.db.models import ProtectedError + +from greg.models import ( + OrganizationalUnit, + Person, + PersonRole, + Role, + Sponsor, +) + + +person_role_with = partial( + PersonRole.objects.create, + start_date="2020-03-05", + end_date="2020-06-10", + contact_person_unit="Contact Person", + available_in_search=True, +) + + +@pytest.fixture +def role_foo() -> Role: + return Role.objects.create(type="role_foo", name_en="Role Foo") + + +@pytest.fixture +def role_bar() -> Role: + return Role.objects.create(type="role_bar", name_en="Role Bar") + + +@pytest.fixture +def person(role_foo, role_bar) -> Person: + person = Person.objects.create( + first_name="Test", + last_name="Tester", + date_of_birth="2000-01-27", + email="test@example.org", + mobile_phone="123456789", + ) + + ou = OrganizationalUnit.objects.create(orgreg_id="12345", name_en="Test unit") + person_role_with( + person=person, + role=role_foo, + unit=ou, + registered_by=Sponsor.objects.create(feide_id="foosponsor@uio.no"), + ) + person_role_with( + person=person, + role=role_bar, + unit=ou, + registered_by=Sponsor.objects.create(feide_id="barsponsor@uio.no"), + ) + + return person + + +@pytest.mark.django_db +def test_add_multiple_roles_to_person(person, role_foo, role_bar): + person_roles = person.roles.all() + assert len(person_roles) == 2 + assert role_foo in person_roles + assert role_bar in person_roles + + +@pytest.mark.django_db +def test_delete_person_with_roles(person): + # it is not clear what cleanup needs to be done when removing a person, + # so for now it is prohibited to delete a person with role relationships + # attached in other tables + with pytest.raises(ProtectedError): + person.delete() diff --git a/greg/tests/models/test_sponsor.py b/greg/tests/models/test_sponsor.py new file mode 100644 index 0000000000000000000000000000000000000000..e82ec2a29d5276bace3bf1dde8c07c4456b37729 --- /dev/null +++ b/greg/tests/models/test_sponsor.py @@ -0,0 +1,52 @@ +from functools import partial + +import pytest + +from greg.models import ( + OrganizationalUnit, + Sponsor, + SponsorOrganizationalUnit, +) + + +sponsor_ou_relation = partial( + SponsorOrganizationalUnit.objects.create, + hierarchical_access=False, +) + + +@pytest.fixture +def sponsor_foo() -> Sponsor: + return Sponsor.objects.create(feide_id="foosponsor@uio.no") + + +@pytest.fixture +def sponsor_bar() -> Sponsor: + return Sponsor.objects.create(feide_id="barsponsor@uio.no") + + +@pytest.fixture +def unit1() -> OrganizationalUnit: + return OrganizationalUnit.objects.create(orgreg_id="1", name_en="First unit") + + +@pytest.fixture +def unit2() -> OrganizationalUnit: + return OrganizationalUnit.objects.create(orgreg_id="2", name_en="Second unit") + + +@pytest.mark.django_db +def test_add_sponsor_to_multiple_units(sponsor_foo, unit1, unit2): + sponsor_ou_relation(sponsor=sponsor_foo, organizational_unit=unit1) + sponsor_ou_relation(sponsor=sponsor_foo, organizational_unit=unit2) + assert list(sponsor_foo.units.all()) == [unit1, unit2] + + +@pytest.mark.django_db +def test_add_muliple_sponsors_to_unit(sponsor_foo, sponsor_bar, unit1, unit2): + sponsor_ou_relation(sponsor=sponsor_foo, organizational_unit=unit1) + sponsor_ou_relation(sponsor=sponsor_bar, organizational_unit=unit1) + assert list(sponsor_foo.units.all()) == [unit1] + assert list(sponsor_bar.units.all()) == [unit1] + assert list(Sponsor.objects.filter(units=unit1.id)) == [sponsor_foo, sponsor_bar] + assert list(Sponsor.objects.filter(units=unit2.id)) == [] diff --git a/greg/tests/populate_database.py b/greg/tests/populate_database.py index 9d999aa55c216ca98504f144930b9f00d373ec01..1dd064aabab4ffaea36a2546180f7ce6a3f310e8 100644 --- a/greg/tests/populate_database.py +++ b/greg/tests/populate_database.py @@ -13,17 +13,9 @@ from greg.models import ( 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 @@ -38,10 +30,21 @@ class DatabasePopulation: sponsors: List[Sponsor] = [] role_types: List[Role] = [] consents: List[Consent] = [] + random: random.Random - def __init__(self): + def __init__(self, set_seed_to_zero=True): self.faker = Faker() + if set_seed_to_zero: + # Set seeds so that the generated data is always the same + self.random = random.Random(0) + # Note that it is possible the logic the Faker uses + # to compute the random sequences can change when + # the version changes + self.faker.seed_instance(0) + else: + self.random = random.Random() + def populate_database(self): for i in range(10): first_name = self.faker.first_name() @@ -85,7 +88,7 @@ class DatabasePopulation: 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, + user_allowed_to_change=self.random.random() > 0.5, ) ) @@ -97,15 +100,15 @@ class DatabasePopulation: 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( + person = self.get_random_element_from_list(self.persons) + identity_type = self.get_random_element_from_list( PersonIdentity.IdentityType.choices )[0] - if random.random() > 0.5: - sponsor = get_random_element_from_list(self.sponsors) + if self.random.random() > 0.5: + sponsor = self.get_random_element_from_list(self.sponsors) verified_when = self.faker.date_this_year() - identity_type = get_random_element_from_list( + identity_type = self.get_random_element_from_list( PersonIdentity.IdentityType.choices )[0] verified = self.faker.text(max_nb_chars=50) @@ -129,11 +132,12 @@ class DatabasePopulation: 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 = self.get_random_element_from_list(self.sponsors) + unit = self.get_random_element_from_list(self.units) sponsor.units.add( - unit, through_defaults={"hierarchical_access": random.random() > 0.5} + unit, + through_defaults={"hierarchical_access": self.random.random() > 0.5}, ) sponsor_unit_count += 1 @@ -142,16 +146,16 @@ class DatabasePopulation: 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), + person=self.get_random_element_from_list(self.persons), + role=self.get_random_element_from_list(self.role_types), + unit=self.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), + available_in_search=self.random.random() > 0.5, + registered_by=self.get_random_element_from_list(self.sponsors), ) person_role_count += 1 except IntegrityError: @@ -162,8 +166,8 @@ class DatabasePopulation: 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 = self.get_random_element_from_list(self.persons) + consent = self.get_random_element_from_list(self.consents) person.consents.add( consent, @@ -187,6 +191,9 @@ class DatabasePopulation: ): cursor.execute(f"DELETE FROM {table}") + def get_random_element_from_list(self, input_list: List[R]) -> R: + return input_list[self.random.randint(0, len(input_list) - 1)] + if __name__ == "__main__": database_population = DatabasePopulation() diff --git a/greg/tests/test_api_person.py b/greg/tests/test_api_person.py deleted file mode 100644 index cc0b0932a73859aee37327cb38ced81f4187f77b..0000000000000000000000000000000000000000 --- a/greg/tests/test_api_person.py +++ /dev/null @@ -1,61 +0,0 @@ -from django.contrib.auth import get_user_model -from rest_framework import status -from rest_framework.authtoken.models import Token -from rest_framework.reverse import reverse -from rest_framework.test import ( - APIClient, - APITestCase, -) - -from greg.models import Person - - -class GregAPITestCase(APITestCase): - def setUp(self): - self.client = self.get_token_client() - - def get_token_client(self, username="test") -> APIClient: - self.user, _ = get_user_model().objects.get_or_create(username=username) - token, _ = Token.objects.get_or_create(user=self.user) - client = APIClient() - client.credentials(HTTP_AUTHORIZATION="Token {}".format(token.key)) - return client - - -class PersonTestData(GregAPITestCase): - def setUp(self): - super().setUp() - self.person_foo_data = dict( - first_name="Foo", - last_name="Foo", - date_of_birth="2000-01-27", - email="test@example.org", - mobile_phone="123456788", - ) - self.person_bar_data = dict( - first_name="Bar", - last_name="Bar", - date_of_birth="2000-07-01", - email="test2@example.org", - mobile_phone="123456789", - ) - self.person_foo = Person.objects.create(**self.person_foo_data) - self.person_bar = Person.objects.create(**self.person_bar_data) - - -class PersonAPITestCase(PersonTestData): - def test_get_person(self): - url = reverse("person-detail", kwargs={"id": self.person_foo.id}) - response = self.client.get(url) - data = response.json() - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(data.get("id"), self.person_foo.id) - self.assertEqual(data.get("first_name"), self.person_foo.first_name) - self.assertEqual(data.get("last_name"), self.person_foo.last_name) - - def test_persons(self): - url = reverse("person-list") - response = self.client.get(url) - data = response.json() - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(2, len(data["results"])) diff --git a/greg/tests/test_models.py b/greg/tests/test_models.py deleted file mode 100644 index 0c5879241c3466d76d0f9fb1c46b329e233873bf..0000000000000000000000000000000000000000 --- a/greg/tests/test_models.py +++ /dev/null @@ -1,182 +0,0 @@ -from django.db.models import ProtectedError -from django.test import TestCase - -from greg.models import ( - Person, - Role, - PersonRole, - OrganizationalUnit, - Sponsor, - SponsorOrganizationalUnit, - Consent, -) - - -class PersonModelTests(TestCase): - test_person = dict( - first_name="Test", - last_name="Tester", - date_of_birth="2000-01-27", - email="test@example.org", - mobile_phone="123456789", - ) - - def test_add_multiple_roles_to_person(self): - role1 = Role.objects.create(type="role1", name_en="Role 1") - role2 = Role.objects.create(type="role2", name_en="Role 2") - - unit = OrganizationalUnit.objects.create(orgreg_id="12345", name_en="Test unit") - sponsor = Sponsor.objects.create(feide_id="test@uio.no") - sponsor2 = Sponsor.objects.create(feide_id="test2@uio.no") - - person = Person.objects.create(**self.test_person) - - # Add two roles to the person and check that they appear when listing the roles for the person - PersonRole.objects.create( - person=person, - role=role1, - unit=unit, - start_date="2020-03-05", - end_date="2020-06-10", - contact_person_unit="Contact Person", - available_in_search=True, - registered_by=sponsor, - ) - - PersonRole.objects.create( - person=person, - role=role2, - unit=unit, - start_date="2021-03-05", - end_date="2021-06-10", - contact_person_unit="Contact Person", - available_in_search=True, - registered_by=sponsor2, - ) - - person_roles = person.roles.all() - - self.assertEqual(2, len(person_roles)) - self.assertIn(role1, person_roles) - self.assertIn(role2, person_roles) - - def test_person_not_allowed_deleted_when_roles_are_present(self): - person = Person.objects.create(**self.test_person) - role = Role.objects.create(type="role1", name_en="Role 1") - unit = OrganizationalUnit.objects.create(orgreg_id="12345", name_en="Test unit") - sponsor = Sponsor.objects.create(feide_id="test@uio.no") - - PersonRole.objects.create( - person=person, - role=role, - unit=unit, - start_date="2020-03-05", - end_date="2020-06-10", - contact_person_unit="Contact Person", - available_in_search=True, - registered_by=sponsor, - ) - - # It is not clear what cleanup needs to be done when a person is going to be - # removed, so for now is prohibited to delete a person if there is data - # attached to him in other tables - self.assertRaises(ProtectedError, person.delete) - - -class SponsorModelTests(TestCase): - def test_add_sponsor_to_multiple_units(self): - sponsor = Sponsor.objects.create(feide_id="test@uio.no") - - unit1 = OrganizationalUnit.objects.create( - orgreg_id="12345", name_en="Test unit" - ) - unit2 = OrganizationalUnit.objects.create( - orgreg_id="123456", name_en="Test unit2" - ) - - SponsorOrganizationalUnit.objects.create( - sponsor=sponsor, organizational_unit=unit1, hierarchical_access=False - ) - SponsorOrganizationalUnit.objects.create( - sponsor=sponsor, organizational_unit=unit2, hierarchical_access=False - ) - - sponsor_units = sponsor.units.all() - - self.assertEqual(2, len(sponsor_units)) - self.assertIn(unit1, sponsor_units) - self.assertIn(unit2, sponsor_units) - - def test_add_multiple_sponsors_to_unit(self): - sponsor1 = Sponsor.objects.create(feide_id="test@uio.no") - sponsor2 = Sponsor.objects.create(feide_id="test2@uio.no") - - unit = OrganizationalUnit.objects.create(orgreg_id="12345", name_en="Test unit") - unit2 = OrganizationalUnit.objects.create( - orgreg_id="123456", name_en="Test unit2" - ) - - SponsorOrganizationalUnit.objects.create( - sponsor=sponsor1, organizational_unit=unit, hierarchical_access=False - ) - SponsorOrganizationalUnit.objects.create( - sponsor=sponsor2, organizational_unit=unit, hierarchical_access=True - ) - - sponsor1_units = sponsor1.units.all() - self.assertEqual(1, len(sponsor1_units)) - self.assertIn(unit, sponsor1_units) - - sponsor2_units = sponsor2.units.all() - self.assertEqual(1, len(sponsor2_units)) - self.assertIn(unit, sponsor2_units) - - sponsors_for_unit = Sponsor.objects.filter(units=unit.id) - self.assertEqual(2, len(sponsors_for_unit)) - self.assertIn(sponsor1, sponsors_for_unit) - self.assertIn(sponsor2, sponsors_for_unit) - - sponsors_for_unit2 = Sponsor.objects.filter(units=unit2.id) - self.assertEqual(0, len(sponsors_for_unit2)) - - -class ConsentModelTest(TestCase): - def test_add_consent_to_person(self): - person = Person.objects.create( - first_name="Test", - last_name="Tester", - date_of_birth="2000-01-27", - email="test@example.org", - mobile_phone="123456789", - ) - - consent = Consent.objects.create( - type="it_guidelines", - consent_name_en="IT Guidelines", - consent_name_nb="IT Regelverk", - consent_description_en="IT Guidelines description", - consent_description_nb="IT Regelverk beskrivelse", - consent_link_en="https://example.org/it_guidelines", - user_allowed_to_change=False, - ) - - person.consents.add( - consent, through_defaults={"consent_given_at": "2021-06-20"} - ) - - -class OrganizationalUnitTest(TestCase): - def test_set_parent_for_unit(self): - parent = OrganizationalUnit.objects.create( - orgreg_id="12345", name_en="Parent unit", name_nb="Foreldreseksjon" - ) - child = OrganizationalUnit.objects.create( - orgreg_id="123456", - name_en="Child unit", - name_nb="Barneseksjon", - parent=parent, - ) - - query_result = OrganizationalUnit.objects.filter(parent__id=parent.id) - self.assertEqual(1, len(query_result)) - self.assertIn(child, query_result) diff --git a/gregsite/static/.gitignore b/gregsite/static/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..d6b7ef32c8478a48c3994dcadc86837f4371184d --- /dev/null +++ b/gregsite/static/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/poetry.lock b/poetry.lock index 9a2c461654d98b329cd50623b97ac5410c1934ea..2946530def28779d1376ffe1a6c906930952f4dd 100644 --- a/poetry.lock +++ b/poetry.lock @@ -37,6 +37,14 @@ python-versions = "~=3.6" lazy-object-proxy = ">=1.4.0" wrapt = ">=1.11,<1.13" +[[package]] +name = "atomicwrites" +version = "1.4.0" +description = "Atomic file writes." +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + [[package]] name = "attrs" version = "21.2.0" @@ -51,18 +59,6 @@ docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"] tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface"] tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins"] -[[package]] -name = "autopep8" -version = "1.5.7" -description = "A tool that automatically formats Python code to conform to the PEP 8 style guide" -category = "dev" -optional = false -python-versions = "*" - -[package.dependencies] -pycodestyle = ">=2.7.0" -toml = "*" - [[package]] name = "backcall" version = "0.2.0" @@ -328,6 +324,14 @@ category = "main" optional = false python-versions = ">=3.5" +[[package]] +name = "iniconfig" +version = "1.1.1" +description = "iniconfig: brain-dead simple config-ini parsing" +category = "dev" +optional = false +python-versions = "*" + [[package]] name = "ipython" version = "7.25.0" @@ -504,6 +508,17 @@ category = "dev" optional = false python-versions = "*" +[[package]] +name = "packaging" +version = "21.0" +description = "Core utilities for Python packages" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +pyparsing = ">=2.0.2" + [[package]] name = "parso" version = "0.8.2" @@ -574,6 +589,17 @@ url = "https://git.app.uib.no/it-bott-integrasjoner/pika-context-manager.git" reference = "v1.2.0" resolved_reference = "32fc2e04b6fc6528056cd0d8d0ca716abb5ab3be" +[[package]] +name = "pluggy" +version = "0.13.1" +description = "plugin and hook calling mechanisms for python" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[package.extras] +dev = ["pre-commit", "tox"] + [[package]] name = "prompt-toolkit" version = "3.0.19" @@ -602,9 +628,9 @@ optional = false python-versions = "*" [[package]] -name = "pycodestyle" -version = "2.7.0" -description = "Python style guide checker" +name = "py" +version = "1.10.0" +description = "library with cross-python path, ini-parsing, io, code, log facilities" category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" @@ -659,6 +685,14 @@ python-versions = "*" [package.dependencies] pylint = ">=1.7" +[[package]] +name = "pyparsing" +version = "2.4.7" +description = "Python parsing module" +category = "dev" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" + [[package]] name = "pyrsistent" version = "0.18.0" @@ -667,6 +701,42 @@ category = "main" optional = false python-versions = ">=3.6" +[[package]] +name = "pytest" +version = "6.2.4" +description = "pytest: simple powerful testing with Python" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} +attrs = ">=19.2.0" +colorama = {version = "*", markers = "sys_platform == \"win32\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=0.12,<1.0.0a1" +py = ">=1.8.2" +toml = "*" + +[package.extras] +testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] + +[[package]] +name = "pytest-django" +version = "4.4.0" +description = "A Django plugin for pytest." +category = "dev" +optional = false +python-versions = ">=3.5" + +[package.dependencies] +pytest = ">=5.4.0" + +[package.extras] +docs = ["sphinx", "sphinx-rtd-theme"] +testing = ["django", "django-configurations (>=2.0)"] + [[package]] name = "python-daemon" version = "2.3.0" @@ -880,7 +950,7 @@ python-versions = "*" [metadata] lock-version = "1.1" python-versions = "^3.9" -content-hash = "0c839c13a8f69537c16ef0d2b5bc1d5e103072a0b145c27b7b3ea08def26d3de" +content-hash = "afc64275e89091b126ea8c086a48db4714542e0abf1fd2ca475f6448045bdae1" [metadata.files] appdirs = [ @@ -899,14 +969,14 @@ astroid = [ {file = "astroid-2.6.2-py3-none-any.whl", hash = "sha256:606b2911d10c3dcf35e58d2ee5c97360e8477d7b9f3efc3f24811c93e6fc2cd9"}, {file = "astroid-2.6.2.tar.gz", hash = "sha256:38b95085e9d92e2ca06cf8b35c12a74fa81da395a6f9e65803742e6509c05892"}, ] +atomicwrites = [ + {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, + {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, +] attrs = [ {file = "attrs-21.2.0-py2.py3-none-any.whl", hash = "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1"}, {file = "attrs-21.2.0.tar.gz", hash = "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb"}, ] -autopep8 = [ - {file = "autopep8-1.5.7-py2.py3-none-any.whl", hash = "sha256:aa213493c30dcdac99537249ee65b24af0b2c29f2e83cd8b3f68760441ed0db9"}, - {file = "autopep8-1.5.7.tar.gz", hash = "sha256:276ced7e9e3cb22e5d7c14748384a5cf5d9002257c0ed50c0e075b68011bb6d0"}, -] backcall = [ {file = "backcall-0.2.0-py2.py3-none-any.whl", hash = "sha256:fbbce6a29f263178a1f7915c1940bde0ec2b2a967566fe1c65c1dfb7422bd255"}, {file = "backcall-0.2.0.tar.gz", hash = "sha256:5cbdbf27be5e7cfadb448baf0aa95508f91f2bbc6c6437cd9cd06e2a4c215e1e"}, @@ -999,6 +1069,10 @@ inflection = [ {file = "inflection-0.5.1-py2.py3-none-any.whl", hash = "sha256:f38b2b640938a4f35ade69ac3d053042959b62a0f1076a5bbaa1b9526605a8a2"}, {file = "inflection-0.5.1.tar.gz", hash = "sha256:1a29730d366e996aaacffb2f1f1cb9593dc38e2ddd30c91250c6dde09ea9b417"}, ] +iniconfig = [ + {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, + {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, +] ipython = [ {file = "ipython-7.25.0-py3-none-any.whl", hash = "sha256:aa21412f2b04ad1a652e30564fff6b4de04726ce875eab222c8430edc6db383a"}, {file = "ipython-7.25.0.tar.gz", hash = "sha256:54bbd1fe3882457aaf28ae060a5ccdef97f212a741754e420028d4ec5c2291dc"}, @@ -1128,6 +1202,10 @@ mypy-extensions = [ {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, ] +packaging = [ + {file = "packaging-21.0-py3-none-any.whl", hash = "sha256:c86254f9220d55e31cc94d69bade760f0847da8000def4dfe1c6b872fd14ff14"}, + {file = "packaging-21.0.tar.gz", hash = "sha256:7dc96269f53a4ccec5c0670940a4281106dd0bb343f47b7471f779df49c2fbe7"}, +] parso = [ {file = "parso-0.8.2-py2.py3-none-any.whl", hash = "sha256:a8c4922db71e4fdb90e0d0bc6e50f9b273d3397925e5e60a717e719201778d22"}, {file = "parso-0.8.2.tar.gz", hash = "sha256:12b83492c6239ce32ff5eed6d3639d6a536170723c6f3f1506869f1ace413398"}, @@ -1149,6 +1227,10 @@ pika = [ {file = "pika-1.2.0.tar.gz", hash = "sha256:f023d6ac581086b124190cb3dc81dd581a149d216fa4540ac34f9be1e3970b89"}, ] pika-context-manager = [] +pluggy = [ + {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"}, + {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"}, +] prompt-toolkit = [ {file = "prompt_toolkit-3.0.19-py3-none-any.whl", hash = "sha256:7089d8d2938043508aa9420ec18ce0922885304cddae87fb96eebca942299f88"}, {file = "prompt_toolkit-3.0.19.tar.gz", hash = "sha256:08360ee3a3148bdb5163621709ee322ec34fc4375099afa4bbf751e9b7b7fa4f"}, @@ -1188,9 +1270,9 @@ ptyprocess = [ {file = "ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35"}, {file = "ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220"}, ] -pycodestyle = [ - {file = "pycodestyle-2.7.0-py2.py3-none-any.whl", hash = "sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068"}, - {file = "pycodestyle-2.7.0.tar.gz", hash = "sha256:c389c1d06bf7904078ca03399a4816f974a1d590090fecea0c63ec26ebaf1cef"}, +py = [ + {file = "py-1.10.0-py2.py3-none-any.whl", hash = "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"}, + {file = "py-1.10.0.tar.gz", hash = "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3"}, ] pygments = [ {file = "Pygments-2.9.0-py3-none-any.whl", hash = "sha256:d66e804411278594d764fc69ec36ec13d9ae9147193a1740cd34d272ca383b8e"}, @@ -1208,6 +1290,10 @@ pylint-plugin-utils = [ {file = "pylint-plugin-utils-0.6.tar.gz", hash = "sha256:57625dcca20140f43731311cd8fd879318bf45a8b0fd17020717a8781714a25a"}, {file = "pylint_plugin_utils-0.6-py3-none-any.whl", hash = "sha256:2f30510e1c46edf268d3a195b2849bd98a1b9433229bb2ba63b8d776e1fc4d0a"}, ] +pyparsing = [ + {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"}, + {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"}, +] pyrsistent = [ {file = "pyrsistent-0.18.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f4c8cabb46ff8e5d61f56a037974228e978f26bfefce4f61a4b1ac0ba7a2ab72"}, {file = "pyrsistent-0.18.0-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:da6e5e818d18459fa46fac0a4a4e543507fe1110e808101277c5a2b5bab0cd2d"}, @@ -1231,6 +1317,14 @@ pyrsistent = [ {file = "pyrsistent-0.18.0-cp39-cp39-win_amd64.whl", hash = "sha256:404e1f1d254d314d55adb8d87f4f465c8693d6f902f67eb6ef5b4526dc58e6ea"}, {file = "pyrsistent-0.18.0.tar.gz", hash = "sha256:773c781216f8c2900b42a7b638d5b517bb134ae1acbebe4d1e8f1f41ea60eb4b"}, ] +pytest = [ + {file = "pytest-6.2.4-py3-none-any.whl", hash = "sha256:91ef2131a9bd6be8f76f1f08eac5c5317221d6ad1e143ae03894b862e8976890"}, + {file = "pytest-6.2.4.tar.gz", hash = "sha256:50bcad0a0b9c5a72c8e4e7c9855a3ad496ca6a881a3641b4260605450772c54b"}, +] +pytest-django = [ + {file = "pytest-django-4.4.0.tar.gz", hash = "sha256:b5171e3798bf7e3fc5ea7072fe87324db67a4dd9f1192b037fed4cc3c1b7f455"}, + {file = "pytest_django-4.4.0-py3-none-any.whl", hash = "sha256:65783e78382456528bd9d79a35843adde9e6a47347b20464eb2c885cb0f1f606"}, +] python-daemon = [ {file = "python-daemon-2.3.0.tar.gz", hash = "sha256:bda993f1623b1197699716d68d983bb580043cf2b8a66a01274d9b8297b0aeaf"}, {file = "python_daemon-2.3.0-py2.py3-none-any.whl", hash = "sha256:191c7b67b8f7aac58849abf54e19fe1957ef7290c914210455673028ad454989"}, diff --git a/pyproject.toml b/pyproject.toml index 43c9785c889dd16dcf5d3733ffc1b43041d4dfc1..90b2373de6d04aece61eea173bed1ddcf4e2380e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,6 +29,7 @@ ipython = "*" mypy = "*" pylint = "*" pylint-django = "*" +pytest-django = "^4.4.0" rope = "*" [tool.black]