diff --git a/.idea/misc.xml b/.idea/misc.xml
index 10af178..a970995 100644
--- a/.idea/misc.xml
+++ b/.idea/misc.xml
@@ -3,4 +3,5 @@
+
diff --git a/config/api_router.py b/config/api_router.py
index 1633bdb..ce8228d 100644
--- a/config/api_router.py
+++ b/config/api_router.py
@@ -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(
+ "",
+ RetrieveUpdateDeleteDicomApi.as_view(),
+ name="get_update_delete_dicom",
+ ),
+ ]
+ ),
+ ),
+]
diff --git a/config/settings/base.py b/config/settings/base.py
index 4c4d7c2..10904c6 100644
--- a/config/settings/base.py
+++ b/config/settings/base.py
@@ -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")
diff --git a/config/settings/local.py b/config/settings/local.py
index 559abc1..b4cd5a0 100644
--- a/config/settings/local.py
+++ b/config/settings/local.py
@@ -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
diff --git a/config/urls.py b/config/urls.py
index f4538df..150f240 100644
--- a/config/urls.py
+++ b/config/urls.py
@@ -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
diff --git a/image_markuper/dicom/__init__.py b/image_markuper/dicom/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/image_markuper/dicom/admin.py b/image_markuper/dicom/admin.py
new file mode 100644
index 0000000..e69de29
diff --git a/image_markuper/dicom/api/__init__.py b/image_markuper/dicom/api/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/image_markuper/dicom/api/serializers.py b/image_markuper/dicom/api/serializers.py
new file mode 100644
index 0000000..6164c58
--- /dev/null
+++ b/image_markuper/dicom/api/serializers.py
@@ -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"]
diff --git a/image_markuper/dicom/api/views.py b/image_markuper/dicom/api/views.py
new file mode 100644
index 0000000..9c248b1
--- /dev/null
+++ b/image_markuper/dicom/api/views.py
@@ -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"
diff --git a/image_markuper/dicom/apps.py b/image_markuper/dicom/apps.py
new file mode 100644
index 0000000..b2643f8
--- /dev/null
+++ b/image_markuper/dicom/apps.py
@@ -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
diff --git a/image_markuper/dicom/migrations/0001_initial.py b/image_markuper/dicom/migrations/0001_initial.py
new file mode 100644
index 0000000..5c9370b
--- /dev/null
+++ b/image_markuper/dicom/migrations/0001_initial.py
@@ -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)),
+ ],
+ ),
+ ]
diff --git a/image_markuper/dicom/migrations/__init__.py b/image_markuper/dicom/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/image_markuper/dicom/models.py b/image_markuper/dicom/models.py
new file mode 100644
index 0000000..315d5b3
--- /dev/null
+++ b/image_markuper/dicom/models.py
@@ -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
diff --git a/image_markuper/dicom/signals.py b/image_markuper/dicom/signals.py
new file mode 100644
index 0000000..2f138e9
--- /dev/null
+++ b/image_markuper/dicom/signals.py
@@ -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
diff --git a/image_markuper/users/admin.py b/image_markuper/users/admin.py
index 10e5a31..d2d6698 100644
--- a/image_markuper/users/admin.py
+++ b/image_markuper/users/admin.py
@@ -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"]
diff --git a/image_markuper/users/api/serializers.py b/image_markuper/users/api/serializers.py
index b5ccabb..e6ee9d6 100644
--- a/image_markuper/users/api/serializers.py
+++ b/image_markuper/users/api/serializers.py
@@ -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
diff --git a/image_markuper/users/api/views.py b/image_markuper/users/api/views.py
index 98bb04e..afa7569 100644
--- a/image_markuper/users/api/views.py
+++ b/image_markuper/users/api/views.py
@@ -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
diff --git a/image_markuper/users/models.py b/image_markuper/users/models.py
index 6898a0f..6557eb2 100644
--- a/image_markuper/users/models.py
+++ b/image_markuper/users/models.py
@@ -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.
diff --git a/image_markuper/users/views.py b/image_markuper/users/views.py
index baa04a0..cc31b1e 100644
--- a/image_markuper/users/views.py
+++ b/image_markuper/users/views.py
@@ -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):
diff --git a/image_markuper/utils/files.py b/image_markuper/utils/files.py
new file mode 100644
index 0000000..af79808
--- /dev/null
+++ b/image_markuper/utils/files.py
@@ -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)
diff --git a/image_markuper/utils/generators.py b/image_markuper/utils/generators.py
new file mode 100644
index 0000000..eeebfb8
--- /dev/null
+++ b/image_markuper/utils/generators.py
@@ -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))
diff --git a/merge_production_dotenvs_in_dotenv.py b/merge_production_dotenvs_in_dotenv.py
deleted file mode 100644
index d702a5f..0000000
--- a/merge_production_dotenvs_in_dotenv.py
+++ /dev/null
@@ -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()
diff --git a/requirements/base.txt b/requirements/base.txt
index ce9009a..531a40d 100644
--- a/requirements/base.txt
+++ b/requirements/base.txt
@@ -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