Skip to content

Commit

Permalink
First release (#33)
Browse files Browse the repository at this point in the history
Co-authored-by: root <[email protected]>
Co-authored-by: Osama Yasser <[email protected]>
  • Loading branch information
3 people authored May 15, 2022
1 parent 7f6c4ab commit f4d348e
Show file tree
Hide file tree
Showing 114 changed files with 5,348 additions and 500 deletions.
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

0 comments on commit f4d348e

Please sign in to comment.