added auth, dicom crud

This commit is contained in:
Alexander Karpov 2022-10-26 23:22:04 +03:00
parent 8d9d0697da
commit 4f15a29b07
24 changed files with 239 additions and 142 deletions

View File

@ -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>

View File

@ -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",
),
]
),
),
]

View File

@ -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")

View File

@ -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

View File

@ -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

View File

View File

View File

View 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"]

View 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"

View 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

View 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)),
],
),
]

View 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

View 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

View File

@ -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"]

View File

@ -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

View File

@ -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

View File

@ -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.

View File

@ -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):

View 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)

View 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))

View File

@ -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()

View File

@ -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