Skip to content

Commit

Permalink
Add redoc api (#12)
Browse files Browse the repository at this point in the history
* add drf yasg 1.21.7 in requirements

* add gap_api app and redoc documentation

* fix typing in model factories

* fix lint

* Refactore code

* Clean schema

---------

Co-authored-by: Irwan Fathurrahman <[email protected]>
  • Loading branch information
danangmassandy and meomancer authored Jun 27, 2024
1 parent 80cb1b0 commit 1dea7d0
Show file tree
Hide file tree
Showing 25 changed files with 390 additions and 12 deletions.
2 changes: 2 additions & 0 deletions deployment/docker/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,5 @@ django-celery-beat==2.5.0
redis==4.3.4

psycopg2-binary==2.9.9
# drf yasg
drf-yasg==1.21.7
49 changes: 49 additions & 0 deletions django_project/core/factories.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# coding=utf-8
"""
Tomorrow Now GAP.
.. note:: Core factories.
"""

from typing import Generic, TypeVar

import factory
from django.contrib.auth import get_user_model

T = TypeVar('T')
User = get_user_model()


class BaseMetaFactory(Generic[T], factory.base.FactoryMetaClass):
"""Base meta factory class."""

def __call__(cls, *args, **kwargs) -> T:
"""Override the default Factory() syntax to call the default strategy.
Returns an instance of the associated class.
"""
return super().__call__(*args, **kwargs)


class BaseFactory(Generic[T], factory.django.DjangoModelFactory):
"""Base factory class to make the factory return correct class typing."""

@classmethod
def create(cls, **kwargs) -> T:
"""Create an instance of the model, and save it to the database."""
return super().create(**kwargs)


class UserF(
BaseFactory[User], metaclass=BaseMetaFactory[User]
):
"""Factory class for User."""

class Meta: # noqa
model = User

username = factory.Sequence(
lambda n: u'username %s' % n
)
first_name = 'John'
last_name = 'Doe'
File renamed without changes.
1 change: 1 addition & 0 deletions django_project/core/settings/contrib.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
'django_cleanup.apps.CleanupConfig',
'django_celery_beat',
'django_celery_results',
'drf_yasg',
)
WEBPACK_LOADER = {
'DEFAULT': {
Expand Down
3 changes: 2 additions & 1 deletion django_project/core/settings/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@
INSTALLED_APPS = INSTALLED_APPS + (
'core',
'frontend',
'gap'
'gap',
'gap_api'
)

TEMPLATES[0]['DIRS'] += [
Expand Down
Empty file.
32 changes: 32 additions & 0 deletions django_project/core/tests/common.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# coding=utf-8
"""
Tomorrow Now GAP.
.. note:: Common class for unit tests.
"""

from django.test import TestCase
from rest_framework.test import APIRequestFactory
from core.factories import UserF


class BaseAPIViewTest(TestCase):
"""Base class for API test."""

def setUp(self):
"""Init test class."""
self.factory = APIRequestFactory()
self.superuser = UserF.create(
is_staff=True,
is_superuser=True,
is_active=True
)
self.user_1 = UserF.create(
is_active=True
)


class FakeResolverMatchV1:
"""Fake class to mock versioning."""

namespace = 'v1'
7 changes: 5 additions & 2 deletions django_project/core/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,14 @@
from django.conf import settings
from django.conf.urls.static import static
from django.contrib import admin
from django.urls import path, include
from django.urls import path, include, re_path

urlpatterns = [
path('', include('frontend.urls')),
re_path(
r'^api/', include(('gap_api.urls', 'api'), namespace='api')
),
path('admin/', admin.site.urls),
path('', include('frontend.urls')),
]

if settings.DEBUG:
Expand Down
29 changes: 21 additions & 8 deletions django_project/gap/factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
.. note:: Factory classes for Models
"""
import factory
from factory.django import DjangoModelFactory
from django.contrib.gis.geos import Point, MultiPolygon, Polygon

from core.factories import BaseMetaFactory, BaseFactory
from gap.models import (
Provider,
Attribute,
Expand All @@ -14,10 +16,11 @@
Measurement,
ObservationType
)
from django.contrib.gis.geos import Point, MultiPolygon, Polygon


class ProviderFactory(DjangoModelFactory):
class ProviderFactory(
BaseFactory[Provider], metaclass=BaseMetaFactory[Provider]
):
"""Factory class for Provider model."""

class Meta: # noqa
Expand All @@ -27,7 +30,9 @@ class Meta: # noqa
description = factory.Faker('text')


class AttributeFactory(DjangoModelFactory):
class AttributeFactory(
BaseFactory[Attribute], metaclass=BaseMetaFactory[Attribute]
):
"""Factory class for Attribute model."""

class Meta: # noqa
Expand All @@ -39,7 +44,9 @@ class Meta: # noqa
description = factory.Faker('text')


class ObservationTypeFactory(DjangoModelFactory):
class ObservationTypeFactory(
BaseFactory[ObservationType], metaclass=BaseMetaFactory[ObservationType]
):
"""Factory class for ObservationType model."""

class Meta: # noqa
Expand All @@ -51,7 +58,9 @@ class Meta: # noqa
description = factory.Faker('text')


class CountryFactory(DjangoModelFactory):
class CountryFactory(
BaseFactory[Country], metaclass=BaseMetaFactory[Country]
):
"""Factory class for Country model."""

class Meta: # noqa
Expand All @@ -67,7 +76,9 @@ class Meta: # noqa
description = factory.Faker('text')


class StationFactory(DjangoModelFactory):
class StationFactory(
BaseFactory[Station], metaclass=BaseMetaFactory[Station]
):
"""Factory class for Station model."""

class Meta: # noqa
Expand All @@ -83,7 +94,9 @@ class Meta: # noqa
observation_type = factory.SubFactory(ObservationTypeFactory)


class MeasurementFactory(DjangoModelFactory):
class MeasurementFactory(
BaseFactory[Measurement], metaclass=BaseMetaFactory[Measurement]
):
"""Factory class for Measurement model."""

class Meta: # noqa
Expand Down
2 changes: 1 addition & 1 deletion django_project/gap/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

from django.contrib.gis.db import models

from core.models.general import Definition
from core.models.common import Definition


class Provider(Definition):
Expand Down
Empty file.
Empty file.
38 changes: 38 additions & 0 deletions django_project/gap_api/api_views/user.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# coding=utf-8
"""
Tomorrow Now GAP.
.. note:: User APIs
"""

from drf_yasg.utils import swagger_auto_schema
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView

from gap_api.serializers.common import APIErrorSerializer
from gap_api.serializers.user import UserInfoSerializer
from gap_api.utils.helper import ApiTag


class UserInfo(APIView):
"""API to return user info."""

permission_classes = [IsAuthenticated]

@swagger_auto_schema(
operation_id='user-info',
tags=[ApiTag.USER],
responses={
200: UserInfoSerializer,
400: APIErrorSerializer
}
)
def get(self, request, *args, **kwargs):
"""Login user info.
Return current login user information.
"""
return Response(
status=200, data=UserInfoSerializer(request.user).data
)
15 changes: 15 additions & 0 deletions django_project/gap_api/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# coding=utf-8
"""
Tomorrow Now GAP.
.. note:: App Config for GAP API
"""

from django.apps import AppConfig


class GapApiConfig(AppConfig):
"""App Config for GAP API."""

default_auto_field = 'django.db.models.BigAutoField'
name = 'gap_api'
Empty file.
Empty file.
20 changes: 20 additions & 0 deletions django_project/gap_api/serializers/common.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# coding=utf-8
"""
Tomorrow Now GAP.
.. note:: Common serializer class.
"""

from rest_framework import serializers


class APIErrorSerializer(serializers.Serializer):
"""Serializer for error in the API."""

detail = serializers.CharField()


class NoContentSerializer(serializers.Serializer):
"""Empty serializer for API that returns 204 No Content."""

pass
28 changes: 28 additions & 0 deletions django_project/gap_api/serializers/user.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# coding=utf-8
"""
Tomorrow Now GAP.
.. note:: User serializer class.
"""

from django.contrib.auth import get_user_model
from rest_framework import serializers

User = get_user_model()


class UserInfoSerializer(serializers.ModelSerializer):
"""Serializer for User Info."""

class Meta: # noqa
model = User
fields = ['username', 'email', 'first_name', 'last_name']
swagger_schema_fields = {
'title': 'User Info',
'example': {
'username': '[email protected]',
'email': '[email protected]',
'first_name': 'Jane',
'last_name': 'Doe'
}
}
Empty file.
41 changes: 41 additions & 0 deletions django_project/gap_api/tests/test_user_api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# coding=utf-8
"""
Tomorrow Now GAP.
.. note:: Unit tests for User API.
"""

from django.urls import reverse

from core.tests.common import FakeResolverMatchV1, BaseAPIViewTest
from gap_api.api_views.user import UserInfo


class UserInfoAPITest(BaseAPIViewTest):
"""User info api test case."""

def test_get_user_info_without_auth(self):
"""Test get user info without authentication."""
view = UserInfo.as_view()
request = self.factory.get(
reverse('api:v1:user-info')
)
request.resolver_match = FakeResolverMatchV1
response = view(request)
self.assertEqual(response.status_code, 401)

def test_get_user_info(self):
"""Test get user info with superuser."""
view = UserInfo.as_view()
request = self.factory.get(
reverse('api:v1:user-info')
)
request.user = self.superuser
request.resolver_match = FakeResolverMatchV1
response = view(request)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data['username'], self.superuser.username)
self.assertEqual(
response.data['first_name'], self.superuser.first_name
)
self.assertEqual(response.data['last_name'], self.superuser.last_name)
14 changes: 14 additions & 0 deletions django_project/gap_api/urls/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# coding=utf-8
"""
Tomorrow Now GAP.
.. note:: GAP API urls.
"""

from django.urls import include, re_path

urlpatterns = [
re_path(
r'^v1/', include(('gap_api.urls.v1', 'v1'), namespace='v1')
)
]
Loading

0 comments on commit 1dea7d0

Please sign in to comment.