diff --git a/.gitignore b/.gitignore index 3d4c1480..a53dd6e9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,7 @@ +# Data files +backend/files +backend/uploads + # IDE things .idea .vscode diff --git a/backend/backend/settings.py b/backend/backend/settings.py index 52b1d12f..3079af0b 100644 --- a/backend/backend/settings.py +++ b/backend/backend/settings.py @@ -148,7 +148,7 @@ LANGUAGE_CODE = 'en-us' -TIME_ZONE = 'UTC' +TIME_ZONE = 'CET' USE_I18N = True @@ -158,9 +158,12 @@ # https://docs.djangoproject.com/en/4.1/howto/static-files/ STATIC_URL = 'static/' - STATIC_ROOT = BASE_DIR / "staticfiles" +MEDIA_URL = '/media/' +MEDIA_ROOT = BASE_DIR / 'uploads' + + # Default primary key field type # https://docs.djangoproject.com/en/4.1/ref/settings/#default-auto-field diff --git a/backend/backend/urls.py b/backend/backend/urls.py index 5bb45e9c..ed300b1d 100644 --- a/backend/backend/urls.py +++ b/backend/backend/urls.py @@ -15,8 +15,10 @@ """ from django.contrib import admin from django.urls import include, path +from django.conf import settings +from django.conf.urls.static import static urlpatterns = [ path('api/', include('drtrottoir.urls')), path('admin/', admin.site.urls), -] +] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) diff --git a/backend/drtrottoir/migrations/0014_photo_image_alter_photo_visit.py b/backend/drtrottoir/migrations/0014_photo_image_alter_photo_visit.py new file mode 100644 index 00000000..76f60284 --- /dev/null +++ b/backend/drtrottoir/migrations/0014_photo_image_alter_photo_visit.py @@ -0,0 +1,29 @@ +# Generated by Django 4.1.7 on 2023-03-16 19:31 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + dependencies = [ + ("drtrottoir", "0013_remove_visit_building_visit_building_in_tour"), + ] + + operations = [ + migrations.AddField( + model_name="photo", + name="image", + field=models.ImageField( + null=True, upload_to="images/", verbose_name="image" + ), + ), + migrations.AlterField( + model_name="photo", + name="visit", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="drtrottoir.visit", + verbose_name="id of visit", + ), + ), + ] diff --git a/backend/drtrottoir/models/photo.py b/backend/drtrottoir/models/photo.py index 855901e2..2c429974 100644 --- a/backend/drtrottoir/models/photo.py +++ b/backend/drtrottoir/models/photo.py @@ -1,16 +1,22 @@ from django.db import models - -from .building import Building +from .visit import Visit +from django.dispatch import receiver +from django.db.models.signals import pre_delete IMAGE_STATES = ((1, 'Arrival'), (2, 'Departure'), (3, 'Extra')) class Photo(models.Model): - # image = models.ImageField(verbose_name="image", upload_to="images/") Pillow needs to be set up - visit = models.ForeignKey(Building, verbose_name="id of visit", on_delete=models.CASCADE) + image = models.ImageField(verbose_name="image", upload_to="images/", null=True) + visit = models.ForeignKey(Visit, verbose_name="id of visit", on_delete=models.CASCADE) state = models.IntegerField(verbose_name="type of photo", choices=IMAGE_STATES) comment = models.TextField(verbose_name="comment on the photo") created_at = models.DateTimeField(verbose_name="time of creation") def __str__(self): - return f"{self.visit.name}, {self.created_at}" + return f"{self.visit}, {self.created_at}" + + +@receiver(pre_delete, sender=Photo) +def pre_delete(sender, instance: Photo, **kwargs): + instance.image.delete() diff --git a/backend/drtrottoir/serializers/__init__.py b/backend/drtrottoir/serializers/__init__.py index 5400938d..3414d858 100644 --- a/backend/drtrottoir/serializers/__init__.py +++ b/backend/drtrottoir/serializers/__init__.py @@ -6,6 +6,7 @@ from .building_in_tour_serializer import BuildingInTourSerializer from .user_partial import UserPartialSerializer from .visit_serializer import VisitSerializer +from .photo_serializer import PhotoSerializer from .waste_serializer import WasteSerializer from .waste_partial import WastePartialSerializer from .register_serializer import RegisterSerializer @@ -20,6 +21,7 @@ UserSerializer, UserPartialSerializer, VisitSerializer, + PhotoSerializer, WasteSerializer, WastePartialSerializer, RegisterSerializer, diff --git a/backend/drtrottoir/serializers/photo_serializer.py b/backend/drtrottoir/serializers/photo_serializer.py new file mode 100644 index 00000000..45fdf7fd --- /dev/null +++ b/backend/drtrottoir/serializers/photo_serializer.py @@ -0,0 +1,12 @@ +from rest_framework import serializers +from drtrottoir.models import Photo + + +class PhotoSerializer(serializers.HyperlinkedModelSerializer): + """ + A serializer for photos + """ + + class Meta: + model = Photo + fields = '__all__' diff --git a/backend/drtrottoir/tests/factories/__init__.py b/backend/drtrottoir/tests/factories/__init__.py index 87403cf7..320939e6 100644 --- a/backend/drtrottoir/tests/factories/__init__.py +++ b/backend/drtrottoir/tests/factories/__init__.py @@ -1,6 +1,7 @@ from .region_factory import RegionFactory from .user_factory import DeveloperUserFactory from .visit_factory import VisitFactory +from .photo_factory import PhotoFactory from .waste_factory import WasteFactory from .tour_factory import TourFactory from .building_factory import BuildingFactory @@ -11,6 +12,8 @@ DeveloperUserFactory, BuildingFactory, VisitFactory, + PhotoFactory, + TourFactory, WasteFactory, TourFactory, BuildingInTourFactory diff --git a/backend/drtrottoir/tests/factories/photo_factory.py b/backend/drtrottoir/tests/factories/photo_factory.py new file mode 100644 index 00000000..01bc1de8 --- /dev/null +++ b/backend/drtrottoir/tests/factories/photo_factory.py @@ -0,0 +1,17 @@ +import random +from factory.django import DjangoModelFactory +import factory +from django.utils import timezone +from drtrottoir.models import Photo +from .visit_factory import VisitFactory + + +class PhotoFactory(DjangoModelFactory): + image = factory.Faker("image_url") + visit = factory.SubFactory(VisitFactory) + state = random.choice([0, 1, 2]) + comment = factory.Faker("sentence") + created_at = factory.Faker("date_time", tzinfo=timezone.utc) + + class Meta: + model = Photo diff --git a/backend/drtrottoir/tests/views/test_photo.py b/backend/drtrottoir/tests/views/test_photo.py new file mode 100644 index 00000000..775c2642 --- /dev/null +++ b/backend/drtrottoir/tests/views/test_photo.py @@ -0,0 +1,48 @@ +from rest_framework import status +from rest_framework.test import APITestCase +from django.urls import reverse + +from drtrottoir.serializers import PhotoSerializer +from drtrottoir.tests.factories import PhotoFactory, DeveloperUserFactory + + +class TestPhotoView(APITestCase): + """ Test module for GET single Photo API """ + + def setUp(self): + self.photo = PhotoFactory() + user = DeveloperUserFactory() + self.client.force_authenticate(user=user) + + def test_get(self): + response = self.client.get(reverse("photo-detail", kwargs={'pk': self.photo.pk})) + serializer = PhotoSerializer(self.photo, context={'request': response.wsgi_request}) + self.assertEqual(response.data, serializer.data) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + def test_get_fault(self): + response = self.client.get(reverse("photo-detail", kwargs={'pk': self.photo.pk+1})) + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + + def test_delete(self): + response1 = self.client.delete('/api/photo/' + str(self.photo.pk) + "/", follow=True) + response2 = self.client.get(reverse("photo-detail", kwargs={'pk': self.photo.pk})) + self.assertEqual(response1.status_code, status.HTTP_204_NO_CONTENT) + self.assertEqual(response2.status_code, status.HTTP_404_NOT_FOUND) + + def test_delete_fault(self): + response = self.client.delete('/api/photo/', follow=True) + self.assertEqual(response.status_code, status.HTTP_405_METHOD_NOT_ALLOWED) + + def test_patch(self): + response1 = self.client.patch( + '/api/photo/' + str(self.photo.pk) + "/", + content_type="application/x-www-form-urlencoded", + data={"comment": "nieuwe zin"}, + follow=True + ) + + response2 = self.client.get(reverse("photo-detail", kwargs={'pk': self.photo.pk})) + + self.assertNotEqual(response1.data, self.photo) + self.assertEqual(response1.data["comment"], response2.data["comment"]) diff --git a/backend/drtrottoir/urls.py b/backend/drtrottoir/urls.py index 44d6c81f..64802755 100644 --- a/backend/drtrottoir/urls.py +++ b/backend/drtrottoir/urls.py @@ -17,7 +17,8 @@ UserViewSet, WasteViewSet, RegisterView, - MeView, + PhotoViewSet, + MeView ) router = routers.DefaultRouter() @@ -27,6 +28,7 @@ router.register(r'building_in_tour', BuildingInTourViewSet) router.register(r'visit', VisitViewSet) router.register(r'user', UserViewSet) +router.register(r'photo', PhotoViewSet) router.register(r'waste', WasteViewSet) urlpatterns = [ diff --git a/backend/drtrottoir/views/__init__.py b/backend/drtrottoir/views/__init__.py index 9ce9b50f..b462e520 100644 --- a/backend/drtrottoir/views/__init__.py +++ b/backend/drtrottoir/views/__init__.py @@ -5,6 +5,7 @@ from .building_in_tour_viewset import BuildingInTourViewSet from .visit_viewset import VisitViewSet from .user_viewset import UserViewSet +from .photo_viewset import PhotoViewSet from .register_view import RegisterView from .me_view import MeView @@ -13,6 +14,8 @@ RegionViewSet, VisitViewSet, UserViewSet, + PhotoViewSet, + RegisterView, WasteViewSet, TourViewSet, BuildingInTourViewSet, diff --git a/backend/drtrottoir/views/photo_viewset.py b/backend/drtrottoir/views/photo_viewset.py new file mode 100644 index 00000000..cd30e531 --- /dev/null +++ b/backend/drtrottoir/views/photo_viewset.py @@ -0,0 +1,17 @@ +from rest_framework import viewsets +from rest_framework.permissions import IsAuthenticated +from drtrottoir.models import Photo +from drtrottoir.permissions.user_permissions import SuperPermissionOrReadOnly +from drtrottoir.serializers import PhotoSerializer +from rest_framework.parsers import MultiPartParser, FormParser + + +class PhotoViewSet(viewsets.ModelViewSet): + """ + API endpoint that allows photos to be viewed or edited. + """ + + queryset = Photo.objects.all() + serializer_class = PhotoSerializer + parser_classes = (MultiPartParser, FormParser) + permission_classes = [IsAuthenticated & SuperPermissionOrReadOnly] diff --git a/backend/drtrottoir/views/visit_viewset.py b/backend/drtrottoir/views/visit_viewset.py index 3756b130..e67b062b 100644 --- a/backend/drtrottoir/views/visit_viewset.py +++ b/backend/drtrottoir/views/visit_viewset.py @@ -1,5 +1,10 @@ -from rest_framework import viewsets +from django.urls import reverse +from rest_framework import viewsets, status +from rest_framework.decorators import action from rest_framework.permissions import IsAuthenticated +from rest_framework.response import Response + +from drtrottoir.models import Photo from drtrottoir.models import Visit from drtrottoir.permissions.user_permissions import SuperPermissionOrReadOnly from drtrottoir.serializers import VisitSerializer @@ -29,3 +34,19 @@ class VisitViewSet(viewsets.ModelViewSet): queryset = Visit.objects.all() serializer_class = VisitSerializer permission_classes = [IsAuthenticated & SuperPermissionOrReadOnly] + + @action(detail=True, methods=['get']) + def photos(self, request, pk=None): + """ + Get all photos inside a visit. Authentication required. + """ + if pk is not None and Visit.objects.filter(pk=pk).exists(): + + urls = [] + for photo in Photo.objects.filter(visit=pk): + url = reverse('photo-detail', args=[photo.id]) + urls.append(request.build_absolute_uri(url)) + + return Response({"photos": urls}) + else: + return Response("Given visit doesn't exist.", status=status.HTTP_400_BAD_REQUEST) diff --git a/backend/fixtures/init_data.json b/backend/fixtures/init_data.json index 91380902..43da225d 100644 --- a/backend/fixtures/init_data.json +++ b/backend/fixtures/init_data.json @@ -135,4 +135,5 @@ "building": 1 } } + ] diff --git a/backend/requirements.txt b/backend/requirements.txt index 5bb710b7..f1bebd09 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -12,5 +12,6 @@ factory-boy==3.2.1 flake8==6.0.0 coverage==7.2.1 django-nose==1.4.7 +pillow==9.4.0 pyyaml==6.0 -uritemplate==4.1.1 \ No newline at end of file +uritemplate==4.1.1 diff --git a/backend/test_file.py b/backend/test_file.py deleted file mode 100644 index e69de29b..00000000