Skip to content

Commit d31e802

Browse files
committed
Created v1 API.
1 parent 8330f7a commit d31e802

File tree

101 files changed

+5259
-19
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

101 files changed

+5259
-19
lines changed

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ jobs:
4444

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

4949
- name: Create coverage XML report
5050
# Need to run as root because we are writing the results into the shared volume.

Dockerfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ RUN pip install numpy==1.23.1
3737

3838
WORKDIR /app
3939
COPY . /app
40+
ENV PYTHONPATH "${PYTHONPATH}:/app/"
4041

4142
RUN pip install -r requirements/dev.pip
4243

api/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
default_app_config = "api.apps.ApiConfig"

api/apps.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
from django.apps import AppConfig
2+
3+
4+
class ApiConfig(AppConfig):
5+
name = "api"
6+
label = "api"
7+
8+
def ready(self) -> None:
9+
import api.schema
10+
import api.auth.schema

api/auth/__init__.py

Whitespace-only changes.

api/auth/authentication.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
from rest_framework_simplejwt.authentication import JWTAuthentication
2+
3+
from tigaserver_app.models import TigaUser
4+
5+
6+
class AppUserJWTAuthentication(JWTAuthentication):
7+
def __init__(self, *args, **kwargs):
8+
super().__init__(*args, **kwargs)
9+
self.user_model = TigaUser

api/auth/schema.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
from drf_spectacular.contrib.rest_framework_simplejwt import SimpleJWTScheme
2+
3+
4+
class AppUserJWTAuthentication(SimpleJWTScheme):
5+
target_class = "api.auth.authentication.AppUserJWTAuthentication"

api/auth/serializers.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
from django.contrib.auth import authenticate
2+
3+
from rest_framework import exceptions, serializers
4+
from rest_framework_simplejwt.settings import api_settings
5+
from rest_framework_simplejwt.serializers import (
6+
TokenObtainSerializer,
7+
TokenObtainPairSerializer,
8+
)
9+
10+
11+
class AppUserTokenObtainSerializer(TokenObtainSerializer):
12+
uuid = serializers.UUIDField(required=True)
13+
device_token = serializers.CharField(allow_blank=True)
14+
15+
def __init__(self, *args, **kwargs):
16+
super().__init__(*args, **kwargs)
17+
18+
del self.fields[self.username_field]
19+
del self.fields["password"]
20+
21+
def validate(self, attrs):
22+
authenticate_kwargs = {
23+
"uuid": attrs["uuid"],
24+
"device_token": attrs["device_token"], # or None,
25+
}
26+
try:
27+
authenticate_kwargs["request"] = self.context["request"]
28+
except KeyError:
29+
pass
30+
31+
self.user = authenticate(**authenticate_kwargs)
32+
33+
if not api_settings.USER_AUTHENTICATION_RULE(self.user):
34+
raise exceptions.AuthenticationFailed(
35+
self.error_messages["no_active_account"],
36+
"no_active_account",
37+
)
38+
39+
return {}
40+
41+
42+
class AppUserTokenObtainPairSerializer(
43+
TokenObtainPairSerializer, AppUserTokenObtainSerializer
44+
):
45+
pass

