diff --git a/api/cases/apis.py b/api/cases/apis.py index abb4e72..0990c9e 100644 --- a/api/cases/apis.py +++ b/api/cases/apis.py @@ -15,8 +15,8 @@ class InputSerializer(serializers.Serializer): photos_urls = serializers.ListField(child=serializers.URLField()) location = inline_serializer( fields={ - "gov_id": serializers.IntegerField(), - "city_id": serializers.IntegerField(), + "gov": serializers.IntegerField(), + "city": serializers.IntegerField(), "address": serializers.CharField(required=False), "lon": serializers.DecimalField( max_digits=9, decimal_places=6, required=False @@ -65,11 +65,15 @@ class OutputSerializer(serializers.Serializer): id = serializers.IntegerField() type = serializers.CharField() name = serializers.CharField(source="details.name") - gov = serializers.CharField(source="location.gov.name_ar") - city = serializers.CharField(source="location.city.name_ar") - photo = serializers.URLField(source="photo_urls") + 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.DateField() + posted_at = serializers.DateTimeField() def get(self, request): # Make sure the filters are valid, if passed @@ -92,11 +96,11 @@ class OutputSerializer(serializers.Serializer): user = serializers.IntegerField() type = serializers.CharField() state = serializers.CharField(source="get_state_display") - photos_urls = serializers.ListField(child=serializers.URLField()) + photos = serializers.ListField(source="photo_urls") location = inline_serializer( fields={ - "gov_id": serializers.IntegerField(), - "city_id": serializers.IntegerField(), + "gov": serializers.CharField(), + "city": serializers.CharField(), "address": serializers.CharField(), "lon": serializers.DecimalField( max_digits=9, diff --git a/api/cases/filters.py b/api/cases/filters.py index 5a5abb0..af1c4f6 100644 --- a/api/cases/filters.py +++ b/api/cases/filters.py @@ -26,4 +26,10 @@ class CaseFilter(django_filters.FilterSet): class Meta: model = Case - fields = ["type", "details__age", "details__last_seen", "location__gov", "name"] + fields = [ + "type", + "details__age", + "details__last_seen", + "location__gov", + "details__name", + ] diff --git a/api/cases/migrations/0006_auto_20220508_0416.py b/api/cases/migrations/0006_auto_20220508_0416.py new file mode 100644 index 0000000..cb90f46 --- /dev/null +++ b/api/cases/migrations/0006_auto_20220508_0416.py @@ -0,0 +1,24 @@ +# Generated by Django 3.2.13 on 2022-05-08 04:16 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('cases', '0005_alter_casedetails_last_seen'), + ] + + operations = [ + migrations.AlterField( + model_name='casematch', + name='case', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='old_matches', to='cases.case'), + ), + migrations.AlterField( + model_name='casematch', + name='match', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='new_matches', to='cases.case'), + ), + ] diff --git a/api/cases/models.py b/api/cases/models.py index a4fc198..a6b5475 100644 --- a/api/cases/models.py +++ b/api/cases/models.py @@ -1,7 +1,6 @@ -import datetime - from django.core.validators import MaxValueValidator, MinValueValidator from django.db import models +from django.utils import timezone from django.utils.translation import gettext_lazy as _ from django_fsm import FSMField, transition @@ -39,6 +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 @property def photo_urls(self): @@ -73,8 +73,9 @@ def archive(self): def activate_again(self): self.is_active = True + # FIXME def publish(self): - self.posted_at = datetime.now() + self.posted_at = timezone.now() class CaseDetails(models.Model): diff --git a/api/cases/selectors.py b/api/cases/selectors.py index 5f265af..853a85c 100644 --- a/api/cases/selectors.py +++ b/api/cases/selectors.py @@ -1,7 +1,7 @@ from django.core.exceptions import PermissionDenied -from django.db.models import Q +from django.db.models.query import Q, QuerySet +from django.shortcuts import get_object_or_404 -from api.common.utils import get_object from api.users.models import User from .filters import CaseFilter @@ -9,7 +9,7 @@ def get_case(*, pk: int, fetched_by: User) -> Case: - case = get_object(Case, pk=pk) + case = get_object_or_404(Case, pk=pk) if not (case.is_active or fetched_by == case.user): raise PermissionDenied() @@ -17,7 +17,7 @@ def get_case(*, pk: int, fetched_by: User) -> Case: return case -def list_case(*, filters=None): +def list_case(*, filters=None) -> QuerySet[Case]: filters = filters or {} # TODO Switch to posted cases only @@ -30,8 +30,8 @@ def list_user_case(*, user: User): return user.cases.all() -def list_case_match(*, pk: int, fetched_by: User): - case = get_object(Case, pk=pk) +def list_case_match(*, pk: int, fetched_by: User) -> QuerySet[Case]: + case = get_object_or_404(Case, pk=pk) if fetched_by != case.user: raise PermissionDenied() diff --git a/api/locations/selectors.py b/api/locations/selectors.py index ffc3552..d400ae0 100644 --- a/api/locations/selectors.py +++ b/api/locations/selectors.py @@ -1,11 +1,11 @@ -from typing import Iterable +from django.db.models.query import QuerySet from .models import City, Governorate -def list_governorate() -> Iterable[Governorate]: +def list_governorate() -> QuerySet[Governorate]: return Governorate.objects.all() -def list_cities() -> Iterable[City]: +def list_cities() -> QuerySet[City]: return City.objects.all() diff --git a/api/locations/services.py b/api/locations/services.py index 61defde..07b94f4 100644 --- a/api/locations/services.py +++ b/api/locations/services.py @@ -2,7 +2,6 @@ from django.conf import settings from django.shortcuts import get_object_or_404 -from rest_framework.exceptions import ValidationError from api.common.services import model_update from api.locations.models import City, Governorate, Location @@ -48,44 +47,45 @@ def create_location( lon: Optional[float] = None, lat: Optional[float] = None, address: Optional[str] = None, - gov_id: int, - city_id: int, + gov: int, + city: int, ) -> Location: - gov = Governorate.objects.get(pk=gov_id) - city = City.objects.get(pk=city_id) - loc = Location(lon=lon, lat=lat, address=address, gov=gov, city=city) - loc.full_clean() - # loc.clean() - loc.save() + # Fetch Governorate & City + gov = Governorate.objects.get(pk=gov) + city = City.objects.get(pk=city) - return loc + # Pack location data for validation + location = Location(lon=lon, lat=lat, address=address, gov=gov, city=city) + + # Data validation + location.full_clean() + + # Save location instance to the database + location.save() + + return location def update_location( *, - location_id: int, + location: Location, data: Dict, ) -> Location: - fields = ["lon", "lat", "address", "gov", "city"] - location = get_object_or_404(Location, pk=location_id) - - gov_id = data.get("gov_id") - city_id = data.get("city_id") - - gov = get_object_or_404(Governorate, pk=gov_id) - city = get_object_or_404(City, pk=city_id) + # Fetch Governorate & City if given + gov_id, city_id = data.get("gov"), data.get("city") - if city.gov != gov: - raise ValidationError("City does not belong to Governorate") + if gov_id: + data["gov"] = get_object_or_404(Governorate, pk=gov_id) + if city_id: + data["city"] = get_object_or_404(City, pk=city_id) - data["gov"] = gov - data["city"] = city + non_side_effect_fields = ["lon", "lat", "address", "gov", "city"] location, _ = model_update( instance=location, - fields=fields, + fields=non_side_effect_fields, data=data, ) diff --git a/api/users/apis.py b/api/users/apis.py index 53c622f..58ed28d 100644 --- a/api/users/apis.py +++ b/api/users/apis.py @@ -15,9 +15,12 @@ class InputSerializer(serializers.Serializer): username = serializers.CharField() password = serializers.CharField() name = serializers.CharField() - email = serializers.EmailField(required=False) - gov_id = serializers.IntegerField() - city_id = serializers.IntegerField() + location = inline_serializer( + fields={ + "gov": serializers.IntegerField(), + "city": serializers.IntegerField(), + } + ) fcm_token = serializers.CharField() firebase_token = serializers.CharField() @@ -35,22 +38,11 @@ class DetailUserApi(APIView): class OutputSerializer(serializers.Serializer): username = serializers.CharField() name = serializers.CharField() - email = serializers.CharField() location = inline_serializer( fields={ "address": serializers.CharField(), - "gov": inline_serializer( - fields={ - "name_ar": serializers.CharField(), - "name_en": serializers.CharField(), - } - ), - "city": inline_serializer( - fields={ - "name_ar": serializers.CharField(), - "name_en": serializers.CharField(), - } - ), + "gov": serializers.CharField(source="gov.name_ar"), + "city": serializers.CharField(source="city.name_ar"), } ) @@ -65,9 +57,20 @@ class UpdateUserApi(APIView): class InputSerializer(serializers.Serializer): name = serializers.CharField(required=False) - email = serializers.CharField(required=False) - gov_id = serializers.IntegerField(required=False) - city_id = serializers.IntegerField(required=False) + location = inline_serializer( + fields={ + "gov": serializers.IntegerField(), + "city": serializers.IntegerField(), + "address": serializers.CharField(required=False), + "lon": serializers.DecimalField( + max_digits=9, decimal_places=6, required=False + ), + "lat": serializers.DecimalField( + max_digits=8, decimal_places=6, required=False + ), + }, + required=False, + ) def post(self, request, user_id): serializer = self.InputSerializer(data=request.data) diff --git a/api/users/migrations/0003_alter_user_email.py b/api/users/migrations/0003_alter_user_email.py new file mode 100644 index 0000000..a5e1547 --- /dev/null +++ b/api/users/migrations/0003_alter_user_email.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.13 on 2022-05-08 04:16 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0002_remove_user_id_photo_url'), + ] + + operations = [ + migrations.AlterField( + model_name='user', + name='email', + field=models.EmailField(blank=True, max_length=254, verbose_name='email address'), + ), + ] diff --git a/api/users/models.py b/api/users/models.py index ddccd3f..407d940 100644 --- a/api/users/models.py +++ b/api/users/models.py @@ -12,7 +12,6 @@ class User(AbstractUser): last_name = None # type: ignore name = models.CharField(max_length=256) - email = models.EmailField(null=True, blank=True) username = models.CharField(max_length=10, unique=True, validators=[is_phone]) id_exp_date = models.DateTimeField(null=True, blank=True) diff --git a/api/users/services.py b/api/users/services.py index 3205416..a7d0dc2 100644 --- a/api/users/services.py +++ b/api/users/services.py @@ -1,7 +1,8 @@ -from typing import Dict, Optional +from typing import Dict +from django.contrib.auth.hashers import make_password +from django.contrib.auth.password_validation import validate_password from django.db import transaction -from django.shortcuts import get_object_or_404 from api.common.services import model_update from api.locations.models import Location @@ -16,23 +17,27 @@ def create_user( name: str, username: str, password: str, - email: Optional[str] = None, + firebase_token: str, + location: Dict, fcm_token: str, - firebase_token: Optional[str], - gov_id: int, - city_id: int, ) -> User: - loc: Location = create_location(gov_id=gov_id, city_id=city_id) + # Creating user's related entities + location: Location = create_location(**location) + + # Pack user data for validation user: User = User( - name=name, - username=username, - email=email, - location=loc, - firebase_token=firebase_token, + name=name, username=username, firebase_token=firebase_token, location=location ) - user.set_password(password) + + # Password validation + validate_password(password) + user.password = make_password(password) + + # Data validation user.full_clean() + + # Saving user to the database user.save() create_fcm_device(user=user, fcm_token=fcm_token) @@ -43,29 +48,19 @@ def create_user( @transaction.atomic def update_user( *, - user_id: int, + user: User, data: Dict, ) -> User: - fields = ["name", "email"] - - user = get_object_or_404(User, pk=user_id) - - gov_id = data.get("gov_id") - city_id = data.get("city_id") - - if gov_id and city_id: - update_location( - location_id=user.location.id, - data={ - "gov_id": data.get("gov_id"), - "city_id": data.get("city_id"), - }, - ) + non_side_effect_fields = ["name", "firebase_token"] user, _ = model_update( instance=user, - fields=fields, + fields=non_side_effect_fields, data=data, ) + location_data = data.get("location") + if location_data: + update_location(location=user.location, data=location_data) + return user diff --git a/api/users/tests/test_models.py b/api/users/tests/test_models.py index bf7b043..51cdd84 100644 --- a/api/users/tests/test_models.py +++ b/api/users/tests/test_models.py @@ -13,11 +13,9 @@ def setUpTestData(cls): # Called once at the beginning of the test run name="Osama Yasser", username="1005499972", password="hardpassword", - email="osamayasserr@gmail.com", firebase_token="token", + location={"gov": 1, "city": "4"}, fcm_token="fcm_token", - gov_id="1", - city_id="4", ) def test_name_max_lenght(self):