From 160f58b6aadc817248133b675b0aa6e0de4e95cc Mon Sep 17 00:00:00 2001 From: J-E Castagnede Date: Fri, 28 Jun 2024 13:41:59 +0200 Subject: [PATCH 1/5] add api endpoints --- backend/dev-requirements.txt | 10 +-- backend/project/accounts/admin.py | 6 +- backend/project/accounts/managers.py | 1 - .../migrations/0002_remove_user_is_staff.py | 17 ++++ backend/project/accounts/models.py | 9 +- backend/project/api/__init__.py | 71 ---------------- backend/project/api/apps.py | 8 ++ backend/project/api/filters.py | 10 ++- backend/project/api/serializers/__init__.py | 0 backend/project/api/serializers/accounts.py | 28 +++++++ backend/project/api/serializers/common.py | 8 ++ .../{serializers.py => serializers/events.py} | 38 +-------- backend/project/api/urls.py | 36 +++++++- backend/project/api/views.py | 84 +++++++++++++++++++ .../migrations/0002_alter_media_uuid.py | 20 +++++ ...003_alter_event_source_alter_media_uuid.py | 37 ++++++++ backend/project/events/models.py | 2 +- backend/project/settings/__init__.py | 10 +++ backend/requirements.in | 5 +- backend/requirements.txt | 16 ++-- 20 files changed, 286 insertions(+), 130 deletions(-) create mode 100644 backend/project/accounts/migrations/0002_remove_user_is_staff.py create mode 100644 backend/project/api/apps.py create mode 100644 backend/project/api/serializers/__init__.py create mode 100644 backend/project/api/serializers/accounts.py create mode 100644 backend/project/api/serializers/common.py rename backend/project/api/{serializers.py => serializers/events.py} (70%) create mode 100644 backend/project/api/views.py create mode 100644 backend/project/events/migrations/0002_alter_media_uuid.py create mode 100644 backend/project/events/migrations/0003_alter_event_source_alter_media_uuid.py diff --git a/backend/dev-requirements.txt b/backend/dev-requirements.txt index 79efdcf..ab7942d 100644 --- a/backend/dev-requirements.txt +++ b/backend/dev-requirements.txt @@ -26,7 +26,7 @@ click==8.1.7 # via # black # pip-tools -coverage==7.5.3 +coverage==7.5.4 # via -r dev-requirements.in curtsies==0.4.2 # via bpython @@ -42,9 +42,9 @@ django-debug-toolbar==4.4.2 # via -r dev-requirements.in factory-boy==3.3.0 # via -r dev-requirements.in -faker==25.8.0 +faker==26.0.0 # via factory-boy -flake8==7.0.0 +flake8==7.1.0 # via -r dev-requirements.in greenlet==3.0.3 # via bpython @@ -67,7 +67,7 @@ pip-tools==7.4.1 # via -r dev-requirements.in platformdirs==4.2.2 # via black -pycodestyle==2.11.1 +pycodestyle==2.12.0 # via flake8 pyflakes==3.2.0 # via flake8 @@ -94,7 +94,7 @@ sqlparse==0.5.0 # django-debug-toolbar tblib==3.0.0 # via -r dev-requirements.in -urllib3==2.2.1 +urllib3==2.2.2 # via # -c requirements.txt # requests diff --git a/backend/project/accounts/admin.py b/backend/project/accounts/admin.py index 609d652..6625799 100644 --- a/backend/project/accounts/admin.py +++ b/backend/project/accounts/admin.py @@ -14,11 +14,14 @@ class UserAdmin(BaseUserAdmin): "first_name", "last_name", "is_active", - "is_staff", "is_superuser", "date_joined", "last_login", ) + list_filter = ( + "is_superuser", + "is_active", + ) search_fields = ("email", "first_name", "last_name") ordering = ("email",) fieldsets = ( @@ -37,7 +40,6 @@ class UserAdmin(BaseUserAdmin): { "fields": ( "is_active", - "is_staff", "is_superuser", "groups", "user_permissions", diff --git a/backend/project/accounts/managers.py b/backend/project/accounts/managers.py index 4065b93..95cf4da 100644 --- a/backend/project/accounts/managers.py +++ b/backend/project/accounts/managers.py @@ -12,7 +12,6 @@ def create_user(self, email, password=None, **extra_fields): return user def create_superuser(self, email, password=None, **extra_fields): - extra_fields.setdefault("is_staff", True) extra_fields.setdefault("is_superuser", True) return self.create_user(email, password, **extra_fields) diff --git a/backend/project/accounts/migrations/0002_remove_user_is_staff.py b/backend/project/accounts/migrations/0002_remove_user_is_staff.py new file mode 100644 index 0000000..0b9493f --- /dev/null +++ b/backend/project/accounts/migrations/0002_remove_user_is_staff.py @@ -0,0 +1,17 @@ +# Generated by Django 5.0.6 on 2024-06-28 07:59 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("accounts", "0001_initial"), + ] + + operations = [ + migrations.RemoveField( + model_name="user", + name="is_staff", + ), + ] diff --git a/backend/project/accounts/models.py b/backend/project/accounts/models.py index 57248e3..7a1642f 100644 --- a/backend/project/accounts/models.py +++ b/backend/project/accounts/models.py @@ -19,11 +19,6 @@ class User(AbstractBaseUser, PermissionsMixin): date_joined = models.DateTimeField( _("date joined"), default=timezone.now, db_default=Now() ) - is_staff = models.BooleanField( - _("staff status"), - default=False, - help_text=_("Designates whether the user can log into this admin site."), - ) is_active = models.BooleanField( _("active"), default=True, @@ -37,6 +32,10 @@ class User(AbstractBaseUser, PermissionsMixin): EMAIL_FIELD = "email" USERNAME_FIELD = "email" + @property + def is_staff(self): + return self.is_superuser + class Meta: verbose_name = _("user") verbose_name_plural = _("users") diff --git a/backend/project/api/__init__.py b/backend/project/api/__init__.py index d497b8c..e69de29 100644 --- a/backend/project/api/__init__.py +++ b/backend/project/api/__init__.py @@ -1,71 +0,0 @@ -from django_filters.rest_framework import DjangoFilterBackend -from rest_framework import status, viewsets -from rest_framework.decorators import action -from rest_framework.generics import GenericAPIView -from rest_framework.mixins import CreateModelMixin -from rest_framework.response import Response -from rest_framework.reverse import reverse -from rest_framework.viewsets import GenericViewSet - -from project.accounts.models import User -from project.api.filters import EventFilterSet -from project.api.serializers import ( - AccountSerializer, - EventDetailSerializer, - EventListSerializer, - EventTypeSerializer, - SettingsSerializer, -) -from project.events.models import Event, EventType - - -class SettingsApiView(GenericAPIView): - serializer_class = SettingsSerializer - - def get(self, request): - data = {} - event_types = EventType.objects.prefetch_related("sub_types").all() - event_types_serialized = EventTypeSerializer( - event_types, many=True, context={"request": request} - ) - data["event_types"] = event_types_serialized.data - data["event_url"] = reverse("api:events-list", request=request) - return Response(data) - - -class EventViewSet(viewsets.ModelViewSet): - filter_backends = (DjangoFilterBackend,) - filterset_class = EventFilterSet - - def get_serializer_class(self): - if self.action == "list": - return EventListSerializer - return EventDetailSerializer - - def get_queryset(self): - qs = Event.objects.all().select_related("source", "event_subtype__event_type") - if self.action not in ["list", "retrieve"] or self.request.user.is_anonymous: - # list and retrieve are public - if self.request.user.is_anonymous: - # anonymous can't change anything - qs = qs.none() - elif not self.request.user.is_staff and not self.request.user.is_superuser: - # nor anonymous, nor admin - qs = self.request.user.events.all() - return qs - - def perform_create(self, serializer): - observer = self.request.user if self.request.user.is_authenticated else None - serializer.save(observer=observer) - - -class AccountViewSet(CreateModelMixin, GenericViewSet): - queryset = User.objects.all() - serializer_class = AccountSerializer - - @action(detail=False, methods=["get"]) - def mine(self, request, *args, **kwargs): - if self.request.user.is_anonymous: - return Response({}, status=status.HTTP_404_NOT_FOUND) - serializer = self.get_serializer(request.user) - return Response(serializer.data) diff --git a/backend/project/api/apps.py b/backend/project/api/apps.py new file mode 100644 index 0000000..cd41906 --- /dev/null +++ b/backend/project/api/apps.py @@ -0,0 +1,8 @@ +from django.apps import AppConfig +from django.utils.translation import gettext_lazy as _ + + +class ApiConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "project.api" + verbose_name = _("API") diff --git a/backend/project/api/filters.py b/backend/project/api/filters.py index fb4c538..1eead9d 100644 --- a/backend/project/api/filters.py +++ b/backend/project/api/filters.py @@ -1,12 +1,18 @@ from django_filters.fields import DateRangeField -from django_filters.rest_framework import FilterSet +from django_filters.rest_framework import FilterSet, filters from project.events.models import Event class EventFilterSet(FilterSet): event_date = DateRangeField() + fields = filters.CharFilter( + method="filter_fields", help_text="filter fields you want to get" + ) + + def filter_fields(self, qs): + return qs class Meta: model = Event - fields = ["event_date", "event_subtype"] + fields = ["event_date", "event_subtype", "fields"] diff --git a/backend/project/api/serializers/__init__.py b/backend/project/api/serializers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/project/api/serializers/accounts.py b/backend/project/api/serializers/accounts.py new file mode 100644 index 0000000..796cecc --- /dev/null +++ b/backend/project/api/serializers/accounts.py @@ -0,0 +1,28 @@ +from django.contrib.auth.password_validation import validate_password +from rest_framework import serializers + +from project.accounts.models import User + + +class AccountSerializer(serializers.ModelSerializer): + password = serializers.CharField(write_only=True, validators=[validate_password]) + + def save(self, **kwargs): + super().save() + if "password" in self.validated_data: + self.instance.set_password(self.validated_data["password"]) + self.instance.save() + + return self.instance + + class Meta: + model = User + fields = ( + "id", + "email", + "is_superuser", + "last_name", + "first_name", + "password", + ) + read_only_fields = ("is_superuser",) diff --git a/backend/project/api/serializers/common.py b/backend/project/api/serializers/common.py new file mode 100644 index 0000000..afd4c61 --- /dev/null +++ b/backend/project/api/serializers/common.py @@ -0,0 +1,8 @@ +from rest_framework import serializers + +from project.api.serializers.events import EventTypeSerializer + + +class SettingsSerializer(serializers.Serializer): + event_types = EventTypeSerializer(many=True, read_only=True) + event_url = serializers.HyperlinkedIdentityField(view_name="api:events-list") diff --git a/backend/project/api/serializers.py b/backend/project/api/serializers/events.py similarity index 70% rename from backend/project/api/serializers.py rename to backend/project/api/serializers/events.py index 9594045..bdbba66 100644 --- a/backend/project/api/serializers.py +++ b/backend/project/api/serializers/events.py @@ -1,11 +1,10 @@ -from django.contrib.auth.password_validation import validate_password +from drf_dynamic_fields import DynamicFieldsMixin from drf_spectacular.types import OpenApiTypes from drf_spectacular.utils import extend_schema_field from rest_framework import serializers from rest_framework_gis import serializers as gis_serializers from sorl.thumbnail import get_thumbnail -from project.accounts.models import User from project.events.models import Event, EventSubType, EventType, Media @@ -28,11 +27,6 @@ class Meta: fields = ("id", "label", "description", "pictogram", "sub_types") -class SettingsSerializer(serializers.Serializer): - event_types = EventTypeSerializer(many=True, read_only=True) - event_url = serializers.HyperlinkedIdentityField(view_name="api:events-list") - - class ThumbnailSerializer(serializers.Serializer): small = serializers.SerializerMethodField() medium = serializers.SerializerMethodField() @@ -66,9 +60,10 @@ class Meta: fields = ("id", "uuid", "legend", "media_file", "media_type", "thumbnails") -class EventListSerializer(gis_serializers.GeoFeatureModelSerializer): +class EventListSerializer( + DynamicFieldsMixin, gis_serializers.GeoFeatureModelSerializer +): source = serializers.SlugRelatedField("label", read_only=True) - observer = serializers.SlugRelatedField("email", read_only=True) subtype = serializers.SlugRelatedField("label", read_only=True) # event_type = serializers.SlugRelatedField("event_subtype.event_type.label", read_only=True) @@ -78,7 +73,6 @@ class Meta: fields = ( "id", "uuid", - "observer", "comments", "event_date", "source", @@ -94,27 +88,3 @@ class EventDetailSerializer(EventListSerializer): class Meta(EventListSerializer.Meta): fields = EventListSerializer.Meta.fields + ("medias",) - - -class AccountSerializer(serializers.ModelSerializer): - password = serializers.CharField(write_only=True, validators=[validate_password]) - - def save(self, **kwargs): - password = self.validated_data.pop["password"] - super().save(**kwargs) - if password: - self.instance.set_password(password) - self.instance.save() - return self.instance - - class Meta: - model = User - fields = ( - "id", - "email", - "is_staff", - "is_superuser", - "last_name", - "first_name", - "password", - ) diff --git a/backend/project/api/urls.py b/backend/project/api/urls.py index c32ebe6..43fba66 100644 --- a/backend/project/api/urls.py +++ b/backend/project/api/urls.py @@ -2,15 +2,23 @@ from django.urls import include, path from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView from rest_framework.routers import DefaultRouter +from rest_framework_simplejwt.views import ( + TokenObtainPairView, + TokenRefreshView, + TokenVerifyView, +) -from project import api +from project.api import views as api +from project.api.views import AccountViewSet app_name = "api" router = DefaultRouter() router.register(r"events", api.EventViewSet, basename="events") -router.register(r"accounts", api.AccountViewSet, basename="accounts") +router.register( + r"accounts/me/events", api.AccountEventViewset, basename="account-events" +) urlpatterns = [ path( @@ -27,5 +35,29 @@ name="swagger-ui", ), path("api/settings/", api.SettingsApiView.as_view(), name="settings"), + path("api/token/", TokenObtainPairView.as_view(), name="token_obtain_pair"), + path("api/token/refresh/", TokenRefreshView.as_view(), name="token_refresh"), + path("api/token/verify/", TokenVerifyView.as_view(), name="token_verify"), + path( + "api/accounts/me/", + AccountViewSet.as_view( + { + "get": "retrieve", + "put": "update", + "patch": "partial_update", + "delete": "destroy", + } + ), + name="me", + ), + path( + "api/accounts/sign-up/", + AccountViewSet.as_view( + { + "post": "signup", + } + ), + name="me", + ), path("api/", include(router.urls)), ] diff --git a/backend/project/api/views.py b/backend/project/api/views.py new file mode 100644 index 0000000..0111281 --- /dev/null +++ b/backend/project/api/views.py @@ -0,0 +1,84 @@ +from django_filters.rest_framework import DjangoFilterBackend +from rest_framework import permissions, status, viewsets +from rest_framework.decorators import action +from rest_framework.generics import GenericAPIView +from rest_framework.mixins import ( + DestroyModelMixin, + RetrieveModelMixin, + UpdateModelMixin, +) +from rest_framework.response import Response +from rest_framework.reverse import reverse +from rest_framework.viewsets import GenericViewSet + +from project.accounts.models import User +from project.api.filters import EventFilterSet +from project.api.serializers.accounts import AccountSerializer +from project.api.serializers.common import SettingsSerializer +from project.api.serializers.events import ( + EventDetailSerializer, + EventListSerializer, + EventTypeSerializer, +) +from project.events.models import Event, EventType + + +class SettingsApiView(GenericAPIView): + serializer_class = SettingsSerializer + + def get(self, request): + data = {} + event_types = EventType.objects.prefetch_related("sub_types").all() + event_types_serialized = EventTypeSerializer( + event_types, many=True, context={"request": request} + ) + data["event_types"] = event_types_serialized.data + data["event_url"] = reverse("api:events-list", request=request) + return Response(data) + + +class EventViewSet(viewsets.ReadOnlyModelViewSet): + queryset = Event.objects.all().select_related("source", "event_subtype__event_type") + filter_backends = (DjangoFilterBackend,) + filterset_class = EventFilterSet + + def get_serializer_class(self): + if self.action == "list": + return EventListSerializer + return EventDetailSerializer + + +class AccountViewSet( + UpdateModelMixin, + RetrieveModelMixin, + DestroyModelMixin, + GenericViewSet, +): + queryset = User.objects.all() + serializer_class = AccountSerializer + permission_classes = [permissions.IsAuthenticated] + + def get_object(self): + return self.request.user + + @action(detail=False, methods=["post"], permission_classes=[permissions.AllowAny]) + def signup(self, request, *args, **kwargs): + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + serializer.save() + return Response(serializer.data, status=status.HTTP_201_CREATED) + + +class AccountEventViewset(viewsets.ModelViewSet): + permission_classes = [permissions.IsAuthenticated] + + def get_queryset(self): + return self.request.user.events.all() + + def get_serializer_class(self): + if self.action == "list": + return EventListSerializer + return EventDetailSerializer + + def perform_create(self, serializer): + serializer.save(owner=self.request.user) diff --git a/backend/project/events/migrations/0002_alter_media_uuid.py b/backend/project/events/migrations/0002_alter_media_uuid.py new file mode 100644 index 0000000..903c0c6 --- /dev/null +++ b/backend/project/events/migrations/0002_alter_media_uuid.py @@ -0,0 +1,20 @@ +# Generated by Django 5.0.6 on 2024-06-28 07:59 + +import uuid + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("events", "0001_initial"), + ] + + operations = [ + migrations.AlterField( + model_name="media", + name="uuid", + field=models.UUIDField(default=uuid.uuid4, editable=False, unique=True), + ), + ] diff --git a/backend/project/events/migrations/0003_alter_event_source_alter_media_uuid.py b/backend/project/events/migrations/0003_alter_event_source_alter_media_uuid.py new file mode 100644 index 0000000..b467dbe --- /dev/null +++ b/backend/project/events/migrations/0003_alter_event_source_alter_media_uuid.py @@ -0,0 +1,37 @@ +# Generated by Django 5.0.6 on 2024-06-28 08:49 + +import uuid + +import django.contrib.postgres.functions +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("events", "0002_alter_media_uuid"), + ] + + operations = [ + migrations.AlterField( + model_name="event", + name="source", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to="events.source", + ), + ), + migrations.AlterField( + model_name="media", + name="uuid", + field=models.UUIDField( + db_default=django.contrib.postgres.functions.RandomUUID(), + default=uuid.uuid4, + editable=False, + unique=True, + ), + ), + ] diff --git a/backend/project/events/models.py b/backend/project/events/models.py index 55487b3..a720427 100644 --- a/backend/project/events/models.py +++ b/backend/project/events/models.py @@ -58,7 +58,7 @@ class Event(TimeStampMixin): ) comments = models.TextField() event_date = models.DateField(default=timezone_today, db_index=True) - source = models.ForeignKey(Source, on_delete=models.PROTECT) + source = models.ForeignKey(Source, on_delete=models.SET_NULL, blank=True, null=True) event_subtype = models.ForeignKey(EventSubType, on_delete=models.PROTECT) location = models.PointField(srid=4326, verbose_name=_("Location")) diff --git a/backend/project/settings/__init__.py b/backend/project/settings/__init__.py index 44018cb..8bb7de6 100644 --- a/backend/project/settings/__init__.py +++ b/backend/project/settings/__init__.py @@ -53,12 +53,14 @@ "django.contrib.staticfiles", "django.contrib.postgres", "django.contrib.gis", + "rest_framework_simplejwt", "rest_framework", "rest_framework_gis", "drf_spectacular", "django_filters", "sorl.thumbnail", "project.accounts", + "project.api", "project.events", ] @@ -170,6 +172,10 @@ "rest_framework.throttling.UserRateThrottle", ], "DEFAULT_THROTTLE_RATES": {"anon": "100/day", "user": "1000/day"}, + "DEFAULT_AUTHENTICATION_CLASSES": ( + # "rest_framework.authentication.SessionAuthentication", + "rest_framework_simplejwt.authentication.JWTAuthentication", + ), } API_SWAGGER_SETTINGS = { @@ -195,3 +201,7 @@ "LOCATION": "memcached:11211", } } + +SIMPLE_JWT = { + "UPDATE_LAST_LOGIN": True, +} diff --git a/backend/requirements.in b/backend/requirements.in index 852255b..a8109d1 100644 --- a/backend/requirements.in +++ b/backend/requirements.in @@ -8,5 +8,6 @@ pillow sorl-thumbnail pymemcache sentry-sdk -pmtiles -gunicorn \ No newline at end of file +gunicorn +drf-dynamic-fields +djangorestframework-simplejwt \ No newline at end of file diff --git a/backend/requirements.txt b/backend/requirements.txt index 2e03843..de0d53e 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -17,16 +17,22 @@ django==5.0.6 # -r requirements.in # django-filter # djangorestframework + # djangorestframework-simplejwt # drf-spectacular django-filter==24.2 # via -r requirements.in -djangorestframework==3.15.1 +djangorestframework==3.15.2 # via # -r requirements.in # djangorestframework-gis + # djangorestframework-simplejwt # drf-spectacular djangorestframework-gis==1.0 # via -r requirements.in +djangorestframework-simplejwt==5.3.1 + # via -r requirements.in +drf-dynamic-fields==0.4.0 + # via -r requirements.in drf-spectacular==0.27.2 # via -r requirements.in gunicorn==22.0.0 @@ -41,10 +47,10 @@ packaging==24.1 # via gunicorn pillow==10.3.0 # via -r requirements.in -pmtiles==3.3.0 - # via -r requirements.in psycopg==3.1.19 # via -r requirements.in +pyjwt==2.8.0 + # via djangorestframework-simplejwt pymemcache==4.0.0 # via -r requirements.in pyyaml==6.0.1 @@ -57,7 +63,7 @@ rpds-py==0.18.1 # via # jsonschema # referencing -sentry-sdk==2.5.1 +sentry-sdk==2.7.0 # via -r requirements.in sorl-thumbnail==12.10.0 # via -r requirements.in @@ -67,5 +73,5 @@ typing-extensions==4.12.2 # via psycopg uritemplate==4.1.1 # via drf-spectacular -urllib3==2.2.1 +urllib3==2.2.2 # via sentry-sdk From 85406aacf79225c15e216aa4453b248247c03802 Mon Sep 17 00:00:00 2001 From: J-E Castagnede Date: Fri, 28 Jun 2024 13:54:19 +0200 Subject: [PATCH 2/5] add tests --- backend/project/api/tests/__init__.py | 0 backend/project/api/tests/factories.py | 17 +++++++++++++++ backend/project/api/tests/test_serializers.py | 21 +++++++++++++++++++ 3 files changed, 38 insertions(+) create mode 100644 backend/project/api/tests/__init__.py create mode 100644 backend/project/api/tests/factories.py create mode 100644 backend/project/api/tests/test_serializers.py diff --git a/backend/project/api/tests/__init__.py b/backend/project/api/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/project/api/tests/factories.py b/backend/project/api/tests/factories.py new file mode 100644 index 0000000..6818cf6 --- /dev/null +++ b/backend/project/api/tests/factories.py @@ -0,0 +1,17 @@ +import factory +from factory import faker + +from project.accounts.models import User + + +class UserFactory(factory.django.DjangoModelFactory): + email = faker.Faker("email") + password = factory.PostGenerationMethodCall("set_password", "password") + is_active = True + + class Meta: + model = User + + +class SuperUserFactory(UserFactory): + is_superuser = True diff --git a/backend/project/api/tests/test_serializers.py b/backend/project/api/tests/test_serializers.py new file mode 100644 index 0000000..bac6647 --- /dev/null +++ b/backend/project/api/tests/test_serializers.py @@ -0,0 +1,21 @@ +from django.test import TestCase + +from project.api.serializers.accounts import AccountSerializer +from project.api.tests.factories import UserFactory + + +class AccountSerializerTestCase(TestCase): + def test_password_check_success_after_change(self): + """Test password change method""" + user = UserFactory(password="password") + self.assertTrue(user.check_password("password")) + + serializer = AccountSerializer( + instance=user, data={"email": user.email, "password": "new_password"} + ) + self.assertTrue(serializer.is_valid(), serializer.errors) + + serializer.save() + + user.refresh_from_db() + self.assertTrue(user.check_password("new_password")) From 8758e0b1941f6d19c4d402e36f8d0f5505a82098 Mon Sep 17 00:00:00 2001 From: J-E Castagnede Date: Fri, 28 Jun 2024 15:05:53 +0200 Subject: [PATCH 3/5] enable basic auth --- backend/project/settings/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/project/settings/__init__.py b/backend/project/settings/__init__.py index 8bb7de6..609e786 100644 --- a/backend/project/settings/__init__.py +++ b/backend/project/settings/__init__.py @@ -173,7 +173,7 @@ ], "DEFAULT_THROTTLE_RATES": {"anon": "100/day", "user": "1000/day"}, "DEFAULT_AUTHENTICATION_CLASSES": ( - # "rest_framework.authentication.SessionAuthentication", + "rest_framework.authentication.BasicAuthentication", "rest_framework_simplejwt.authentication.JWTAuthentication", ), } From db6c77ef72a1d0523443cb5f827b042a0d0138a3 Mon Sep 17 00:00:00 2001 From: J-E Castagnede Date: Fri, 28 Jun 2024 15:24:37 +0200 Subject: [PATCH 4/5] change event to Observation --- .../accounts/migrations/0001_initial.py | 10 +-- .../migrations/0002_remove_user_is_staff.py | 17 ----- .../{events => accounts/tests}/__init__.py | 0 backend/project/accounts/tests/test_admin.py | 6 ++ backend/project/accounts/tests/test_models.py | 6 ++ backend/project/api/filters.py | 8 +-- backend/project/api/serializers/common.py | 9 +-- .../{events.py => observations.py} | 45 ++++++++----- backend/project/api/urls.py | 6 +- backend/project/api/views.py | 48 +++++++------ .../migrations/0002_alter_media_uuid.py | 20 ------ ...003_alter_event_source_alter_media_uuid.py | 37 ---------- .../migrations => observations}/__init__.py | 0 .../project/{events => observations}/admin.py | 37 +++++----- .../project/{events => observations}/apps.py | 4 +- .../locale/en/LC_MESSAGES/django.po | 12 ++-- .../locale/fr/LC_MESSAGES/django.po | 24 +++---- .../migrations/0001_initial.py | 67 ++++++++++--------- .../migrations/__init__.py} | 0 .../{events => observations}/models.py | 45 ++++++++----- backend/project/observations/tests.py | 0 backend/project/settings/__init__.py | 2 +- 22 files changed, 186 insertions(+), 217 deletions(-) delete mode 100644 backend/project/accounts/migrations/0002_remove_user_is_staff.py rename backend/project/{events => accounts/tests}/__init__.py (100%) create mode 100644 backend/project/accounts/tests/test_admin.py create mode 100644 backend/project/accounts/tests/test_models.py rename backend/project/api/serializers/{events.py => observations.py} (63%) delete mode 100644 backend/project/events/migrations/0002_alter_media_uuid.py delete mode 100644 backend/project/events/migrations/0003_alter_event_source_alter_media_uuid.py rename backend/project/{events/migrations => observations}/__init__.py (100%) rename backend/project/{events => observations}/admin.py (77%) rename backend/project/{events => observations}/apps.py (56%) rename backend/project/{events => observations}/locale/en/LC_MESSAGES/django.po (89%) rename backend/project/{events => observations}/locale/fr/LC_MESSAGES/django.po (73%) rename backend/project/{events => observations}/migrations/0001_initial.py (85%) rename backend/project/{events/tests.py => observations/migrations/__init__.py} (100%) rename backend/project/{events => observations}/models.py (64%) create mode 100644 backend/project/observations/tests.py diff --git a/backend/project/accounts/migrations/0001_initial.py b/backend/project/accounts/migrations/0001_initial.py index b1c1031..a0ca2f3 100644 --- a/backend/project/accounts/migrations/0001_initial.py +++ b/backend/project/accounts/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 5.0.6 on 2024-06-12 20:15 +# Generated by Django 5.0.6 on 2024-06-28 13:23 import django.db.models.functions.datetime import django.utils.timezone @@ -72,14 +72,6 @@ class Migration(migrations.Migration): verbose_name="date joined", ), ), - ( - "is_staff", - models.BooleanField( - default=False, - help_text="Designates whether the user can log into this admin site.", - verbose_name="staff status", - ), - ), ( "is_active", models.BooleanField( diff --git a/backend/project/accounts/migrations/0002_remove_user_is_staff.py b/backend/project/accounts/migrations/0002_remove_user_is_staff.py deleted file mode 100644 index 0b9493f..0000000 --- a/backend/project/accounts/migrations/0002_remove_user_is_staff.py +++ /dev/null @@ -1,17 +0,0 @@ -# Generated by Django 5.0.6 on 2024-06-28 07:59 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ("accounts", "0001_initial"), - ] - - operations = [ - migrations.RemoveField( - model_name="user", - name="is_staff", - ), - ] diff --git a/backend/project/events/__init__.py b/backend/project/accounts/tests/__init__.py similarity index 100% rename from backend/project/events/__init__.py rename to backend/project/accounts/tests/__init__.py diff --git a/backend/project/accounts/tests/test_admin.py b/backend/project/accounts/tests/test_admin.py new file mode 100644 index 0000000..ca59b8a --- /dev/null +++ b/backend/project/accounts/tests/test_admin.py @@ -0,0 +1,6 @@ +from django.test import TestCase + + +class AccountAdminTestCase(TestCase): + def test_superuser_ca(self): + """Test that only superuser can login to the admin site.""" diff --git a/backend/project/accounts/tests/test_models.py b/backend/project/accounts/tests/test_models.py new file mode 100644 index 0000000..f6349a4 --- /dev/null +++ b/backend/project/accounts/tests/test_models.py @@ -0,0 +1,6 @@ +from django.test import TestCase + + +class AccountTestCase(TestCase): + def test_is_staff(self): + """Test that is_staff is same value as is_superuser""" diff --git a/backend/project/api/filters.py b/backend/project/api/filters.py index 1eead9d..4c2fb13 100644 --- a/backend/project/api/filters.py +++ b/backend/project/api/filters.py @@ -1,10 +1,10 @@ from django_filters.fields import DateRangeField from django_filters.rest_framework import FilterSet, filters -from project.events.models import Event +from project.observations.models import Observation -class EventFilterSet(FilterSet): +class ObservationFilterSet(FilterSet): event_date = DateRangeField() fields = filters.CharFilter( method="filter_fields", help_text="filter fields you want to get" @@ -14,5 +14,5 @@ def filter_fields(self, qs): return qs class Meta: - model = Event - fields = ["event_date", "event_subtype", "fields"] + model = Observation + fields = ["event_date", "observation_subtype", "fields"] diff --git a/backend/project/api/serializers/common.py b/backend/project/api/serializers/common.py index afd4c61..78e0ef5 100644 --- a/backend/project/api/serializers/common.py +++ b/backend/project/api/serializers/common.py @@ -1,8 +1,9 @@ +from project.api.serializers.observations import ObservationTypeSerializer from rest_framework import serializers -from project.api.serializers.events import EventTypeSerializer - class SettingsSerializer(serializers.Serializer): - event_types = EventTypeSerializer(many=True, read_only=True) - event_url = serializers.HyperlinkedIdentityField(view_name="api:events-list") + observation_types = ObservationTypeSerializer(many=True, read_only=True) + observation_public_url = serializers.HyperlinkedIdentityField( + view_name="api:observations-list" + ) diff --git a/backend/project/api/serializers/events.py b/backend/project/api/serializers/observations.py similarity index 63% rename from backend/project/api/serializers/events.py rename to backend/project/api/serializers/observations.py index bdbba66..4ac5446 100644 --- a/backend/project/api/serializers/events.py +++ b/backend/project/api/serializers/observations.py @@ -5,12 +5,17 @@ from rest_framework_gis import serializers as gis_serializers from sorl.thumbnail import get_thumbnail -from project.events.models import Event, EventSubType, EventType, Media +from project.observations.models import ( + ObservationSubType, + ObservationType, + Media, + Observation, +) -class EventSubTypeSerializer(serializers.ModelSerializer): +class ObservationSubTypeSerializer(serializers.ModelSerializer): class Meta: - model = EventSubType + model = ObservationSubType fields = ( "id", "label", @@ -19,11 +24,11 @@ class Meta: ) -class EventTypeSerializer(serializers.ModelSerializer): - sub_types = EventSubTypeSerializer(many=True) +class ObservationTypeSerializer(serializers.ModelSerializer): + sub_types = ObservationSubTypeSerializer(many=True) class Meta: - model = EventType + model = ObservationType fields = ("id", "label", "description", "pictogram", "sub_types") @@ -32,24 +37,28 @@ class ThumbnailSerializer(serializers.Serializer): medium = serializers.SerializerMethodField() large = serializers.SerializerMethodField() - def get_thumbnail_by_size(self, obj, height=100, width=100, format="PNG"): + def get_thumbnail_by_size( + self, obj, height=100, width=100, format="JPG", quality=70 + ): if obj.media_type == Media.MediaType.IMAGE: return self.context["request"].build_absolute_uri( - get_thumbnail(obj.media_file, f"{height}x{width}", format="PNG").url + get_thumbnail( + obj.media_file, f"{height}x{width}", format=format, quality=quality + ).url ) return None @extend_schema_field(OpenApiTypes.URI) def get_small(self, obj): - return self.get_thumbnail_by_size(obj, 100, 100) + return self.get_thumbnail_by_size(obj, 449, 599) @extend_schema_field(OpenApiTypes.URI) def get_medium(self, obj): - return self.get_thumbnail_by_size(obj, 300, 300) + return self.get_thumbnail_by_size(obj, 720, 1280) @extend_schema_field(OpenApiTypes.URI) def get_large(self, obj): - return self.get_thumbnail_by_size(obj, 600, 600) + return self.get_thumbnail_by_size(obj, 1080, 1920) class MediaSerializer(serializers.ModelSerializer): @@ -60,7 +69,7 @@ class Meta: fields = ("id", "uuid", "legend", "media_file", "media_type", "thumbnails") -class EventListSerializer( +class ObservationListSerializer( DynamicFieldsMixin, gis_serializers.GeoFeatureModelSerializer ): source = serializers.SlugRelatedField("label", read_only=True) @@ -68,7 +77,7 @@ class EventListSerializer( # event_type = serializers.SlugRelatedField("event_subtype.event_type.label", read_only=True) class Meta: - model = Event + model = Observation geo_field = "location" fields = ( "id", @@ -77,14 +86,14 @@ class Meta: "event_date", "source", "subtype", - "event_subtype", + "observation_subtype", "location", ) - write_only_fields = ("event_subtype__id",) + write_only_fields = ("observation_subtype__id",) -class EventDetailSerializer(EventListSerializer): +class ObservationDetailSerializer(ObservationListSerializer): medias = MediaSerializer(many=True, read_only=True) - class Meta(EventListSerializer.Meta): - fields = EventListSerializer.Meta.fields + ("medias",) + class Meta(ObservationListSerializer.Meta): + fields = ObservationListSerializer.Meta.fields + ("medias",) diff --git a/backend/project/api/urls.py b/backend/project/api/urls.py index 43fba66..a392507 100644 --- a/backend/project/api/urls.py +++ b/backend/project/api/urls.py @@ -15,9 +15,11 @@ router = DefaultRouter() -router.register(r"events", api.EventViewSet, basename="events") +router.register(r"observations", api.ObservationViewSet, basename="observations") router.register( - r"accounts/me/events", api.AccountEventViewset, basename="account-events" + r"accounts/me/observations", + api.AccountObservationViewset, + basename="account-observations", ) urlpatterns = [ diff --git a/backend/project/api/views.py b/backend/project/api/views.py index 0111281..2086282 100644 --- a/backend/project/api/views.py +++ b/backend/project/api/views.py @@ -1,4 +1,10 @@ from django_filters.rest_framework import DjangoFilterBackend +from project.api.serializers.observations import ( + ObservationDetailSerializer, + ObservationListSerializer, + ObservationTypeSerializer, +) +from project.observations.models import Observation, ObservationType from rest_framework import permissions, status, viewsets from rest_framework.decorators import action from rest_framework.generics import GenericAPIView @@ -12,15 +18,9 @@ from rest_framework.viewsets import GenericViewSet from project.accounts.models import User -from project.api.filters import EventFilterSet +from project.api.filters import ObservationFilterSet from project.api.serializers.accounts import AccountSerializer from project.api.serializers.common import SettingsSerializer -from project.api.serializers.events import ( - EventDetailSerializer, - EventListSerializer, - EventTypeSerializer, -) -from project.events.models import Event, EventType class SettingsApiView(GenericAPIView): @@ -28,24 +28,28 @@ class SettingsApiView(GenericAPIView): def get(self, request): data = {} - event_types = EventType.objects.prefetch_related("sub_types").all() - event_types_serialized = EventTypeSerializer( - event_types, many=True, context={"request": request} + observation_types = ObservationType.objects.prefetch_related("sub_types").all() + observation_types_serialized = ObservationTypeSerializer( + observation_types, many=True, context={"request": request} + ) + data["observation_types"] = observation_types_serialized.data + data["observation_public_url"] = reverse( + "api:observations-list", request=request ) - data["event_types"] = event_types_serialized.data - data["event_url"] = reverse("api:events-list", request=request) return Response(data) -class EventViewSet(viewsets.ReadOnlyModelViewSet): - queryset = Event.objects.all().select_related("source", "event_subtype__event_type") +class ObservationViewSet(viewsets.ReadOnlyModelViewSet): + queryset = Observation.objects.all().select_related( + "source", "observation_subtype__observation_type" + ) filter_backends = (DjangoFilterBackend,) - filterset_class = EventFilterSet + filterset_class = ObservationFilterSet def get_serializer_class(self): if self.action == "list": - return EventListSerializer - return EventDetailSerializer + return ObservationListSerializer + return ObservationDetailSerializer class AccountViewSet( @@ -69,16 +73,18 @@ def signup(self, request, *args, **kwargs): return Response(serializer.data, status=status.HTTP_201_CREATED) -class AccountEventViewset(viewsets.ModelViewSet): +class AccountObservationViewset(viewsets.ModelViewSet): + filter_backends = (DjangoFilterBackend,) + filterset_class = ObservationFilterSet permission_classes = [permissions.IsAuthenticated] def get_queryset(self): - return self.request.user.events.all() + return self.request.user.observations.all() def get_serializer_class(self): if self.action == "list": - return EventListSerializer - return EventDetailSerializer + return ObservationListSerializer + return ObservationDetailSerializer def perform_create(self, serializer): serializer.save(owner=self.request.user) diff --git a/backend/project/events/migrations/0002_alter_media_uuid.py b/backend/project/events/migrations/0002_alter_media_uuid.py deleted file mode 100644 index 903c0c6..0000000 --- a/backend/project/events/migrations/0002_alter_media_uuid.py +++ /dev/null @@ -1,20 +0,0 @@ -# Generated by Django 5.0.6 on 2024-06-28 07:59 - -import uuid - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ("events", "0001_initial"), - ] - - operations = [ - migrations.AlterField( - model_name="media", - name="uuid", - field=models.UUIDField(default=uuid.uuid4, editable=False, unique=True), - ), - ] diff --git a/backend/project/events/migrations/0003_alter_event_source_alter_media_uuid.py b/backend/project/events/migrations/0003_alter_event_source_alter_media_uuid.py deleted file mode 100644 index b467dbe..0000000 --- a/backend/project/events/migrations/0003_alter_event_source_alter_media_uuid.py +++ /dev/null @@ -1,37 +0,0 @@ -# Generated by Django 5.0.6 on 2024-06-28 08:49 - -import uuid - -import django.contrib.postgres.functions -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ("events", "0002_alter_media_uuid"), - ] - - operations = [ - migrations.AlterField( - model_name="event", - name="source", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - to="events.source", - ), - ), - migrations.AlterField( - model_name="media", - name="uuid", - field=models.UUIDField( - db_default=django.contrib.postgres.functions.RandomUUID(), - default=uuid.uuid4, - editable=False, - unique=True, - ), - ), - ] diff --git a/backend/project/events/migrations/__init__.py b/backend/project/observations/__init__.py similarity index 100% rename from backend/project/events/migrations/__init__.py rename to backend/project/observations/__init__.py diff --git a/backend/project/events/admin.py b/backend/project/observations/admin.py similarity index 77% rename from backend/project/events/admin.py rename to backend/project/observations/admin.py index 50938f7..7def0ec 100644 --- a/backend/project/events/admin.py +++ b/backend/project/observations/admin.py @@ -1,13 +1,18 @@ from django.contrib import admin from django.contrib.gis.admin import GISModelAdmin from django.utils.safestring import mark_safe +from project.observations.models import ( + Observation, + ObservationSubType, + ObservationType, + Media, + Source, +) from sorl.thumbnail import get_thumbnail -from project.events.models import Event, EventSubType, EventType, Media, Source - class SubTypeInline(admin.TabularInline): - model = EventSubType + model = ObservationSubType extra = 0 fields = ("label", "description", "pictogram", "picto_preview") readonly_fields = ("picto_preview",) @@ -52,8 +57,8 @@ def media_preview(self, obj): return "-" -@admin.register(EventType) -class EventTypeAdmin(admin.ModelAdmin): +@admin.register(ObservationType) +class ObservationTypeAdmin(admin.ModelAdmin): list_display = ("label", "description", "picto_preview") search_fields = ("label", "description") ordering = ("label",) @@ -68,11 +73,11 @@ def picto_preview(self, obj): ) -@admin.register(EventSubType) -class EventSubTypeAdmin(admin.ModelAdmin): - list_display = ("label", "event_type", "description", "picto_preview") +@admin.register(ObservationSubType) +class ObservationSubTypeAdmin(admin.ModelAdmin): + list_display = ("label", "observation_type", "description", "picto_preview") search_fields = ("label", "description") - list_filter = ("event_type",) + list_filter = ("observation_type",) ordering = ("label",) def picto_preview(self, obj): @@ -84,13 +89,13 @@ def picto_preview(self, obj): ) def get_queryset(self, request): - return super().get_queryset(request).select_related("event_type") + return super().get_queryset(request).select_related("observation_type") -@admin.register(Event) -class EventAdmin(GISModelAdmin): - list_display = ("event_subtype", "event_date", "observer") - list_filter = ("event_subtype", "event_date") +@admin.register(Observation) +class ObservationAdmin(GISModelAdmin): + list_display = ("observation_subtype", "event_date", "observer") + list_filter = ("observation_subtype", "event_date") ordering = ("-event_date",) date_hierarchy = "event_date" readonly_fields = ("uuid",) @@ -100,7 +105,9 @@ def get_queryset(self, request): return ( super() .get_queryset(request) - .select_related("event_subtype__event_type", "source", "observer") + .select_related( + "observation_subtype__observation_type", "source", "observer" + ) ) diff --git a/backend/project/events/apps.py b/backend/project/observations/apps.py similarity index 56% rename from backend/project/events/apps.py rename to backend/project/observations/apps.py index a23f80b..184bbe3 100644 --- a/backend/project/events/apps.py +++ b/backend/project/observations/apps.py @@ -1,6 +1,6 @@ from django.apps import AppConfig -class CoreConfig(AppConfig): +class ObservationsConfig(AppConfig): default_auto_field = "django.db.models.BigAutoField" - name = "project.events" + name = "project.observations" diff --git a/backend/project/events/locale/en/LC_MESSAGES/django.po b/backend/project/observations/locale/en/LC_MESSAGES/django.po similarity index 89% rename from backend/project/events/locale/en/LC_MESSAGES/django.po rename to backend/project/observations/locale/en/LC_MESSAGES/django.po index 33f35d6..ab03f4c 100644 --- a/backend/project/events/locale/en/LC_MESSAGES/django.po +++ b/backend/project/observations/locale/en/LC_MESSAGES/django.po @@ -26,19 +26,19 @@ msgid "Sources" msgstr "" #: project/events/models.py:30 -msgid "Event type" +msgid "Observation type" msgstr "" #: project/events/models.py:31 -msgid "Event types" +msgid "Observation types" msgstr "" #: project/events/models.py:47 -msgid "Event sub-type" +msgid "Observation sub-type" msgstr "" #: project/events/models.py:48 -msgid "Event sub-types" +msgid "Observation sub-types" msgstr "" #: project/events/models.py:60 @@ -46,11 +46,11 @@ msgid "Location" msgstr "" #: project/events/models.py:66 project/events/models.py:83 -msgid "Event" +msgid "Observation" msgstr "" #: project/events/models.py:67 -msgid "Events" +msgid "Observations" msgstr "" #: project/events/models.py:73 diff --git a/backend/project/events/locale/fr/LC_MESSAGES/django.po b/backend/project/observations/locale/fr/LC_MESSAGES/django.po similarity index 73% rename from backend/project/events/locale/fr/LC_MESSAGES/django.po rename to backend/project/observations/locale/fr/LC_MESSAGES/django.po index 8920bcf..a0cc7ea 100644 --- a/backend/project/events/locale/fr/LC_MESSAGES/django.po +++ b/backend/project/observations/locale/fr/LC_MESSAGES/django.po @@ -24,26 +24,26 @@ msgstr "Source" msgid "Sources" msgstr "Sources" -msgid "Event type" -msgstr "Type d'évènement" +msgid "Observation type" +msgstr "Type d'observation" -msgid "Event types" -msgstr "Types d'évènement" +msgid "Observation types" +msgstr "Types d'observation" -msgid "Event sub-type" -msgstr "Sous-type d'évènement" +msgid "Observation sub-type" +msgstr "Sous-type d'observation" -msgid "Event sub-types" -msgstr "Sous-types d'évènement" +msgid "Observation sub-types" +msgstr "Sous-types d'observation" msgid "Location" msgstr "Emplacement" -msgid "Event" -msgstr "Évènement" +msgid "Observation" +msgstr "Observation" -msgid "Events" -msgstr "Évènements" +msgid "Observations" +msgstr "Observations" msgid "Image" msgstr "Image" diff --git a/backend/project/events/migrations/0001_initial.py b/backend/project/observations/migrations/0001_initial.py similarity index 85% rename from backend/project/events/migrations/0001_initial.py rename to backend/project/observations/migrations/0001_initial.py index a9247b3..12e48d2 100644 --- a/backend/project/events/migrations/0001_initial.py +++ b/backend/project/observations/migrations/0001_initial.py @@ -1,11 +1,10 @@ -# Generated by Django 5.0.6 on 2024-06-12 20:15 - -import uuid +# Generated by Django 5.0.6 on 2024-06-28 13:23 import django.contrib.gis.db.models.fields import django.db.models.deletion import django.db.models.functions.datetime import django.views.generic.dates +import uuid from django.conf import settings from django.db import migrations, models @@ -20,7 +19,7 @@ class Migration(migrations.Migration): operations = [ migrations.CreateModel( - name="EventSubType", + name="ObservationSubType", fields=[ ( "id", @@ -52,18 +51,18 @@ class Migration(migrations.Migration): ( "pictogram", models.ImageField( - upload_to="event_types", verbose_name="Pictogram" + upload_to="observation_types", verbose_name="Pictogram" ), ), ], options={ - "verbose_name": "Event sub-type", - "verbose_name_plural": "Event sub-types", - "ordering": ["event_type__label", "label"], + "verbose_name": "Observation sub-type", + "verbose_name_plural": "Observation sub-types", + "ordering": ["observation_type__label", "label"], }, ), migrations.CreateModel( - name="EventType", + name="ObservationType", fields=[ ( "id", @@ -95,13 +94,13 @@ class Migration(migrations.Migration): ( "pictogram", models.ImageField( - upload_to="event_types", verbose_name="Pictogram" + upload_to="observation_types", verbose_name="Pictogram" ), ), ], options={ - "verbose_name": "Event type", - "verbose_name_plural": "Event types", + "verbose_name": "Observation type", + "verbose_name_plural": "Observation types", "ordering": ["label"], }, ), @@ -143,7 +142,7 @@ class Migration(migrations.Migration): }, ), migrations.CreateModel( - name="Event", + name="Observation", fields=[ ( "id", @@ -197,34 +196,28 @@ class Migration(migrations.Migration): ), ), ( - "event_subtype", + "observation_subtype", models.ForeignKey( on_delete=django.db.models.deletion.PROTECT, - to="events.eventsubtype", + to="observations.observationsubtype", ), ), ( "source", models.ForeignKey( - on_delete=django.db.models.deletion.PROTECT, to="events.source" + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to="observations.source", ), ), ], options={ - "verbose_name": "Event", - "verbose_name_plural": "Events", + "verbose_name": "Observation", + "verbose_name_plural": "Observations", "ordering": ["-created_at"], }, ), - migrations.AddField( - model_name="eventsubtype", - name="event_type", - field=models.ForeignKey( - on_delete=django.db.models.deletion.PROTECT, - related_name="sub_types", - to="events.eventtype", - ), - ), migrations.CreateModel( name="Media", fields=[ @@ -254,7 +247,10 @@ class Migration(migrations.Migration): ), ), ("media_file", models.FileField(upload_to="media")), - ("uuid", models.UUIDField(unique=True)), + ( + "uuid", + models.UUIDField(default=uuid.uuid4, editable=False, unique=True), + ), ( "media_type", models.CharField( @@ -266,12 +262,12 @@ class Migration(migrations.Migration): ), ("legend", models.CharField(blank=True, default="", max_length=100)), ( - "event", + "observation", models.ForeignKey( on_delete=django.db.models.deletion.CASCADE, related_name="medias", - to="events.event", - verbose_name="Event", + to="observations.observation", + verbose_name="Observation", ), ), ], @@ -281,4 +277,13 @@ class Migration(migrations.Migration): "ordering": ["created_at"], }, ), + migrations.AddField( + model_name="observationsubtype", + name="observation_type", + field=models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + related_name="sub_types", + to="observations.observationtype", + ), + ), ] diff --git a/backend/project/events/tests.py b/backend/project/observations/migrations/__init__.py similarity index 100% rename from backend/project/events/tests.py rename to backend/project/observations/migrations/__init__.py diff --git a/backend/project/events/models.py b/backend/project/observations/models.py similarity index 64% rename from backend/project/events/models.py rename to backend/project/observations/models.py index a720427..6f2ed7d 100644 --- a/backend/project/events/models.py +++ b/backend/project/observations/models.py @@ -20,38 +20,42 @@ class Meta: ordering = ["label"] -class EventType(TimeStampMixin): +class ObservationType(TimeStampMixin): label = models.CharField(max_length=100, unique=True) description = models.TextField(blank=True) - pictogram = models.ImageField(upload_to="event_types", verbose_name=_("Pictogram")) + pictogram = models.ImageField( + upload_to="observation_types", verbose_name=_("Pictogram") + ) def __str__(self): return self.label class Meta: - verbose_name = _("Event type") - verbose_name_plural = _("Event types") + verbose_name = _("Observation type") + verbose_name_plural = _("Observation types") ordering = ["label"] -class EventSubType(TimeStampMixin): +class ObservationSubType(TimeStampMixin): label = models.CharField(max_length=100, unique=True) description = models.TextField(blank=True) - event_type = models.ForeignKey( - EventType, on_delete=models.PROTECT, related_name="sub_types" + observation_type = models.ForeignKey( + ObservationType, on_delete=models.PROTECT, related_name="sub_types" + ) + pictogram = models.ImageField( + upload_to="observation_types", verbose_name=_("Pictogram") ) - pictogram = models.ImageField(upload_to="event_types", verbose_name=_("Pictogram")) def __str__(self): - return f"{self.label} ({self.event_type})" + return f"{self.label} ({self.observation_type})" class Meta: - verbose_name = _("Event sub-type") - verbose_name_plural = _("Event sub-types") - ordering = ["event_type__label", "label"] + verbose_name = _("Observation sub-type") + verbose_name_plural = _("Observation sub-types") + ordering = ["observation_type__label", "label"] -class Event(TimeStampMixin): +class Observation(TimeStampMixin): uuid = models.UUIDField(unique=True, default=uuid4, editable=False) observer = models.ForeignKey( "accounts.User", on_delete=models.SET_NULL, null=True, blank=True @@ -59,15 +63,17 @@ class Event(TimeStampMixin): comments = models.TextField() event_date = models.DateField(default=timezone_today, db_index=True) source = models.ForeignKey(Source, on_delete=models.SET_NULL, blank=True, null=True) - event_subtype = models.ForeignKey(EventSubType, on_delete=models.PROTECT) + observation_subtype = models.ForeignKey( + ObservationSubType, on_delete=models.PROTECT + ) location = models.PointField(srid=4326, verbose_name=_("Location")) def __str__(self): return str(self.uuid) class Meta: - verbose_name = _("Event") - verbose_name_plural = _("Events") + verbose_name = _("Observation") + verbose_name_plural = _("Observations") ordering = ["-created_at"] @@ -82,8 +88,11 @@ class MediaType(models.TextChoices): max_length=10, choices=MediaType.choices, default=MediaType.IMAGE, db_index=True ) legend = models.CharField(max_length=100, blank=True, default="") - event = models.ForeignKey( - Event, on_delete=models.CASCADE, related_name="medias", verbose_name=_("Event") + observation = models.ForeignKey( + Observation, + on_delete=models.CASCADE, + related_name="medias", + verbose_name=_("Observation"), ) class Meta: diff --git a/backend/project/observations/tests.py b/backend/project/observations/tests.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/project/settings/__init__.py b/backend/project/settings/__init__.py index 609e786..3f28936 100644 --- a/backend/project/settings/__init__.py +++ b/backend/project/settings/__init__.py @@ -61,7 +61,7 @@ "sorl.thumbnail", "project.accounts", "project.api", - "project.events", + "project.observations", ] MIDDLEWARE = [ From 38e58a9ad8bea66edfa5343bd92abc676ea081e2 Mon Sep 17 00:00:00 2001 From: J-E Castagnede Date: Mon, 1 Jul 2024 16:23:52 +0200 Subject: [PATCH 5/5] lint and set tests --- backend/project/accounts/tests.py | 0 backend/project/{api => accounts}/tests/factories.py | 2 +- backend/project/accounts/tests/test_admin.py | 12 +++++++++++- backend/project/accounts/tests/test_models.py | 8 ++++++++ backend/project/api/serializers/common.py | 3 ++- backend/project/api/serializers/observations.py | 4 ++-- backend/project/api/tests/test_serializers.py | 2 +- backend/project/api/views.py | 12 ++++++------ backend/project/observations/admin.py | 5 +++-- .../project/observations/migrations/0001_initial.py | 3 ++- backend/project/settings/__init__.py | 4 +++- backend/project/settings/dev.py | 1 + 12 files changed, 40 insertions(+), 16 deletions(-) delete mode 100644 backend/project/accounts/tests.py rename backend/project/{api => accounts}/tests/factories.py (88%) diff --git a/backend/project/accounts/tests.py b/backend/project/accounts/tests.py deleted file mode 100644 index e69de29..0000000 diff --git a/backend/project/api/tests/factories.py b/backend/project/accounts/tests/factories.py similarity index 88% rename from backend/project/api/tests/factories.py rename to backend/project/accounts/tests/factories.py index 6818cf6..df26eeb 100644 --- a/backend/project/api/tests/factories.py +++ b/backend/project/accounts/tests/factories.py @@ -1,7 +1,7 @@ import factory from factory import faker -from project.accounts.models import User +from ..models import User class UserFactory(factory.django.DjangoModelFactory): diff --git a/backend/project/accounts/tests/test_admin.py b/backend/project/accounts/tests/test_admin.py index ca59b8a..5807716 100644 --- a/backend/project/accounts/tests/test_admin.py +++ b/backend/project/accounts/tests/test_admin.py @@ -1,6 +1,16 @@ from django.test import TestCase +from .factories import SuperUserFactory, UserFactory + class AccountAdminTestCase(TestCase): def test_superuser_ca(self): - """Test that only superuser can login to the admin site.""" + """Test that only superuser can log in to the admin site.""" + simple_user = UserFactory() + self.client.force_login(simple_user) + response = self.client.get("/admin/") + self.assertEqual(response.status_code, 302) + super_user = SuperUserFactory() + self.client.force_login(super_user) + response = self.client.get("/admin/") + self.assertEqual(response.status_code, 200) diff --git a/backend/project/accounts/tests/test_models.py b/backend/project/accounts/tests/test_models.py index f6349a4..96a0ffe 100644 --- a/backend/project/accounts/tests/test_models.py +++ b/backend/project/accounts/tests/test_models.py @@ -1,6 +1,14 @@ from django.test import TestCase +from .factories import SuperUserFactory, UserFactory + class AccountTestCase(TestCase): def test_is_staff(self): """Test that is_staff is same value as is_superuser""" + user = UserFactory() + self.assertFalse(user.is_staff) + self.assertFalse(user.is_superuser) + super_user = SuperUserFactory() + self.assertTrue(super_user.is_staff) + self.assertTrue(super_user.is_superuser) diff --git a/backend/project/api/serializers/common.py b/backend/project/api/serializers/common.py index 78e0ef5..58179a0 100644 --- a/backend/project/api/serializers/common.py +++ b/backend/project/api/serializers/common.py @@ -1,6 +1,7 @@ -from project.api.serializers.observations import ObservationTypeSerializer from rest_framework import serializers +from project.api.serializers.observations import ObservationTypeSerializer + class SettingsSerializer(serializers.Serializer): observation_types = ObservationTypeSerializer(many=True, read_only=True) diff --git a/backend/project/api/serializers/observations.py b/backend/project/api/serializers/observations.py index 4ac5446..38f0925 100644 --- a/backend/project/api/serializers/observations.py +++ b/backend/project/api/serializers/observations.py @@ -6,10 +6,10 @@ from sorl.thumbnail import get_thumbnail from project.observations.models import ( - ObservationSubType, - ObservationType, Media, Observation, + ObservationSubType, + ObservationType, ) diff --git a/backend/project/api/tests/test_serializers.py b/backend/project/api/tests/test_serializers.py index bac6647..883c348 100644 --- a/backend/project/api/tests/test_serializers.py +++ b/backend/project/api/tests/test_serializers.py @@ -1,7 +1,7 @@ from django.test import TestCase +from project.accounts.tests.factories import UserFactory from project.api.serializers.accounts import AccountSerializer -from project.api.tests.factories import UserFactory class AccountSerializerTestCase(TestCase): diff --git a/backend/project/api/views.py b/backend/project/api/views.py index 2086282..a15e6de 100644 --- a/backend/project/api/views.py +++ b/backend/project/api/views.py @@ -1,10 +1,4 @@ from django_filters.rest_framework import DjangoFilterBackend -from project.api.serializers.observations import ( - ObservationDetailSerializer, - ObservationListSerializer, - ObservationTypeSerializer, -) -from project.observations.models import Observation, ObservationType from rest_framework import permissions, status, viewsets from rest_framework.decorators import action from rest_framework.generics import GenericAPIView @@ -21,6 +15,12 @@ from project.api.filters import ObservationFilterSet from project.api.serializers.accounts import AccountSerializer from project.api.serializers.common import SettingsSerializer +from project.api.serializers.observations import ( + ObservationDetailSerializer, + ObservationListSerializer, + ObservationTypeSerializer, +) +from project.observations.models import Observation, ObservationType class SettingsApiView(GenericAPIView): diff --git a/backend/project/observations/admin.py b/backend/project/observations/admin.py index 7def0ec..3014701 100644 --- a/backend/project/observations/admin.py +++ b/backend/project/observations/admin.py @@ -1,14 +1,15 @@ from django.contrib import admin from django.contrib.gis.admin import GISModelAdmin from django.utils.safestring import mark_safe +from sorl.thumbnail import get_thumbnail + from project.observations.models import ( + Media, Observation, ObservationSubType, ObservationType, - Media, Source, ) -from sorl.thumbnail import get_thumbnail class SubTypeInline(admin.TabularInline): diff --git a/backend/project/observations/migrations/0001_initial.py b/backend/project/observations/migrations/0001_initial.py index 12e48d2..53445e5 100644 --- a/backend/project/observations/migrations/0001_initial.py +++ b/backend/project/observations/migrations/0001_initial.py @@ -1,10 +1,11 @@ # Generated by Django 5.0.6 on 2024-06-28 13:23 +import uuid + import django.contrib.gis.db.models.fields import django.db.models.deletion import django.db.models.functions.datetime import django.views.generic.dates -import uuid from django.conf import settings from django.db import migrations, models diff --git a/backend/project/settings/__init__.py b/backend/project/settings/__init__.py index 3f28936..4df8b82 100644 --- a/backend/project/settings/__init__.py +++ b/backend/project/settings/__init__.py @@ -59,9 +59,9 @@ "drf_spectacular", "django_filters", "sorl.thumbnail", - "project.accounts", "project.api", "project.observations", + "project.accounts", ] MIDDLEWARE = [ @@ -205,3 +205,5 @@ SIMPLE_JWT = { "UPDATE_LAST_LOGIN": True, } + +DEBUG_TOOLBAR_CONFIG = {"IS_RUNNING_TESTS": False} diff --git a/backend/project/settings/dev.py b/backend/project/settings/dev.py index b37130b..c474359 100644 --- a/backend/project/settings/dev.py +++ b/backend/project/settings/dev.py @@ -13,4 +13,5 @@ DEBUG_TOOLBAR_CONFIG = { "SHOW_TOOLBAR_CALLBACK": lambda request: True, + "IS_RUNNING_TESTS": False, }