api/base_serializers.py

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
from django.db.models import Model
2+
from django.core.exceptions import ImproperlyConfigured
3+
4+
from rest_framework import serializers
5+
from rest_framework.exceptions import ValidationError
6+
from rest_framework.fields import empty
7+
8+
9+
class FieldPolymorphicSerializer(serializers.Serializer):
10+
field_value_serializer_mapping = None
11+
resource_type_field_name = "type"
12+
model = None
13+
14+
def __new__(cls, *args, **kwargs):
15+
if cls.field_value_serializer_mapping is None:
16+
raise ImproperlyConfigured(
17+
"`{cls}` is missing a "
18+
"`{cls}.field_value_serializer_mapping` attribute".format(
19+
cls=cls.__name__
20+
)
21+
)
22+
if not isinstance(cls.resource_type_field_name, str):
23+
raise ImproperlyConfigured(
24+
"`{cls}.resource_type_field_name` must be a string".format(
25+
cls=cls.__name__
26+
)
27+
)
28+
if not issubclass(cls.model, Model):
29+
raise ImproperlyConfigured(
30+
"`{cls}.model` must be a django Model".format(cls=cls.__name__)
31+
)
32+
return super().__new__(cls, *args, **kwargs)
33+
34+
def __init__(self, *args, **kwargs):
35+
super().__init__(*args, **kwargs)
36+
field_value_serializer_mapping = self.field_value_serializer_mapping
37+
self.field_value_serializer_mapping = {}
38+
for field_value, serializer in field_value_serializer_mapping.items():
39+
if callable(serializer):
40+
serializer = serializer(*args, **kwargs)
41+
serializer.parent = self
42+
43+
self.field_value_serializer_mapping[field_value] = serializer
44+
45+
def to_representation(self, instance):
46+
serializer = self._get_serializer_for_instance(instance=instance)
47+
return serializer.to_representation(instance)
48+
49+
def to_internal_value(self, data):
50+
if self.instance:
51+
serializer = self._get_serializer_for_instance(instance=self.instance)
52+
else:
53+
serializer = self._get_serializer_for_data(data=data)
54+
return serializer.to_internal_value(data=data)
55+
56+
def create(self, validated_data):
57+
serializer = self._get_serializer_for_data(data=validated_data)
58+
return serializer.create(validated_data)
59+
60+
def update(self, instance, validated_data):
61+
serializer = self._get_serializer_for_data(data=validated_data)
62+
return serializer.update(instance, validated_data)
63+
64+
def is_valid(self, raise_exception=False):
65+
valid = super().is_valid(raise_exception)
66+
67+
try:
68+
if self.instance:
69+
serializer = self._get_serializer_for_instance(instance=self.instance)
70+
else:
71+
serializer = self._get_serializer_for_data(data=self.initial_data)
72+
except ValidationError:
73+
child_valid = False
74+
else:
75+
child_valid = serializer.is_valid(raise_exception)
76+
self._errors.update(serializer.errors)
77+
78+
return valid and child_valid
79+
80+
def run_validation(self, data=empty):
81+
if self.instance:
82+
serializer = self._get_serializer_for_instance(instance=self.instance)
83+
else:
84+
serializer = self._get_serializer_for_data(data=data)
85+
return serializer.run_validation(data)
86+
87+
def _get_serializer_for_type(self, type_value):
88+
try:
89+
return self.field_value_serializer_mapping[type_value]
90+
except KeyError:
91+
raise ValidationError(
92+
f"Value {type_value} has not serializer assigned for field '{self.resource_type_field_name}'"
93+
)
94+
95+
def _get_serializer_for_instance(self, instance):
96+
return self._get_serializer_for_type(
97+
type_value=getattr(instance, self.resource_type_field_name)
98+
)
99+
100+
def _get_serializer_for_data(self, data):
101+
try:
102+
return self._get_serializer_for_type(
103+
type_value=data[self.resource_type_field_name]
104+
)
105+
except KeyError:
106+
raise ValidationError(
107+
f"{self.resource_type_field_name} is a required field."
108+
)

api/fields.py

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import pytz
2+
3+
try:
4+
import zoneinfo
5+
except ImportError:
6+
from backports import zoneinfo
7+
8+
from django.utils.dateparse import parse_datetime
9+
from django.core.exceptions import ValidationError
10+
from rest_framework import serializers
11+
12+
from drf_extra_fields.geo_fields import PointField
13+
from timezone_field.utils import use_pytz_default
14+
from timezone_field.rest_framework import TimeZoneSerializerField
15+
16+
17+
class TimezoneAwareDateTimeField(serializers.DateTimeField):
18+
def to_internal_value(self, value):
19+
# Parse the datetime string into a datetime object
20+
parsed_dt = parse_datetime(value)
21+
22+
# Ensure that the datetime contains timezone information
23+
if parsed_dt.tzinfo is None:
24+
raise ValidationError("Datetime value must include timezone information.")
25+
26+
return super().to_internal_value(value=value)
27+
28+
29+
class ExpandedPointField(PointField):
30+
def __init__(self, *args, **kwargs):
31+
super().__init__(str_points=True, *args, **kwargs)
32+
33+
34+
class IntegerDefaultField(serializers.IntegerField):
35+
# When db value is None, use the default.
36+
def get_attribute(self, instance):
37+
attibute = super().get_attribute(instance)
38+
if attibute is None and self.default != serializers.empty:
39+
attibute = self.default
40+
return attibute
41+
42+
43+
class TimeZoneSerializerChoiceField(TimeZoneSerializerField, serializers.ChoiceField):
44+
def __init__(self, **kwargs):
45+
self.use_pytz = kwargs.pop("use_pytz", use_pytz_default())
46+
47+
_tzstrs = (
48+
pytz.common_timezones if self.use_pytz else zoneinfo.available_timezones()
49+
)
50+
51+
super().__init__(choices=_tzstrs, html_cutoff=5, **kwargs)
52+
53+
54+
class WritableSerializerMethodField(serializers.SerializerMethodField):
55+
# Reference: https://stackoverflow.com/a/64274128/8450576
56+
def __init__(self, **kwargs):
57+
self.setter_method_name = kwargs.pop("setter_method_name", None)
58+
self.deserializer_field = kwargs.pop("deserializer_field")
59+
60+
super().__init__(**kwargs)
61+
62+
self.read_only = False
63+
self.required = True
64+
65+
def bind(self, field_name, parent):
66+
retval = super().bind(field_name, parent)
67+
if not self.setter_method_name:
68+
self.setter_method_name = f"set_{field_name}"
69+
70+
return retval
71+
72+
def get_default(self):
73+
default = super().get_default()
74+
75+
return {self.field_name: default}
76+
77+
def to_internal_value(self, data):
78+
value = self.deserializer_field.to_internal_value(data)
79+
method = getattr(self.parent, self.setter_method_name)
80+
return {
81+
self.field_name: self.deserializer_field.to_internal_value(method(value))
82+
}

0 commit comments

Comments
 (0)