Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
96 commits
Select commit Hold shift + click to select a range
dbbee72
chore: crete auth app
Apr 16, 2022
4d4b059
chore: add auth to installed apps
oyasr Apr 16, 2022
a8ad3b2
chore: add .vscode to gitignore
oyasr Apr 16, 2022
94b8696
feat: create initial user model
oyasr Apr 16, 2022
9b30190
chore: create locations app
oyasr Apr 16, 2022
7adf5a3
feat: create initial locations models
oyasr Apr 16, 2022
120a724
feat: add govs and cities data
oyasr Apr 16, 2022
974fd8f
feat: add gov-city db population service
oyasr Apr 16, 2022
5abf6cc
feat: add user create service
oyasr Apr 17, 2022
06118a0
merge dev
oyasr Apr 17, 2022
d3dc2c4
refactor: move auth app inside api/
oyasr Apr 17, 2022
e5332fd
feat: Implement Case model
OsamaRagab520 Apr 17, 2022
ed92a3d
feat: Implement Case services
OsamaRagab520 Apr 17, 2022
c81d9f2
Merge branch 'dev' into feature/case-model
OsamaRagab520 Apr 17, 2022
07d909b
fix: add cases app to local apps
oyasr Apr 17, 2022
1e78ea0
fix: locations typo
oyasr Apr 17, 2022
35e28d2
fix: add api namespace to app names
oyasr Apr 17, 2022
fc8d6eb
fix: rquirements typo
oyasr Apr 17, 2022
fee730d
fix: circular import
oyasr Apr 17, 2022
ff30b99
Merge branch 'dev' into feature/user-authentication
oyasr Apr 17, 2022
6fbdc72
feat: add location attribute to case model
OsamaRagab520 Apr 17, 2022
c614576
Merge branch 'dev' into feature/case-model
OsamaRagab520 Apr 17, 2022
796ad07
fix: admin page typo
oyasr Apr 17, 2022
b4f1678
Merge branch 'dev' into feature/user-model
oyasr Apr 17, 2022
7b77806
Merge branch 'dev' into feature/case-model
OsamaRagab520 Apr 17, 2022
a631c03
feat: initial database migration
oyasr Apr 17, 2022
27eed05
feat: add cases app models to admin site
OsamaRagab520 Apr 17, 2022
3aaeba6
feat: add locations app models to admin site
oyasr Apr 17, 2022
755fd88
Merge branch 'dev' into feature/user-authentication
OsamaRagab520 Apr 17, 2022
619987c
chore: update requirements folder
oyasr Apr 17, 2022
0816e2e
fix: json paths
oyasr Apr 17, 2022
457c8cd
Merge branch 'dev' into feature/location-model
oyasr Apr 17, 2022
a7b64e4
feat: add governorate list selector
oyasr Apr 17, 2022
2e1de07
chore: add dj-rest-auth lib
OsamaRagab520 Apr 17, 2022
4b08e24
Merge branch 'dev' into feature/user-authentication
OsamaRagab520 Apr 17, 2022
9892c81
fix: add None as default value for lon & lat
oyasr Apr 17, 2022
c5e47a5
fix: add firebase token to create user service
oyasr Apr 17, 2022
acef245
Merge branch 'dev' into feature/user-authentication
OsamaRagab520 Apr 17, 2022
612641c
feat: implement user registration
OsamaRagab520 Apr 17, 2022
6a191ad
chore: rename auth app to authentication
OsamaRagab520 Apr 18, 2022
cc16528
fix: RegisterSerializer fields
OsamaRagab520 Apr 18, 2022
10742a9
fix: location clean logic
oyasr Apr 18, 2022
7b59ff2
Merge branch 'dev' into feature/location-model
oyasr Apr 18, 2022
43bcb2c
Merge branch 'dev' into feature/user-authentication
OsamaRagab520 Apr 18, 2022
d6df20c
fix: modify address to accpet null
OsamaRagab520 Apr 18, 2022
fe03a91
Merge branch 'dev' into feature/location-model
OsamaRagab520 Apr 18, 2022
bb3f8fe
Merge branch 'dev' into feature/user-authentication
OsamaRagab520 Apr 18, 2022
198caa6
feat: create custom error handler
oyasr Apr 19, 2022
1737adf
fix: update token auth
OsamaRagab520 Apr 19, 2022
7506331
Merge branch 'dev' of https://github.com/MafQud/API into dev
OsamaRagab520 Apr 19, 2022
a4a1306
feat: implement user registration
OsamaRagab520 Apr 19, 2022
5ef868b
Merge branch 'dev' into feature/user-model
OsamaRagab520 Apr 19, 2022
507a8df
refactor: create_location service
OsamaRagab520 Apr 19, 2022
ce911ae
Merge branch 'dev' into feature/location-model
OsamaRagab520 Apr 19, 2022
28148e9
Merge branch 'feature/location-model' into dev
OsamaRagab520 Apr 19, 2022
c571802
Merge branch 'dev' into feature/user-authentication
OsamaRagab520 Apr 19, 2022
f086437
refactor: migrate to simpleJWT
OsamaRagab520 Apr 19, 2022
9ffb86b
fix: CreateUserApi
OsamaRagab520 Apr 19, 2022
aa00cfa
fix: circural referance
OsamaRagab520 Apr 19, 2022
e8e3ebf
Merge branch 'feature/api-errors' into dev
OsamaRagab520 Apr 19, 2022
c58b39b
Merge branch 'feature/user-model' into dev
OsamaRagab520 Apr 19, 2022
7352c07
chore: apply new model migrations
OsamaRagab520 Apr 19, 2022
5f73013
fix: remove dj-rest-auth from installed apps
oyasr Apr 19, 2022
083303a
Merge branch 'dev' into feature/case-model
OsamaRagab520 Apr 20, 2022
0e21f5d
feat: add common tools
OsamaRagab520 Apr 20, 2022
aa03b9d
Merge branch 'dev' into feature/case-model
OsamaRagab520 Apr 20, 2022
0850ecc
refactor: case services
OsamaRagab520 Apr 20, 2022
7e2d5a3
feat: add custom permissions
oyasr Apr 21, 2022
2dd6f16
refactor: CaseDetails model
OsamaRagab520 Apr 21, 2022
191919c
refactor: case services
OsamaRagab520 Apr 21, 2022
aa9dc34
feat: implement case create view
OsamaRagab520 Apr 21, 2022
0da530b
Merge branch 'dev' into feature/case-model
OsamaRagab520 Apr 22, 2022
390f519
feat: s3 direct upload
oyasr Apr 22, 2022
898f6d0
feat: implement user detail view
oyasr Apr 23, 2022
bcfb922
feat: implement user update view
oyasr Apr 24, 2022
9f914c1
feat: implement update location service
oyasr Apr 24, 2022
7e51ec3
Merge branch 'dev' into feature/user-model
oyasr Apr 24, 2022
0ec293b
refactor: update user location in place
oyasr Apr 24, 2022
99d549d
refactor: make urls for each app
oyasr Apr 24, 2022
1e650b9
fix: api namespace collision
oyasr Apr 24, 2022
3f20267
test: user model test cases
oyasr Apr 24, 2022
e2d1c90
feat: implement DetailCaseApi
OsamaRagab520 Apr 24, 2022
ca2c659
Merge branch 'dev' into feature/case-model
OsamaRagab520 Apr 26, 2022
00766bc
fix: append api namespace to apis url
oyasr Apr 26, 2022
0feb4e8
Merge branch 'feature/user-authentication' into dev
oyasr Apr 26, 2022
dac4f4b
feat: implement CaseListApi
OsamaRagab520 May 3, 2022
b669bf6
Merge branch 'dev' into feature/case-model
OsamaRagab520 May 3, 2022
800eadb
chore: run jobs on dev PR & commits
OsamaRagab520 May 8, 2022
f6a96f6
MAFQUD-208 : notification system (#20)
oyasr May 8, 2022
a9f24ee
MAFQUD-206 : phone validation (#21)
oyasr May 8, 2022
c5e5f97
MAFQUD-207 : Refactor services (#22)
OsamaRagab520 May 8, 2022
c1e5893
MAFQUD-205 : Write list case matches Endpoint (#26)
OsamaRagab520 May 10, 2022
c149531
MAFQUD-210: national id (#27)
oyasr May 10, 2022
3f1d6b7
Final tweaks (#30)
OsamaRagab520 May 13, 2022
e75b3f7
chore: disable push notification & hot fixes
OsamaRagab520 May 13, 2022
de3ab40
chore: integrate files into cases instead of urls
OsamaRagab520 May 15, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .envs/.local/.django
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
# ------------------------------------------------------------------------------
USE_DOCKER=yes
IPYTHONDIR=/app/.ipython

# Redis
# ------------------------------------------------------------------------------
REDIS_URL=redis://redis:6379/0
Expand All @@ -12,3 +13,11 @@ REDIS_URL=redis://redis:6379/0
# Flower
CELERY_FLOWER_USER=debug
CELERY_FLOWER_PASSWORD=debug

# Files
# ------------------------------------------------------------------------------
FILE_UPLOAD_STORAGE=local

# Firebase
# ------------------------------------------------------------------------------
GOOGLE_APPLICATION_CREDENTIALS=/home/$USER/google-services.json
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@ env:

on:
pull_request:
branches: [ "master", "main" ]
branches: [ "master", "main", "dev"]
paths-ignore: [ "docs/**" ]

push:
branches: [ "master", "main" ]
branches: [ "master", "main", "dev"]
paths-ignore: [ "docs/**" ]

concurrency:
Expand Down
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -277,3 +277,9 @@ api/media/
.env
.envs/*
!.envs/.local/

# VScode
.vscode

# Media files
media
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -53,4 +53,4 @@ destroy:


rm_pyc:
find . -name '__pycache__' -name '*.pyc' | xargs rm -rf
find . -name '__pycache__' -name '*.pyc' | xargs rm -rf
1 change: 1 addition & 0 deletions api/apis/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Init
6 changes: 6 additions & 0 deletions api/apis/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from django.apps import AppConfig


class ApisConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "api.apis"
1 change: 1 addition & 0 deletions api/apis/migrations/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Init
55 changes: 55 additions & 0 deletions api/apis/pagination.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
from collections import OrderedDict

from rest_framework.pagination import LimitOffsetPagination as _LimitOffsetPagination
from rest_framework.response import Response


def get_paginated_response(
*, pagination_class, serializer_class, queryset, request, view
):
paginator = pagination_class()

page = paginator.paginate_queryset(queryset, request, view=view)

if page is not None:
serializer = serializer_class(page, many=True)
return paginator.get_paginated_response(serializer.data)

serializer = serializer_class(queryset, many=True)

return Response(data=serializer.data)


class LimitOffsetPagination(_LimitOffsetPagination):
default_limit = 10
max_limit = 50

def get_paginated_data(self, data):
return OrderedDict(
[
("limit", self.limit),
("offset", self.offset),
("count", self.count),
("next", self.get_next_link()),
("previous", self.get_previous_link()),
("results", data),
]
)

def get_paginated_response(self, data):
"""
We redefine this method in order to return `limit` and `offset`.
This is used by the frontend to construct the pagination itself.
"""
return Response(
OrderedDict(
[
("limit", self.limit),
("offset", self.offset),
("count", self.count),
("next", self.get_next_link()),
("previous", self.get_previous_link()),
("results", data),
]
)
)
1 change: 1 addition & 0 deletions api/apis/tests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Tests
11 changes: 11 additions & 0 deletions api/apis/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from django.urls import include, path

app_name = "apis"
urlpatterns = [
path("auth/", include("api.authentication.urls", "authentication")),
path("users/", include("api.users.urls", "users")),
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")),
]
1 change: 1 addition & 0 deletions api/authentication/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Init
1 change: 1 addition & 0 deletions api/authentication/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Admin
22 changes: 22 additions & 0 deletions api/authentication/apis.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from rest_framework import permissions, serializers, status
from rest_framework.response import Response
from rest_framework.views import APIView

from api.authentication.selectors import validate_phone
from api.common.validators import is_phone


class ValidatePhoneAPI(APIView):
permission_classes = [permissions.AllowAny]

class InputSerializer(serializers.Serializer):
phone = serializers.CharField(validators=[is_phone])

def post(self, request):
serializer = self.InputSerializer(data=request.data)
serializer.is_valid(raise_exception=True)

# Raises validation error if phone is taken
validate_phone(**serializer.validated_data)

return Response(status=status.HTTP_200_OK)
6 changes: 6 additions & 0 deletions api/authentication/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from django.apps import AppConfig


class AuthConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "api.authentication"
1 change: 1 addition & 0 deletions api/authentication/migrations/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Init
1 change: 1 addition & 0 deletions api/authentication/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Models
12 changes: 12 additions & 0 deletions api/authentication/selectors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from typing import Union

from rest_framework.exceptions import ValidationError

from api.users.models import User


def validate_phone(*, phone: str) -> Union[None, ValidationError]:
if User.objects.filter(username=phone).exists():
raise ValidationError(f"Phone number: {phone} already taken")

return None
16 changes: 16 additions & 0 deletions api/authentication/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from django.urls import path
from rest_framework_simplejwt.views import (
TokenObtainPairView,
TokenRefreshView,
TokenVerifyView,
)

from api.authentication.apis import ValidatePhoneAPI

app_name = "auth"
urlpatterns = [
path("token/", TokenObtainPairView.as_view(), name="obtain_token"),
path("token/refresh/", TokenRefreshView.as_view(), name="refresh_token"),
path("token/verify/", TokenVerifyView.as_view(), name="verify_token"),
path("phone/validate/", ValidatePhoneAPI.as_view(), name="validate_phone"),
]
Empty file added api/cases/__init__.py
Empty file.
5 changes: 5 additions & 0 deletions api/cases/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from django.contrib import admin

from .models import Case, CaseDetails, CaseMatch, CasePhoto

admin.site.register((Case, CaseDetails, CaseMatch, CasePhoto))
178 changes: 178 additions & 0 deletions api/cases/apis.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
from rest_framework import serializers, status
from rest_framework.response import Response
from rest_framework.views import APIView

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, publish_case
from api.common.utils import inline_serializer


class CreateCaseApi(APIView):
class InputSerializer(serializers.Serializer):
type = serializers.CharField()
thumbnail = serializers.IntegerField()
file_ids = serializers.ListField(child=serializers.IntegerField())
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
),
}
)
details = inline_serializer(
fields={
"name": serializers.CharField(required=False),
"gender": serializers.CharField(required=False),
"age": serializers.IntegerField(required=False),
"last_seen": serializers.DateField(required=False),
"description": serializers.CharField(required=False),
"location": inline_serializer(
required=False,
fields={**location.fields},
),
}
)

def post(self, request):
serializer = self.InputSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
create_case(user=request.user, **serializer.validated_data)

return Response(status=status.HTTP_201_CREATED)


class CaseListApi(APIView):
class Pagination(LimitOffsetPagination):
default_limit = 10

class FilterSerializer(serializers.Serializer):
type = serializers.CharField(required=False)
start_age = serializers.IntegerField(required=False)
end_age = serializers.IntegerField(required=False)
start_date = serializers.DateField(required=False)
end_date = serializers.DateField(required=False)
gov = serializers.IntegerField(required=False)
name = serializers.CharField(required=False)

class OutputSerializer(serializers.Serializer):
id = serializers.IntegerField()
type = serializers.CharField()
name = serializers.CharField(source="details.name")
thumbnail = serializers.URLField(source="thumbnail.url")
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):
# Make sure the filters are valid, if passed
filters_serializer = self.FilterSerializer(data=request.query_params)
filters_serializer.is_valid(raise_exception=True)

cases = list_case(filters=filters_serializer.validated_data)

return get_paginated_response(
pagination_class=self.Pagination,
serializer_class=self.OutputSerializer,
queryset=cases,
request=request,
view=self,
)


class DetailsCaseApi(APIView):
class OutputSerializer(serializers.Serializer):
user = serializers.CharField(source="user.username")
type = serializers.CharField()
state = serializers.CharField(source="get_state_display")
photos = serializers.ListField(source="photo_urls")
location = inline_serializer(
fields={
"gov": serializers.CharField(),
"city": serializers.CharField(),
"address": serializers.CharField(),
"lon": serializers.DecimalField(
max_digits=9,
decimal_places=6,
),
"lat": serializers.DecimalField(
max_digits=8,
decimal_places=6,
),
}
)
details = inline_serializer(
fields={
"name": serializers.CharField(),
"gender": serializers.CharField(),
"age": serializers.IntegerField(),
"last_seen": serializers.DateField(),
"description": serializers.CharField(),
"location": location,
}
)

def get(self, request, case_id):
case = get_case(pk=case_id, fetched_by=request.user)

serializer = self.OutputSerializer(case)

return Response(serializer.data)


class CaseMatchListApi(APIView):
def get(self, request, case_id):

# Fetching our case
case = get_case(pk=case_id, fetched_by=request.user)

# 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
class OutputSerializer(serializers.Serializer):
case = inline_serializer(
fields={
"id": serializers.IntegerField(),
"type": serializers.CharField(),
"name": serializers.CharField(source="details.name"),
"location": inline_serializer(
fields={
"gov": serializers.CharField(source="gov.name_ar"),
"city": serializers.CharField(source="city.name_ar"),
},
),
"thumbnail": serializers.URLField(source="thumbnail.url"),
"last_seen": serializers.DateField(source="details.last_seen"),
"posted_at": serializers.DateTimeField(),
},
source=case_source,
)
score = serializers.IntegerField()

# Listing all case matches
matches = list_case_match(case=case, fetched_by=request.user)

# Serializing the results
serializer = OutputSerializer(matches, many=True)

return Response(serializer.data)


class CasePublishApi(APIView):
def get(self, request, case_id):
case = get_case(pk=case_id, fetched_by=request.user)
publish_case(case=case, performed_by=request.user)
return Response(status=status.HTTP_200_OK)
6 changes: 6 additions & 0 deletions api/cases/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from django.apps import AppConfig


class CasesConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "api.cases"
Loading