Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ jobs:

- name: Run Django Tests
# Need to run as root because we are writing the results into the shared volume.
run: docker compose -f docker-compose-local.yml run -u root --rm django coverage run manage.py test --noinput
run: docker compose -f docker-compose-local.yml run -u root --rm django coverage run -m pytest

- name: Create coverage XML report
# Need to run as root because we are writing the results into the shared volume.
Expand Down
1 change: 1 addition & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ RUN pip install numpy==1.23.1

WORKDIR /app
COPY . /app
ENV PYTHONPATH "${PYTHONPATH}:/app/"

RUN pip install -r requirements/dev.pip

Expand Down
1 change: 1 addition & 0 deletions api/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
default_app_config = "api.apps.ApiConfig"
10 changes: 10 additions & 0 deletions api/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from django.apps import AppConfig


class ApiConfig(AppConfig):
name = "api"
label = "api"

def ready(self) -> None:
import api.schema
import api.auth.schema
Empty file added api/auth/__init__.py
Empty file.
9 changes: 9 additions & 0 deletions api/auth/authentication.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from rest_framework_simplejwt.authentication import JWTAuthentication

from tigaserver_app.models import TigaUser


class AppUserJWTAuthentication(JWTAuthentication):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.user_model = TigaUser
5 changes: 5 additions & 0 deletions api/auth/schema.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from drf_spectacular.contrib.rest_framework_simplejwt import SimpleJWTScheme


class AppUserJWTAuthentication(SimpleJWTScheme):
target_class = "api.auth.authentication.AppUserJWTAuthentication"
43 changes: 43 additions & 0 deletions api/auth/serializers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
from django.contrib.auth import authenticate

from rest_framework import exceptions, serializers
from rest_framework_simplejwt.settings import api_settings
from rest_framework_simplejwt.serializers import (
TokenObtainSerializer,
TokenObtainPairSerializer,
)


class AppUserTokenObtainSerializer(TokenObtainSerializer):
uuid = serializers.UUIDField(required=True)

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)

del self.fields[self.username_field]

def validate(self, attrs):
authenticate_kwargs = {
"uuid": attrs["uuid"],
"password": attrs["password"], # or None,
}
try:
authenticate_kwargs["request"] = self.context["request"]
except KeyError:
pass

self.user = authenticate(**authenticate_kwargs)

if not api_settings.USER_AUTHENTICATION_RULE(self.user):
raise exceptions.AuthenticationFailed(
self.error_messages["no_active_account"],
"no_active_account",
)

return {}


class AppUserTokenObtainPairSerializer(
TokenObtainPairSerializer, AppUserTokenObtainSerializer
):
pass
108 changes: 108 additions & 0 deletions api/base_serializers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
from django.db.models import Model
from django.core.exceptions import ImproperlyConfigured

from rest_framework import serializers
from rest_framework.exceptions import ValidationError
from rest_framework.fields import empty


class FieldPolymorphicSerializer(serializers.Serializer):
field_value_serializer_mapping = None
resource_type_field_name = "type"
model = None

def __new__(cls, *args, **kwargs):
if cls.field_value_serializer_mapping is None:
raise ImproperlyConfigured(
"`{cls}` is missing a "
"`{cls}.field_value_serializer_mapping` attribute".format(
cls=cls.__name__
)
)
if not isinstance(cls.resource_type_field_name, str):
raise ImproperlyConfigured(
"`{cls}.resource_type_field_name` must be a string".format(
cls=cls.__name__
)
)
if not issubclass(cls.model, Model):
raise ImproperlyConfigured(
"`{cls}.model` must be a django Model".format(cls=cls.__name__)
)
return super().__new__(cls, *args, **kwargs)

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
field_value_serializer_mapping = self.field_value_serializer_mapping
self.field_value_serializer_mapping = {}
for field_value, serializer in field_value_serializer_mapping.items():
if callable(serializer):
serializer = serializer(*args, **kwargs)
serializer.parent = self

self.field_value_serializer_mapping[field_value] = serializer

def to_representation(self, instance):
serializer = self._get_serializer_for_instance(instance=instance)
return serializer.to_representation(instance)

def to_internal_value(self, data):
if self.instance:
serializer = self._get_serializer_for_instance(instance=self.instance)
else:
serializer = self._get_serializer_for_data(data=data)
return serializer.to_internal_value(data=data)

def create(self, validated_data):
serializer = self._get_serializer_for_data(data=validated_data)
return serializer.create(validated_data)

def update(self, instance, validated_data):
serializer = self._get_serializer_for_data(data=validated_data)
return serializer.update(instance, validated_data)

def is_valid(self, raise_exception=False):
valid = super().is_valid(raise_exception)

try:
if self.instance:
serializer = self._get_serializer_for_instance(instance=self.instance)
else:
serializer = self._get_serializer_for_data(data=self.initial_data)
except ValidationError:
child_valid = False
else:
child_valid = serializer.is_valid(raise_exception)
self._errors.update(serializer.errors)

return valid and child_valid

def run_validation(self, data=empty):
if self.instance:
serializer = self._get_serializer_for_instance(instance=self.instance)
else:
serializer = self._get_serializer_for_data(data=data)
return serializer.run_validation(data)

