mirror of
https://github.com/leaders-of-digital-9-task/backend.git
synced 2024-11-21 17:06:33 +03:00
added auth, dicom crud
This commit is contained in:
parent
8d9d0697da
commit
4f15a29b07
|
@ -3,4 +3,5 @@
|
|||
<component name="JavaScriptSettings">
|
||||
<option name="languageLevel" value="ES6" />
|
||||
</component>
|
||||
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.10 (lct_backend)" project-jdk-type="Python SDK" />
|
||||
</project>
|
||||
|
|
|
@ -1,15 +1,30 @@
|
|||
from django.conf import settings
|
||||
from rest_framework.routers import DefaultRouter, SimpleRouter
|
||||
from dicom.api.views import ListCreateDicomApi, RetrieveUpdateDeleteDicomApi
|
||||
from django.urls import include, path
|
||||
from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView
|
||||
from users.api.views import RegisterView
|
||||
|
||||
from image_markuper.users.api.views import UserViewSet
|
||||
|
||||
if settings.DEBUG:
|
||||
router = DefaultRouter()
|
||||
else:
|
||||
router = SimpleRouter()
|
||||
|
||||
router.register("users", UserViewSet)
|
||||
|
||||
|
||||
app_name = "api"
|
||||
urlpatterns = router.urls
|
||||
urlpatterns = [
|
||||
path(
|
||||
"auth/",
|
||||
include(
|
||||
[
|
||||
path("token/", TokenObtainPairView.as_view(), name="token_obtain_pair"),
|
||||
path("refresh/", TokenRefreshView.as_view(), name="token_refresh"),
|
||||
path("register/", RegisterView.as_view(), name="user_register"),
|
||||
]
|
||||
),
|
||||
),
|
||||
path(
|
||||
"dicom/",
|
||||
include(
|
||||
[
|
||||
path("", ListCreateDicomApi.as_view(), name="list_create_dicom"),
|
||||
path(
|
||||
"<str:slug>",
|
||||
RetrieveUpdateDeleteDicomApi.as_view(),
|
||||
name="get_update_delete_dicom",
|
||||
),
|
||||
]
|
||||
),
|
||||
),
|
||||
]
|
||||
|
|
|
@ -10,7 +10,7 @@ ROOT_DIR = Path(__file__).resolve(strict=True).parent.parent.parent
|
|||
APPS_DIR = ROOT_DIR / "image_markuper"
|
||||
env = environ.Env()
|
||||
|
||||
READ_DOT_ENV_FILE = env.bool("DJANGO_READ_DOT_ENV_FILE", default=False)
|
||||
READ_DOT_ENV_FILE = env.bool("DJANGO_READ_DOT_ENV_FILE", default=True)
|
||||
if READ_DOT_ENV_FILE:
|
||||
# OS environment variables take precedence over variables from .env
|
||||
env.read_env(str(ROOT_DIR / ".env"))
|
||||
|
@ -80,6 +80,7 @@ THIRD_PARTY_APPS = [
|
|||
|
||||
LOCAL_APPS = [
|
||||
"image_markuper.users",
|
||||
"image_markuper.dicom"
|
||||
# Your stuff: custom apps go here
|
||||
]
|
||||
# https://docs.djangoproject.com/en/dev/ref/settings/#installed-apps
|
||||
|
@ -302,8 +303,7 @@ SOCIALACCOUNT_FORMS = {"signup": "image_markuper.users.forms.UserSocialSignupFor
|
|||
# django-rest-framework - https://www.django-rest-framework.org/api-guide/settings/
|
||||
REST_FRAMEWORK = {
|
||||
"DEFAULT_AUTHENTICATION_CLASSES": (
|
||||
"rest_framework.authentication.SessionAuthentication",
|
||||
"rest_framework.authentication.TokenAuthentication",
|
||||
"rest_framework_simplejwt.authentication.JWTAuthentication",
|
||||
),
|
||||
"DEFAULT_PERMISSION_CLASSES": ("rest_framework.permissions.IsAuthenticated",),
|
||||
"DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema",
|
||||
|
@ -311,18 +311,19 @@ REST_FRAMEWORK = {
|
|||
|
||||
# django-cors-headers - https://github.com/adamchainz/django-cors-headers#setup
|
||||
CORS_URLS_REGEX = r"^/api/.*$"
|
||||
|
||||
# By Default swagger ui is available only to admin user(s). You can change permission classes to change that
|
||||
# See more configuration options at https://drf-spectacular.readthedocs.io/en/latest/settings.html#settings
|
||||
SPECTACULAR_SETTINGS = {
|
||||
"TITLE": "Image markuper API",
|
||||
"DESCRIPTION": "Documentation of API endpoints of Image markuper",
|
||||
"VERSION": "1.0.0",
|
||||
"SERVE_PERMISSIONS": ["rest_framework.permissions.IsAdminUser"],
|
||||
"SCHEMA_PATH_PREFIX": r"/api/",
|
||||
"SERVE_INCLUDE_SCHEMA": False,
|
||||
"SERVE_PERMISSIONS": ["rest_framework.permissions.AllowAny"],
|
||||
"SERVERS": [
|
||||
{"url": "http://127.0.0.1:8000", "description": "Local Development server"},
|
||||
{"url": "https://akarpov.ru", "description": "Production server"},
|
||||
{"url": "https://dev.akarpov.ru", "description": "Development server"},
|
||||
{"url": "https//127.0.0.1:8000", "description": "Development server"},
|
||||
],
|
||||
}
|
||||
# Your stuff...
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
from datetime import timedelta
|
||||
|
||||
from .base import * # noqa
|
||||
from .base import env
|
||||
|
||||
|
@ -11,7 +13,7 @@ SECRET_KEY = env(
|
|||
default="XgO9NMQfMY5CNeVNh98WrKRXiQZnvtPzHJrF9ROPhAFLVEG1FvDD2ZRKTdJKVu8p",
|
||||
)
|
||||
# https://docs.djangoproject.com/en/dev/ref/settings/#allowed-hosts
|
||||
ALLOWED_HOSTS = ["localhost", "0.0.0.0", "127.0.0.1"]
|
||||
ALLOWED_HOSTS = ["localhost", "0.0.0.0", "127.0.0.1", "dev.akarpov.ru"]
|
||||
|
||||
# CACHES
|
||||
# ------------------------------------------------------------------------------
|
||||
|
@ -64,5 +66,9 @@ INSTALLED_APPS += ["django_extensions"] # noqa F405
|
|||
|
||||
# https://docs.celeryq.dev/en/stable/userguide/configuration.html#task-eager-propagates
|
||||
CELERY_TASK_EAGER_PROPAGATES = True
|
||||
# Your stuff...
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
SIMPLE_JWT = {
|
||||
"ACCESS_TOKEN_LIFETIME": timedelta(minutes=10000),
|
||||
}
|
||||
|
||||
CORS_ALLOW_ALL_ORIGINS = True
|
||||
|
|
|
@ -2,21 +2,14 @@ from django.conf import settings
|
|||
from django.conf.urls.static import static
|
||||
from django.contrib import admin
|
||||
from django.urls import include, path
|
||||
from django.views import defaults as default_views
|
||||
from django.views.generic import TemplateView
|
||||
from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView
|
||||
from rest_framework.authtoken.views import obtain_auth_token
|
||||
from drf_spectacular.views import (
|
||||
SpectacularAPIView,
|
||||
SpectacularRedocView,
|
||||
SpectacularSwaggerView,
|
||||
)
|
||||
|
||||
urlpatterns = [
|
||||
path("", TemplateView.as_view(template_name="pages/home.html"), name="home"),
|
||||
path(
|
||||
"about/", TemplateView.as_view(template_name="pages/about.html"), name="about"
|
||||
),
|
||||
# Django Admin, use {% url 'admin:index' %}
|
||||
path(settings.ADMIN_URL, admin.site.urls),
|
||||
# User management
|
||||
path("users/", include("image_markuper.users.urls", namespace="users")),
|
||||
path("accounts/", include("allauth.urls")),
|
||||
path(settings.ADMIN_URL, admin.site.urls)
|
||||
# Your stuff: custom urls includes go here
|
||||
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
||||
|
||||
|
@ -24,37 +17,23 @@ urlpatterns = [
|
|||
urlpatterns += [
|
||||
# API base url
|
||||
path("api/", include("config.api_router")),
|
||||
# DRF auth token
|
||||
path("auth-token/", obtain_auth_token),
|
||||
path("api/schema/", SpectacularAPIView.as_view(), name="api-schema"),
|
||||
path("api_schema/", SpectacularAPIView.as_view(), name="api-schema"),
|
||||
path("api_rschema/", SpectacularAPIView.as_view(), name="api-redoc-schema"),
|
||||
path(
|
||||
"api/docs/",
|
||||
SpectacularSwaggerView.as_view(url_name="api-schema"),
|
||||
name="api-docs",
|
||||
name="home",
|
||||
),
|
||||
path(
|
||||
"api/redoc/",
|
||||
SpectacularRedocView.as_view(url_name="api-redoc-schema"),
|
||||
name="home",
|
||||
),
|
||||
]
|
||||
|
||||
if settings.DEBUG:
|
||||
# This allows the error pages to be debugged during development, just visit
|
||||
# these url in browser to see how these error pages look like.
|
||||
urlpatterns += [
|
||||
path(
|
||||
"400/",
|
||||
default_views.bad_request,
|
||||
kwargs={"exception": Exception("Bad Request!")},
|
||||
),
|
||||
path(
|
||||
"403/",
|
||||
default_views.permission_denied,
|
||||
kwargs={"exception": Exception("Permission Denied")},
|
||||
),
|
||||
path(
|
||||
"404/",
|
||||
default_views.page_not_found,
|
||||
kwargs={"exception": Exception("Page not Found")},
|
||||
),
|
||||
path("500/", default_views.server_error),
|
||||
]
|
||||
if "debug_toolbar" in settings.INSTALLED_APPS:
|
||||
import debug_toolbar
|
||||
|
||||
|
|
0
image_markuper/dicom/__init__.py
Normal file
0
image_markuper/dicom/__init__.py
Normal file
0
image_markuper/dicom/admin.py
Normal file
0
image_markuper/dicom/admin.py
Normal file
0
image_markuper/dicom/api/__init__.py
Normal file
0
image_markuper/dicom/api/__init__.py
Normal file
24
image_markuper/dicom/api/serializers.py
Normal file
24
image_markuper/dicom/api/serializers.py
Normal file
|
@ -0,0 +1,24 @@
|
|||
from dicom.models import Dicom
|
||||
from rest_framework import serializers
|
||||
|
||||
|
||||
class ListDicomSerializer(serializers.ModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(
|
||||
view_name="get_update_delete_dicom", lookup_field="slug"
|
||||
)
|
||||
file = serializers.FileField()
|
||||
|
||||
class Meta:
|
||||
model = Dicom
|
||||
fields = ["file", "uploaded", "url"]
|
||||
|
||||
def create(self, validated_data):
|
||||
return Dicom.objects.create(**validated_data, user=self.context["request"].user)
|
||||
|
||||
|
||||
class DicomSerializer(serializers.ModelSerializer):
|
||||
file = serializers.FileField()
|
||||
|
||||
class Meta:
|
||||
model = Dicom
|
||||
fields = ["file", "uploaded"]
|
36
image_markuper/dicom/api/views.py
Normal file
36
image_markuper/dicom/api/views.py
Normal file
|
@ -0,0 +1,36 @@
|
|||
from drf_spectacular.utils import extend_schema
|
||||
from rest_framework import generics
|
||||
from rest_framework.parsers import FormParser, MultiPartParser
|
||||
|
||||
from ..models import Dicom
|
||||
from .serializers import DicomSerializer, ListDicomSerializer
|
||||
|
||||
|
||||
class ListCreateDicomApi(generics.ListCreateAPIView):
|
||||
serializer_class = ListDicomSerializer
|
||||
parser_classes = [MultiPartParser, FormParser]
|
||||
|
||||
def get_queryset(self):
|
||||
return Dicom.objects.filter(user=self.request.user)
|
||||
|
||||
@extend_schema(
|
||||
operation_id="upload_file",
|
||||
request={
|
||||
"multipart/form-data": {
|
||||
"type": "object",
|
||||
"properties": {"file": {"type": "string", "format": "binary"}},
|
||||
}
|
||||
},
|
||||
)
|
||||
def post(self, request, *args, **kwargs):
|
||||
return self.create(request, *args, **kwargs)
|
||||
|
||||
|
||||
class RetrieveUpdateDeleteDicomApi(generics.RetrieveUpdateDestroyAPIView):
|
||||
def get_queryset(self):
|
||||
return Dicom.objects.filter(user=self.request.user)
|
||||
|
||||
serializer_class = DicomSerializer
|
||||
parser_classes = [MultiPartParser, FormParser]
|
||||
|
||||
lookup_field = "slug"
|
12
image_markuper/dicom/apps.py
Normal file
12
image_markuper/dicom/apps.py
Normal file
|
@ -0,0 +1,12 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class DicomConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "dicom"
|
||||
|
||||
def ready(self):
|
||||
try:
|
||||
import image_markuper.dicom.signals # noqa F401
|
||||
except ImportError:
|
||||
pass
|
28
image_markuper/dicom/migrations/0001_initial.py
Normal file
28
image_markuper/dicom/migrations/0001_initial.py
Normal file
|
@ -0,0 +1,28 @@
|
|||
# Generated by Django 4.0.8 on 2022-10-26 15:01
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import utils.files
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Dicom',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('slug', models.SlugField()),
|
||||
('file', models.FileField(upload_to=utils.files.media_upload_path)),
|
||||
('uploaded', models.DateTimeField(auto_now_add=True)),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='files', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
]
|
0
image_markuper/dicom/migrations/__init__.py
Normal file
0
image_markuper/dicom/migrations/__init__.py
Normal file
16
image_markuper/dicom/models.py
Normal file
16
image_markuper/dicom/models.py
Normal file
|
@ -0,0 +1,16 @@
|
|||
from django.contrib.auth import get_user_model
|
||||
from django.db import models
|
||||
from utils.files import media_upload_path
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
class Dicom(models.Model):
|
||||
user = models.ForeignKey(User, related_name="files", on_delete=models.CASCADE)
|
||||
slug = models.SlugField()
|
||||
|
||||
file = models.FileField(upload_to=media_upload_path)
|
||||
uploaded = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
def __str__(self):
|
||||
return self.file.name
|
12
image_markuper/dicom/signals.py
Normal file
12
image_markuper/dicom/signals.py
Normal file
|
@ -0,0 +1,12 @@
|
|||
from dicom.models import Dicom
|
||||
from django.db.models.signals import pre_save
|
||||
from django.dispatch import receiver
|
||||
from utils.generators import generate_charset
|
||||
|
||||
|
||||
@receiver(pre_save, sender=Dicom)
|
||||
def create_dicom(sender, instance: Dicom, **kwargs):
|
||||
slug = generate_charset(5)
|
||||
while Dicom.objects.filter(slug=slug):
|
||||
slug = generate_charset(5)
|
||||
instance.slug = slug
|
|
@ -15,7 +15,6 @@ class UserAdmin(auth_admin.UserAdmin):
|
|||
add_form = UserAdminCreationForm
|
||||
fieldsets = (
|
||||
(None, {"fields": ("username", "password")}),
|
||||
(_("Personal info"), {"fields": ("name", "email")}),
|
||||
(
|
||||
_("Permissions"),
|
||||
{
|
||||
|
@ -30,5 +29,5 @@ class UserAdmin(auth_admin.UserAdmin):
|
|||
),
|
||||
(_("Important dates"), {"fields": ("last_login", "date_joined")}),
|
||||
)
|
||||
list_display = ["username", "name", "is_superuser"]
|
||||
search_fields = ["name"]
|
||||
list_display = ["username", "is_superuser"]
|
||||
search_fields = ["username"]
|
||||
|
|
|
@ -7,8 +7,28 @@ User = get_user_model()
|
|||
class UserSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ["username", "name", "url"]
|
||||
fields = ["username", "url"]
|
||||
|
||||
extra_kwargs = {
|
||||
"url": {"view_name": "api:user-detail", "lookup_field": "username"}
|
||||
}
|
||||
|
||||
|
||||
class RegisterSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = User
|
||||
fields = (
|
||||
"username",
|
||||
"password",
|
||||
)
|
||||
extra_kwargs = {"password": {"write_only": True}}
|
||||
|
||||
def create(self, validated_data):
|
||||
user = User.objects.create(
|
||||
username=validated_data["username"],
|
||||
)
|
||||
|
||||
user.set_password(validated_data["password"])
|
||||
user.save()
|
||||
|
||||
return user
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
from django.contrib.auth import get_user_model
|
||||
from rest_framework import status
|
||||
from rest_framework import generics, status
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.mixins import ListModelMixin, RetrieveModelMixin, UpdateModelMixin
|
||||
from rest_framework.permissions import AllowAny
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.viewsets import GenericViewSet
|
||||
|
||||
from .serializers import UserSerializer
|
||||
from .serializers import RegisterSerializer, UserSerializer
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
@ -16,10 +17,15 @@ class UserViewSet(RetrieveModelMixin, ListModelMixin, UpdateModelMixin, GenericV
|
|||
lookup_field = "username"
|
||||
|
||||
def get_queryset(self, *args, **kwargs):
|
||||
assert isinstance(self.request.user.id, int)
|
||||
return self.queryset.filter(id=self.request.user.id)
|
||||
|
||||
@action(detail=False)
|
||||
def me(self, request):
|
||||
serializer = UserSerializer(request.user, context={"request": request})
|
||||
return Response(status=status.HTTP_200_OK, data=serializer.data)
|
||||
|
||||
|
||||
class RegisterView(generics.CreateAPIView):
|
||||
queryset = User.objects.all()
|
||||
permission_classes = (AllowAny,)
|
||||
serializer_class = RegisterSerializer
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
from django.contrib.auth.models import AbstractUser
|
||||
from django.db.models import CharField
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
class User(AbstractUser):
|
||||
|
@ -11,10 +9,9 @@ class User(AbstractUser):
|
|||
check forms.SignupForm and forms.SocialSignupForms accordingly.
|
||||
"""
|
||||
|
||||
#: First and last name do not cover name patterns around the globe
|
||||
name = CharField(_("Name of User"), blank=True, max_length=255)
|
||||
first_name = None # type: ignore
|
||||
last_name = None # type: ignore
|
||||
email = None # type: ignore
|
||||
|
||||
def get_absolute_url(self):
|
||||
"""Get url for user's detail view.
|
||||
|
|
|
@ -25,9 +25,6 @@ class UserUpdateView(LoginRequiredMixin, SuccessMessageMixin, UpdateView):
|
|||
success_message = _("Information successfully updated")
|
||||
|
||||
def get_success_url(self):
|
||||
assert (
|
||||
self.request.user.is_authenticated
|
||||
) # for mypy to know that the user is authenticated
|
||||
return self.request.user.get_absolute_url()
|
||||
|
||||
def get_object(self):
|
||||
|
|
7
image_markuper/utils/files.py
Normal file
7
image_markuper/utils/files.py
Normal file
|
@ -0,0 +1,7 @@
|
|||
import os
|
||||
|
||||
from utils.generators import generate_charset
|
||||
|
||||
|
||||
def media_upload_path(instance, filename):
|
||||
return os.path.join(f"uploads/dicom/{generate_charset(7)}/", filename)
|
7
image_markuper/utils/generators.py
Normal file
7
image_markuper/utils/generators.py
Normal file
|
@ -0,0 +1,7 @@
|
|||
import random
|
||||
import string
|
||||
|
||||
|
||||
def generate_charset(length: int) -> str:
|
||||
"""Generate a random string of characters of a given length."""
|
||||
return "".join(random.choice(string.ascii_letters) for _ in range(length))
|
|
@ -1,67 +0,0 @@
|
|||
import os
|
||||
from collections.abc import Sequence
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT_DIR_PATH = Path(__file__).parent.resolve()
|
||||
PRODUCTION_DOTENVS_DIR_PATH = ROOT_DIR_PATH / ".envs" / ".production"
|
||||
PRODUCTION_DOTENV_FILE_PATHS = [
|
||||
PRODUCTION_DOTENVS_DIR_PATH / ".django",
|
||||
PRODUCTION_DOTENVS_DIR_PATH / ".postgres",
|
||||
]
|
||||
DOTENV_FILE_PATH = ROOT_DIR_PATH / ".env"
|
||||
|
||||
|
||||
def merge(
|
||||
output_file_path: str, merged_file_paths: Sequence[str], append_linesep: bool = True
|
||||
) -> None:
|
||||
with open(output_file_path, "w") as output_file:
|
||||
for merged_file_path in merged_file_paths:
|
||||
with open(merged_file_path) as merged_file:
|
||||
merged_file_content = merged_file.read()
|
||||
output_file.write(merged_file_content)
|
||||
if append_linesep:
|
||||
output_file.write(os.linesep)
|
||||
|
||||
|
||||
def main():
|
||||
merge(DOTENV_FILE_PATH, PRODUCTION_DOTENV_FILE_PATHS)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("merged_file_count", range(3))
|
||||
@pytest.mark.parametrize("append_linesep", [True, False])
|
||||
def test_merge(tmpdir_factory, merged_file_count: int, append_linesep: bool):
|
||||
tmp_dir_path = Path(str(tmpdir_factory.getbasetemp()))
|
||||
|
||||
output_file_path = tmp_dir_path / ".env"
|
||||
|
||||
expected_output_file_content = ""
|
||||
merged_file_paths = []
|
||||
for i in range(merged_file_count):
|
||||
merged_file_ord = i + 1
|
||||
|
||||
merged_filename = f".service{merged_file_ord}"
|
||||
merged_file_path = tmp_dir_path / merged_filename
|
||||
|
||||
merged_file_content = merged_filename * merged_file_ord
|
||||
|
||||
with open(merged_file_path, "w+") as file:
|
||||
file.write(merged_file_content)
|
||||
|
||||
expected_output_file_content += merged_file_content
|
||||
if append_linesep:
|
||||
expected_output_file_content += os.linesep
|
||||
|
||||
merged_file_paths.append(merged_file_path)
|
||||
|
||||
merge(output_file_path, merged_file_paths, append_linesep)
|
||||
|
||||
with open(output_file_path) as output_file:
|
||||
actual_output_file_content = output_file.read()
|
||||
|
||||
assert actual_output_file_content == expected_output_file_content
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
|
@ -21,5 +21,6 @@ django-redis==5.2.0 # https://github.com/jazzband/django-redis
|
|||
# Django REST Framework
|
||||
djangorestframework==3.14.0 # https://github.com/encode/django-rest-framework
|
||||
django-cors-headers==3.13.0 # https://github.com/adamchainz/django-cors-headers
|
||||
djangorestframework-simplejwt==5.2.2
|
||||
# DRF-spectacular for api documentation
|
||||
drf-spectacular==0.24.2 # https://github.com/tfranzel/drf-spectacular
|
||||
|
|
Loading…
Reference in New Issue
Block a user