diff --git a/api/apis/urls.py b/api/apis/urls.py index 1e044a7..bfc54f8 100644 --- a/api/apis/urls.py +++ b/api/apis/urls.py @@ -7,4 +7,5 @@ path("cases/", include("api.cases.urls", "cases")), path("files/", include("api.files.urls", "files")), path("notifications/", include("api.notifications.urls", "notifications")), + path("locations/", include("api.locations.urls", "locations")), ] diff --git a/api/cases/apis.py b/api/cases/apis.py index a7dfaf3..154b0ec 100644 --- a/api/cases/apis.py +++ b/api/cases/apis.py @@ -5,14 +5,14 @@ from api.apis.pagination import LimitOffsetPagination, get_paginated_response from api.cases.models import Case from api.cases.selectors import get_case, list_case, list_case_match -from api.cases.services import create_case +from api.cases.services import create_case, publish_case from api.common.utils import inline_serializer -from api.users.models import User class CreateCaseApi(APIView): class InputSerializer(serializers.Serializer): type = serializers.CharField() + thumbnail = serializers.URLField() photos_urls = serializers.ListField(child=serializers.URLField()) location = inline_serializer( fields={ @@ -44,7 +44,7 @@ class InputSerializer(serializers.Serializer): def post(self, request): serializer = self.InputSerializer(data=request.data) serializer.is_valid(raise_exception=True) - create_case(user=User.objects.all()[0], **serializer.validated_data) + create_case(user=request.user, **serializer.validated_data) return Response(status=status.HTTP_201_CREATED) @@ -66,15 +66,15 @@ class OutputSerializer(serializers.Serializer): id = serializers.IntegerField() type = serializers.CharField() name = serializers.CharField(source="details.name") + thumbnail = serializers.URLField() + last_seen = serializers.DateField(source="details.last_seen") + posted_at = serializers.DateTimeField() location = inline_serializer( fields={ "gov": serializers.CharField(source="gov.name_ar"), "city": serializers.CharField(source="city.name_ar"), } ) - photos = serializers.ListField(source="photo_urls") - last_seen = serializers.DateField(source="details.last_seen") - posted_at = serializers.DateTimeField() def get(self, request): # Make sure the filters are valid, if passed @@ -94,7 +94,7 @@ def get(self, request): class DetailsCaseApi(APIView): class OutputSerializer(serializers.Serializer): - user = serializers.IntegerField() + user = serializers.CharField(source="user.username") type = serializers.CharField() state = serializers.CharField(source="get_state_display") photos = serializers.ListField(source="photo_urls") @@ -138,7 +138,7 @@ def get(self, request, case_id): # Fetching our case case = get_case(pk=case_id, fetched_by=request.user) - # Selecting which cases to serialize depding on case type + # Selecting which cases to serialize depending on case type case_source = "missing" if case.type == Case.Types.FOUND else "found" # Writing our serializer here because of case source decision @@ -169,3 +169,10 @@ class OutputSerializer(serializers.Serializer): serializer = OutputSerializer(matches, many=True) return Response(serializer.data) + + +class CasePublishApi(APIView): + def get(self, request, case_id): + case = get_case(case_id) + publish_case(case=case, performed_by=request.user) + return Response(status=status.HTTP_200_OK) diff --git a/api/cases/migrations/0008_case_thumbnail.py b/api/cases/migrations/0008_case_thumbnail.py new file mode 100644 index 0000000..f2cbfdb --- /dev/null +++ b/api/cases/migrations/0008_case_thumbnail.py @@ -0,0 +1,19 @@ +# Generated by Django 3.2.13 on 2022-05-12 12:33 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('cases', '0007_auto_20220510_1459'), + ] + + operations = [ + migrations.AddField( + model_name='case', + name='thumbnail', + field=models.URLField(default='https://picsum.photos/200/300'), + preserve_default=False, + ), + ] diff --git a/api/cases/models.py b/api/cases/models.py index e149b33..64b2b8a 100644 --- a/api/cases/models.py +++ b/api/cases/models.py @@ -38,7 +38,7 @@ class Types(models.TextChoices): updated_at = models.DateTimeField(auto_now=True) posted_at = models.DateTimeField(null=True, default=None, blank=True) is_active = models.BooleanField(default=False, editable=False) - # TODO thumbnail + thumbnail = models.URLField() @property def photo_urls(self): @@ -50,10 +50,6 @@ def __str__(self): # Pass the case to the model then add matched cases if any. @transition(field=state, source=States.PENDING, target=States.ACTIVE) def activate(self): - from .services import case_matching_binding, process_case - - matches = process_case(self) - case_matching_binding(matches) self.is_active = True # If User selected one of the matches to be the correct one @@ -73,7 +69,6 @@ def archive(self): def activate_again(self): self.is_active = True - # FIXME def publish(self): self.posted_at = timezone.now() diff --git a/api/cases/services.py b/api/cases/services.py index 2b9b719..93c4272 100644 --- a/api/cases/services.py +++ b/api/cases/services.py @@ -1,10 +1,16 @@ from datetime import date from typing import Dict, List, Optional +from django.core.exceptions import PermissionDenied from django.db import transaction +from firebase_admin.messaging import Message +from firebase_admin.messaging import Notification as FirebaseNotification +from rest_framework.exceptions import ValidationError from api.locations.models import Location from api.locations.services import create_location +from api.notifications.models import Notification +from api.notifications.services import create_notification from api.users.models import User from .models import Case, CaseDetails, CaseMatch, CasePhoto @@ -28,10 +34,11 @@ def create_case( user: User, location: Dict, details: Dict, + thumbnail: str, photos_urls: List[str], ) -> Case: location: Location = create_location(**location) - case = Case(type=type.upper(), user=user, location=location) + case = Case(type=type.upper(), user=user, location=location, thumbnail=thumbnail) case.full_clean() case.save() @@ -41,6 +48,10 @@ def create_case( create_case_details(case=case, **details) + # TODO Factor out to an async function + activate_case(case) + case.save() + return case @@ -86,23 +97,24 @@ def create_case_match(*, missing: Case, found: Case, score: int) -> CaseMatch: return case_match -def process_case(*, case: Case) -> List[Dict[int, int]]: +def process_case(case: Case) -> List[Dict[int, int]]: """ Send case id and photos to the machine learing model then recives list of ids & scores that matched with the case """ - ... + return [] def case_matching_binding(*, case: Case, matches_list: List[Dict[int, int]]) -> None: """ Bind the processed case with it's matches by instaniating CaseMatch objects """ + if not matches_list: + return + cases_ids = [match["id"] for match in matches_list] cases_scores = [match["score"] for match in matches_list] matches: List[Case] = Case.objects.filter(id__in=cases_ids) - if not matches: - return missing = True if case.type == CaseType.MISSING else False @@ -111,3 +123,50 @@ def case_matching_binding(*, case: Case, matches_list: List[Dict[int, int]]) -> create_case_match(missing=case, found=match, score=score) else: create_case_match(missing=match, found=case, score=score) + + +def activate_case(case: Case): + matches = process_case(case) + case_matching_binding(case=case, matches_list=matches) + # TODO success or failure notification + create_notification( + title="تم رفع الحاله بنجاح", + body="جارى البحث عن المفقود وسنقوم بإشعارك فى حاله العثور لأى نتائج", + level=Notification.Level.INFO, + sent_to=case.user, + ) + + msg = Message( + notification=FirebaseNotification( + title="تم رفع الحاله بنجاح", + body="جارى البحث عن المفقود وسنقوم بإشعارك فى حاله العثور لأى نتائج", + ) + ) + + case.user.fcmdevice.send_message(msg) + + +def publish_case(*, case: Case, performed_by: User): + if case.user != performed_by: + raise PermissionDenied() + + if not case.is_active: + raise ValidationError("Cannot publish inactive case") + + case.publish() + case.save() + + create_notification( + title="تم نشر الحاله بنجاح", + body="تم نشر بيانات المعثور عليه بنجاح انتظر منا اشعار اخر فى حين الوصول لأى نتائج", + level=Notification.Level.SUCCESS, + sent_to=case.user, + ) + + msg = Message( + notification=FirebaseNotification( + title="تم نشر الحاله بنجاح", + body="تم نشر بيانات المعثور عليه بنجاح انتظر منا اشعار اخر فى حين الوصول لأى نتائج", + ) + ) + case.user.fcmdevice.send_message(msg) diff --git a/api/cases/urls.py b/api/cases/urls.py index b30dd5a..824d11f 100644 --- a/api/cases/urls.py +++ b/api/cases/urls.py @@ -1,6 +1,12 @@ from django.urls import path -from .apis import CaseListApi, CaseMatchListApi, CreateCaseApi, DetailsCaseApi +from .apis import ( + CaseListApi, + CaseMatchListApi, + CasePublishApi, + CreateCaseApi, + DetailsCaseApi, +) app_name = "cases" @@ -10,4 +16,5 @@ path("create/", CreateCaseApi.as_view(), name="create"), # path("/update/", UpdateCaseApi.as_view(), name="update"), path("/matches/", CaseMatchListApi.as_view(), name="matches"), + path("/publish/", CasePublishApi.as_view(), name="publish"), ] diff --git a/api/locations/apis.py b/api/locations/apis.py new file mode 100644 index 0000000..c04323f --- /dev/null +++ b/api/locations/apis.py @@ -0,0 +1,43 @@ +from rest_framework import permissions, serializers +from rest_framework.response import Response +from rest_framework.views import APIView + +from api.locations.selectors import list_governorate, list_governorate_cities + + +class GovernorateListApi(APIView): + permission_classes = [permissions.AllowAny] + + class OutputSerializer(serializers.Serializer): + id = serializers.IntegerField() + name_ar = serializers.CharField() + name_en = serializers.CharField() + + def get(self, request): + + # Listing all user cases + govs = list_governorate() + + # Serializing the results + serializer = self.OutputSerializer(govs, many=True) + + return Response(serializer.data) + + +class GovernorateCitiesListApi(APIView): + permission_classes = [permissions.AllowAny] + + class OutputSerializer(serializers.Serializer): + id = serializers.IntegerField() + name_ar = serializers.CharField() + name_en = serializers.CharField() + + def get(self, request, gov_id): + + # Listing all user cases + cities = list_governorate_cities(gov_id) + + # Serializing the results + serializer = self.OutputSerializer(cities, many=True) + + return Response(serializer.data) diff --git a/api/locations/selectors.py b/api/locations/selectors.py index d400ae0..4d7610e 100644 --- a/api/locations/selectors.py +++ b/api/locations/selectors.py @@ -1,4 +1,5 @@ from django.db.models.query import QuerySet +from django.shortcuts import get_object_or_404 from .models import City, Governorate @@ -7,5 +8,6 @@ def list_governorate() -> QuerySet[Governorate]: return Governorate.objects.all() -def list_cities() -> QuerySet[City]: - return City.objects.all() +def list_governorate_cities(gov_id) -> QuerySet[City]: + gov = get_object_or_404(Governorate, pk=gov_id) + return gov.cities.all() diff --git a/api/locations/urls.py b/api/locations/urls.py new file mode 100644 index 0000000..e99920d --- /dev/null +++ b/api/locations/urls.py @@ -0,0 +1,14 @@ +from django.urls import path + +from .apis import GovernorateCitiesListApi, GovernorateListApi + +app_name = "locations" + +urlpatterns = [ + path("governorates/", GovernorateListApi.as_view(), name="Governorates"), + path( + "governorates//cities", + GovernorateCitiesListApi.as_view(), + name="Governorate_cities", + ), +] diff --git a/api/locations/views.py b/api/locations/views.py deleted file mode 100644 index 5873694..0000000 --- a/api/locations/views.py +++ /dev/null @@ -1 +0,0 @@ -# Views diff --git a/api/notifications/models.py b/api/notifications/models.py index ca37412..8f0bd0c 100644 --- a/api/notifications/models.py +++ b/api/notifications/models.py @@ -27,6 +27,7 @@ class Meta: db_table = "notifications" verbose_name = "notification" verbose_name_plural = "notifications" + ordering = ["-created_at"] def __str__(self) -> str: return f"" diff --git a/api/notifications/services.py b/api/notifications/services.py index 52c15ac..d2ead96 100644 --- a/api/notifications/services.py +++ b/api/notifications/services.py @@ -11,7 +11,7 @@ def create_notification( title: str, body: str, level: str, - sent_to: FCMDevice, + sent_to: User, ) -> Notification: notification = Notification( diff --git a/api/users/apis.py b/api/users/apis.py index 6aaecf8..4223b75 100644 --- a/api/users/apis.py +++ b/api/users/apis.py @@ -3,9 +3,9 @@ from rest_framework.response import Response from rest_framework.views import APIView -from api.common.permissions import IsVerified from api.common.utils import inline_serializer from api.users.models import User +from api.users.selectors import get_user, get_user_cases from api.users.services import create_user, set_national_id, update_user @@ -16,14 +16,14 @@ class InputSerializer(serializers.Serializer): username = serializers.CharField() password = serializers.CharField() name = serializers.CharField() + fcm_token = serializers.CharField() + firebase_token = serializers.CharField() location = inline_serializer( fields={ "gov": serializers.IntegerField(), "city": serializers.IntegerField(), } ) - fcm_token = serializers.CharField() - firebase_token = serializers.CharField() def post(self, request): serializer = self.InputSerializer(data=request.data) @@ -34,8 +34,6 @@ def post(self, request): class DetailUserApi(APIView): - permission_classes = [IsVerified] - class OutputSerializer(serializers.Serializer): username = serializers.CharField() name = serializers.CharField() @@ -48,16 +46,16 @@ class OutputSerializer(serializers.Serializer): ) def get(self, request, user_id): - user = get_object_or_404(User, id=user_id) + user = get_user(user_id) serializer = self.OutputSerializer(user) return Response(serializer.data) class UpdateUserApi(APIView): - permission_classes = [permissions.IsAdminUser] - class InputSerializer(serializers.Serializer): name = serializers.CharField(required=False) + firebase_token = serializers.CharField(required=False) + fcm_token = serializers.CharField(required=False) location = inline_serializer( fields={ "gov": serializers.IntegerField(), @@ -76,13 +74,44 @@ class InputSerializer(serializers.Serializer): def post(self, request, user_id): serializer = self.InputSerializer(data=request.data) serializer.is_valid(raise_exception=True) + + user = get_user(user_id) + update_user( - user_id=user_id, + user=user, + performed_by=request.user, data=serializer.validated_data, ) return Response(status=status.HTTP_200_OK) +class UserCasesListApi(APIView): + class OutputSerializer(serializers.Serializer): + id = serializers.IntegerField() + type = serializers.CharField() + state = serializers.CharField(source="get_state_display") + name = serializers.CharField(source="details.name") + thumbnail = serializers.URLField() + last_seen = serializers.DateField(source="details.last_seen") + posted_at = serializers.DateTimeField() + location = inline_serializer( + fields={ + "gov": serializers.CharField(source="gov.name_ar"), + "city": serializers.CharField(source="city.name_ar"), + } + ) + + def get(self, request): + + # Listing all user cases + cases = get_user_cases(request.user) + + # Serializing the results + serializer = self.OutputSerializer(cases, many=True) + + return Response(serializer.data) + + class SetNationalIdApi(APIView): class InputSerializer(serializers.Serializer): national_id = serializers.CharField() diff --git a/api/users/selectors.py b/api/users/selectors.py index dc67fcd..7786b7e 100644 --- a/api/users/selectors.py +++ b/api/users/selectors.py @@ -1,7 +1,13 @@ +from django.db.models.query import QuerySet from django.shortcuts import get_object_or_404 +from api.cases.models import Case from api.users.models import User -def get_user(*, user_id: int) -> User: +def get_user(user_id: int) -> User: return get_object_or_404(User, pk=user_id) + + +def get_user_cases(user: User) -> QuerySet[Case]: + return user.cases.all() diff --git a/api/users/services.py b/api/users/services.py index 1f73066..0e8847e 100644 --- a/api/users/services.py +++ b/api/users/services.py @@ -2,6 +2,7 @@ from django.contrib.auth.hashers import make_password from django.contrib.auth.password_validation import validate_password +from django.core.exceptions import PermissionDenied from django.db import transaction from api.common.services import model_update @@ -46,7 +47,16 @@ def create_user( @transaction.atomic -def update_user(*, user: User, data: Dict) -> User: +def update_user( + *, + user: User, + performed_by: User, + data: Dict, +) -> User: + + if user != performed_by: + raise PermissionDenied() + non_side_effect_fields = ["name", "firebase_token"] user, _ = model_update( diff --git a/api/users/urls.py b/api/users/urls.py index 01897e7..8d6daa2 100644 --- a/api/users/urls.py +++ b/api/users/urls.py @@ -1,11 +1,18 @@ from django.urls import path -from api.users.apis import CreateUserApi, DetailUserApi, SetNationalIdApi, UpdateUserApi +from api.users.apis import ( + CreateUserApi, + DetailUserApi, + SetNationalIdApi, + UpdateUserApi, + UserCasesListApi, +) app_name = "users" urlpatterns = [ path("create/", CreateUserApi.as_view(), name="create_user"), path("/", DetailUserApi.as_view(), name="get_user"), path("/update/", UpdateUserApi.as_view(), name="update_user"), + path("cases/", UserCasesListApi.as_view(), name="get_user_cases"), path("/set/id/", SetNationalIdApi.as_view(), name="set_id"), ]