def _get_serializer_for_type(self, type_value):
try:
return self.field_value_serializer_mapping[type_value]
except KeyError:
raise ValidationError(
f"Value {type_value} has not serializer assigned for field '{self.resource_type_field_name}'"
)

def _get_serializer_for_instance(self, instance):
return self._get_serializer_for_type(
type_value=getattr(instance, self.resource_type_field_name)
)

def _get_serializer_for_data(self, data):
try:
return self._get_serializer_for_type(
type_value=data[self.resource_type_field_name]
)
except KeyError:
raise ValidationError(
f"{self.resource_type_field_name} is a required field."
)
82 changes: 82 additions & 0 deletions api/fields.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import pytz

try:
import zoneinfo
except ImportError:
from backports import zoneinfo

from django.utils.dateparse import parse_datetime
from django.core.exceptions import ValidationError
from rest_framework import serializers

from drf_extra_fields.geo_fields import PointField
from timezone_field.utils import use_pytz_default
from timezone_field.rest_framework import TimeZoneSerializerField


class TimezoneAwareDateTimeField(serializers.DateTimeField):
def to_internal_value(self, value):
# Parse the datetime string into a datetime object
parsed_dt = parse_datetime(value)

# Ensure that the datetime contains timezone information
if parsed_dt.tzinfo is None:
raise ValidationError("Datetime value must include timezone information.")

return super().to_internal_value(value=value)


class ExpandedPointField(PointField):
def __init__(self, *args, **kwargs):
super().__init__(str_points=True, *args, **kwargs)


class IntegerDefaultField(serializers.IntegerField):
# When db value is None, use the default.
def get_attribute(self, instance):
attibute = super().get_attribute(instance)
if attibute is None and self.default != serializers.empty:
attibute = self.default
return attibute


class TimeZoneSerializerChoiceField(TimeZoneSerializerField, serializers.ChoiceField):
def __init__(self, **kwargs):
self.use_pytz = kwargs.pop("use_pytz", use_pytz_default())

_tzstrs = (
pytz.common_timezones if self.use_pytz else zoneinfo.available_timezones()
)

super().__init__(choices=_tzstrs, html_cutoff=5, **kwargs)


class WritableSerializerMethodField(serializers.SerializerMethodField):
# Reference: https://stackoverflow.com/a/64274128/8450576
def __init__(self, **kwargs):
self.setter_method_name = kwargs.pop("setter_method_name", None)
self.deserializer_field = kwargs.pop("deserializer_field")

super().__init__(**kwargs)

self.read_only = False
self.required = True

def bind(self, field_name, parent):
retval = super().bind(field_name, parent)
if not self.setter_method_name:
self.setter_method_name = f"set_{field_name}"

return retval

def get_default(self):
default = super().get_default()

return {self.field_name: default}

def to_internal_value(self, data):
value = self.deserializer_field.to_internal_value(data)
method = getattr(self.parent, self.setter_method_name)
return {
self.field_name: self.deserializer_field.to_internal_value(method(value))
}
86 changes: 86 additions & 0 deletions api/filters.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
from django.db import models
from django.utils import timezone

from django_filters import rest_framework as filters

from tigaserver_app.models import Report, Notification, OWCampaigns


class CampaignFilter(filters.FilterSet):
country_id = filters.CharFilter(field_name="country_id")
is_active = filters.BooleanFilter(method="filter_is_active")

order_by = filters.OrderingFilter(
fields=(
("campaign_start_date", "start_date"),
("campaign_end_date", "end_date"),
)
)

def filter_is_active(self, queryset, name, value):
return queryset.filter(
models.Q(
models.Q(campaign_start_date__lte=timezone.now())
& (
models.Q(campaign_end_date__isnull=True)
| models.Q(campaign_end_date__gt=timezone.now())
),
_negated=not value,
)
)

class Meta:
model = OWCampaigns
fields = ("country_id", "is_active")


class ReportFilter(filters.FilterSet):
user_uuid = filters.UUIDFilter(field_name="user")
short_id = filters.CharFilter(field_name="report_id", label="Short ID")
created_at = filters.IsoDateTimeFromToRangeFilter(
field_name="creation_time", label="Created at"
)
received_at = filters.IsoDateTimeFromToRangeFilter(
field_name="server_upload_time", label="Received at"
)
updated_at = filters.IsoDateTimeFromToRangeFilter(
field_name="updated_at", label="Update at"
)
has_photos = filters.BooleanFilter(
field_name="photos", lookup_expr="isnull", exclude=True, label="Has any photo"
)

location_country = filters.CharFilter(field_name="country")
location_nuts_3 = filters.CharFilter(field_name="nuts_3")
location_nuts_2 = filters.CharFilter(field_name="nuts_2")

order_by = filters.OrderingFilter(
fields=(("server_upload_time", "received_at"), ("creation_time", "created_at"))
)

class Meta:
model = Report
fields = (
"short_id",
"type",
"created_at",
"received_at",
"updated_at",
"location_country",
"location_nuts_3",
"location_nuts_2",
"has_photos",
)


class NotificationFilter(filters.FilterSet):
seen = filters.BooleanFilter(method="filter_seen")

order_by = filters.OrderingFilter(fields=(("date_comment", "created_at"),))

def filter_seen(self, queryset, name, value):
return queryset.seen_by_user(user=self.request.user, state=value)

class Meta:
model = Notification
fields = ("seen",)
Loading
Loading