reverted to old web configuration on main branch

This commit is contained in:
2025-10-06 10:56:21 +02:00
parent 696d0e61f1
commit ed20c841ab
515 changed files with 353022 additions and 7422 deletions

165
.gitignore copy Normal file
View File

@@ -0,0 +1,165 @@
#
*.mp4
backups/
collectedstaticfiles/
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
.pdm.toml
.pdm-python
.pdm-build/
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/

View File

@@ -0,0 +1,23 @@
# vontor-cz
## venv
- windows
```
Set-ExecutionPolicy -Scope Process -ExecutionPolicy RemoteSigned
python -m venv venv
.\venv\Scripts\Activate
#start server
daphne -b localhost -p 8000 vontor_cz.asgi:application
```
## docker compose
spuštění dockeru pro lokální hosting, s instantníma změnami během editace ve vscodu.
```docker-compose up --build```
## dns reset windows
```ipconfig /flushdns```

37
X-Notes/admin(example).py Normal file
View File

@@ -0,0 +1,37 @@
from django.contrib import admin
from .models import MyModel # Importuj modely
# Příklad přizpůsobení zobrazení modelu v administrátorské sekci
class MyModelAdmin(admin.ModelAdmin):
# Určují se pole, která se zobrazí v seznamu (list view)
list_display = ('field1', 'field2', 'field3')
# Určuje, podle kterých polí lze vyhledávat
search_fields = ('field1', 'field2')
# Aktivuje filtrování podle hodnoty pole v pravém postranním panelu
list_filter = ('field1', 'field2')
# Určuje pole, která se zobrazí ve formuláři při detailním pohledu na model
fields = ('field1', 'field2', 'field3')
# Definuje rozložení polí ve formuláři
fieldsets = (
(None, {
'fields': ('field1', 'field2'),
}),
('Další informace', {
'classes': ('collapse',),
'fields': ('field3',),
}),
)
# Nastavuje výchozí řazení záznamů při jejich zobrazení
ordering = ('field1',)
# Určuje počet záznamů zobrazených na jedné stránce
list_per_page = 10
# Definuje akce dostupné pro vybrané objekty
actions = ['custom_action']
# Příklad vlastní akce
def custom_action(self, request, queryset):
# Vlastní logika pro akci
queryset.update(field1='Updated Value')
# Registrování modelu s vlastními nastaveními administrátorského rozhraní
admin.site.register(MyModel, MyModelAdmin)

View File

@@ -1,6 +1,6 @@
from django.apps import AppConfig
class GopayConfig(AppConfig):
class ApiConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'gopay'
name = 'api'

14
api/models.py Normal file
View File

@@ -0,0 +1,14 @@
from django.db import models
# Create your models here.
from django.contrib.auth.models import User
class UserProfile(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE)
# Add custom fields here
bio = models.TextField(blank=True)
birthdate = models.DateField(null=True, blank=True)
profile_picture = models.ImageField(upload_to='profile_pics/', null=True, blank=True)
def __str__(self):
return f'{self.user.username} Profile'

7
api/permissions.py Normal file
View File

@@ -0,0 +1,7 @@
from rest_framework_api_key.permissions import HasAPIKey
class UserEditAPIKeyPermissions(HasAPIKey):
"""
Custom permision for restricting access using API key.
"""
pass

21
api/serializers.py Normal file
View File

@@ -0,0 +1,21 @@
from rest_framework import serializers
from .models import User
#Serializers are for what views can show fields of models
class PublicUserSerializers(serializers.ModelSerializer):
"""
Serializer for public User fields
"""
class Meta:
model = User
fields = ['id', 'username']
class SecureUserSerializers(serializers.ModelSerializer):
"""
Serializer for all User fields
Requires API key
"""
class Meta:
model = User
fields = ['id', 'username', 'email', 'first_name', 'last_name', 'bio']

10
api/urls.py Normal file
View File

@@ -0,0 +1,10 @@
from django.urls import path
from .views import PublicUserView, SecureUserUpdateView
urlpatterns = [
# URL for the public view to list users with public fields
path('users/', PublicUserView.as_view(), name='public-user-list'),
# URL for secure view to retrieve and update user with all fields
path('users/<int:pk>/', SecureUserUpdateView.as_view(), name='secure-user-update'),
]

20
api/views.py Normal file
View File

@@ -0,0 +1,20 @@
from django.shortcuts import render
# Create your views here.
from rest_framework import generics, permissions
from .models import User
from .serializers import PublicUserSerializers, SecureUserSerializers
from .permissions import UserEditAPIKeyPermissions
#Public view: List users with only public fields
class PublicUserView(generics.ListAPIView):
queryset = User.objects.all()
serializer_class = PublicUserSerializers
permission_classes = [permissions.AllowAny]
#Secure view for retrive/update user all fields (API key)
class SecureUserUpdateView(generics.RetrieveUpdateAPIView):
queryset = User.objects.all()
serializer_class = SecureUserSerializers
permission_classes = [UserEditAPIKeyPermissions]

View File

@@ -1,10 +0,0 @@
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8000

View File

@@ -1,23 +0,0 @@
from django.contrib import admin
from django.contrib.auth import get_user_model
from django.utils.translation import gettext_lazy as _
User = get_user_model()
@admin.register(User)
class UserAdmin(admin.ModelAdmin):
list_display = (
"id", "email", "role", "phone_number", "city", "street", "postal_code", "gdpr", "is_active", "email_verified", "create_time"
)
list_filter = ("role", "gdpr", "is_active", "email_verified", "city", "postal_code")
search_fields = ("email", "phone_number", "city", "street", "postal_code")
ordering = ("-create_time",)
fieldsets = (
(None, {"fields": ("email", "password", "role")}),
(_("Personal info"), {"fields": ("phone_number", "city", "street", "postal_code")}),
(_("Permissions"), {"fields": ("gdpr", "is_active", "email_verified")}),
(_("Important dates"), {"fields": ("create_time",)}),
)
readonly_fields = ("create_time",)
# Register your models here.

View File

@@ -1,6 +0,0 @@
from django.apps import AppConfig
class AccountConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'account'

View File

@@ -1,24 +0,0 @@
import django_filters
from django.contrib.auth import get_user_model
User = get_user_model()
class UserFilter(django_filters.FilterSet):
role = django_filters.CharFilter(field_name="role", lookup_expr="exact")
email = django_filters.CharFilter(field_name="email", lookup_expr="icontains")
phone_number = django_filters.CharFilter(field_name="phone_number", lookup_expr="icontains")
city = django_filters.CharFilter(field_name="city", lookup_expr="icontains")
street = django_filters.CharFilter(field_name="street", lookup_expr="icontains")
postal_code = django_filters.CharFilter(field_name="postal_code", lookup_expr="exact")
gdpr = django_filters.BooleanFilter(field_name="gdpr")
is_active = django_filters.BooleanFilter(field_name="is_active")
email_verified = django_filters.BooleanFilter(field_name="email_verified")
create_time_after = django_filters.IsoDateTimeFilter(field_name="create_time", lookup_expr="gte")
create_time_before = django_filters.IsoDateTimeFilter(field_name="create_time", lookup_expr="lte")
class Meta:
model = User
fields = [
"role", "email", "phone_number", "city", "street", "postal_code", "gdpr", "is_active", "email_verified",
"create_time_after", "create_time_before"
]

View File

@@ -1,54 +0,0 @@
# Generated by Django 5.2.5 on 2025-08-13 23:19
import account.models
import django.contrib.auth.validators
import django.core.validators
import django.utils.timezone
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('auth', '0012_alter_user_first_name_max_length'),
]
operations = [
migrations.CreateModel(
name='CustomUser',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('password', models.CharField(max_length=128, verbose_name='password')),
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')),
('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')),
('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
('is_deleted', models.BooleanField(default=False)),
('deleted_at', models.DateTimeField(blank=True, null=True)),
('role', models.CharField(blank=True, choices=[('admin', 'Administrátor'), ('user', 'Uživatel')], max_length=32, null=True)),
('email_verified', models.BooleanField(default=False)),
('phone_number', models.CharField(blank=True, max_length=16, unique=True, validators=[django.core.validators.RegexValidator('^\\+?\\d{9,15}$', message='Zadejte platné telefonní číslo.')])),
('email', models.EmailField(db_index=True, max_length=254, unique=True)),
('create_time', models.DateTimeField(auto_now_add=True)),
('city', models.CharField(blank=True, max_length=100, null=True)),
('street', models.CharField(blank=True, max_length=200, null=True)),
('postal_code', models.CharField(blank=True, max_length=5, null=True, validators=[django.core.validators.RegexValidator(code='invalid_postal_code', message='Postal code must contain exactly 5 digits.', regex='^\\d{5}$')])),
('gdpr', models.BooleanField(default=False)),
('is_active', models.BooleanField(default=False)),
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to.', related_name='customuser_set', related_query_name='customuser', to='auth.group')),
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='customuser_set', related_query_name='customuser', to='auth.permission')),
],
options={
'abstract': False,
},
managers=[
('objects', account.models.CustomUserActiveManager()),
('all_objects', account.models.CustomUserAllManager()),
],
),
]

View File

@@ -1,152 +0,0 @@
import uuid
from django.db import models
from django.contrib.auth.models import AbstractUser, Group, Permission
from django.core.validators import RegexValidator, MinLengthValidator, MaxValueValidator, MinValueValidator
from django.conf import settings
from django.db import models
from django.utils import timezone
from datetime import timedelta
from vontor_cz.models import SoftDeleteModel
from django.contrib.auth.models import UserManager
import logging
logger = logging.getLogger(__name__)
# Custom User Manager to handle soft deletion
class CustomUserActiveManager(UserManager):
def get_queryset(self):
return super().get_queryset().filter(is_deleted=False)
# Custom User Manager to handle all users, including soft deleted
class CustomUserAllManager(UserManager):
def get_queryset(self):
return super().get_queryset()
class CustomUser(SoftDeleteModel, AbstractUser):
groups = models.ManyToManyField(
Group,
related_name="customuser_set", # <- přidáš related_name
blank=True,
help_text="The groups this user belongs to.",
related_query_name="customuser",
)
user_permissions = models.ManyToManyField(
Permission,
related_name="customuser_set", # <- přidáš related_name
blank=True,
help_text="Specific permissions for this user.",
related_query_name="customuser",
)
ROLE_CHOICES = (
('admin', 'Administrátor'),
('user', 'Uživatel'),
)
role = models.CharField(max_length=32, choices=ROLE_CHOICES, null=True, blank=True)
"""ACCOUNT_TYPES = (
('company', 'Firma'),
('individual', 'Fyzická osoba')
)
account_type = models.CharField(max_length=32, choices=ACCOUNT_TYPES, null=True, blank=True)"""
email_verified = models.BooleanField(default=False)
phone_number = models.CharField(
unique=True,
max_length=16,
blank=True,
validators=[RegexValidator(r'^\+?\d{9,15}$', message="Zadejte platné telefonní číslo.")]
)
email = models.EmailField(unique=True, db_index=True)
create_time = models.DateTimeField(auto_now_add=True)
"""company_id = models.CharField(
max_length=8,
blank=True,
null=True,
validators=[
RegexValidator(
regex=r'^\d{8}$',
message="Company ID must contain exactly 8 digits.",
code='invalid_company_id'
)
]
)"""
"""personal_id = models.CharField(
max_length=11,
blank=True,
null=True,
validators=[
RegexValidator(
regex=r'^\d{6}/\d{3,4}$',
message="Personal ID must be in the format 123456/7890.",
code='invalid_personal_id'
)
]
)"""
city = models.CharField(null=True, blank=True, max_length=100)
street = models.CharField(null=True, blank=True, max_length=200)
postal_code = models.CharField(
max_length=5,
blank=True,
null=True,
validators=[
RegexValidator(
regex=r'^\d{5}$',
message="Postal code must contain exactly 5 digits.",
code='invalid_postal_code'
)
]
)
gdpr = models.BooleanField(default=False)
is_active = models.BooleanField(default=False)
objects = CustomUserActiveManager()
all_objects = CustomUserAllManager()
REQUIRED_FIELDS = ['email', "username", "password"]
def __str__(self):
return f"{self.email} at {self.create_time.strftime('%d-%m-%Y %H:%M:%S')}"
def delete(self, *args, **kwargs):
self.is_active = False
#self.orders.all().update(is_deleted=True, deleted_at=timezone.now())
return super().delete(*args, **kwargs)
def save(self, *args, **kwargs):
is_new = self.pk is None # check BEFORE saving
if is_new:
if self.is_superuser or self.role == "admin":
self.is_active = True
if self.role == 'admin':
self.is_staff = True
self.is_superuser = True
if self.is_superuser:
self.role = 'admin'
else:
self.is_staff = False
return super().save(*args, **kwargs)

View File

@@ -1,73 +0,0 @@
from rest_framework.permissions import BasePermission, SAFE_METHODS
from rest_framework.permissions import IsAuthenticated
from rest_framework_api_key.permissions import HasAPIKey
#Podle svého uvážení (NEPOUŽÍVAT!!!)
class RolePermission(BasePermission):
allowed_roles = []
def has_permission(self, request, view):
# Je uživatel přihlášený a má roli z povolených?
user_has_role = (
request.user and
request.user.is_authenticated and
getattr(request.user, "role", None) in self.allowed_roles
)
# Má API klíč?
has_api_key = HasAPIKey().has_permission(request, view)
return user_has_role or has_api_key
#TOHLE POUŽÍT!!!
#Prostě stačí vložit: RoleAllowed('seller','cityClerk')
def RoleAllowed(*roles):
"""
Allows safe methods for any authenticated user.
Allows unsafe methods only for users with specific roles.
Args:
RolerAllowed('admin', 'user')
"""
class SafeOrRolePermission(BasePermission):
def has_permission(self, request, view):
# Allow safe methods for any authenticated user
if request.method in SAFE_METHODS:
return IsAuthenticated().has_permission(request, view)
# Otherwise, check the user's role
user = request.user
return user and user.is_authenticated and getattr(user, "role", None) in roles
return SafeOrRolePermission
def OnlyRolesAllowed(*roles):
class SafeOrRolePermission(BasePermission):
"""
Allows all methods only for users with specific roles.
"""
def has_permission(self, request, view):
# Otherwise, check the user's role
user = request.user
return user and user.is_authenticated and getattr(user, "role", None) in roles
return SafeOrRolePermission
# For Settings.py
class AdminOnly(BasePermission):
""" Allows access only to users with the 'admin' role.
Args:
BasePermission (rest_framework.permissions.BasePermission): Base class for permission classes.
"""
def has_permission(self, request, view):
return request.user and request.user.is_authenticated and getattr(request.user, 'role', None) == 'admin'

View File

@@ -1,210 +0,0 @@
import re
from django.utils.text import slugify
from django.core.validators import MinValueValidator, MaxValueValidator
from rest_framework import serializers
from rest_framework.exceptions import NotFound
from django.contrib.auth import get_user_model
from rest_framework_simplejwt.serializers import TokenObtainPairSerializer
from django.utils.translation import gettext_lazy as _
from django.utils.text import slugify
from .permissions import *
from rest_framework_simplejwt.serializers import TokenObtainPairSerializer
from rest_framework.exceptions import PermissionDenied
User = get_user_model()
class CustomUserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = [
"id",
"username",
"first_name",
"last_name",
"email",
"role",
"account_type",
"email_verified",
"phone_number",
"create_time",
"var_symbol",
"bank_account",
"ICO",
"RC",
"city",
"street",
"PSC",
"GDPR",
"is_active",
]
read_only_fields = ["id", "create_time", "GDPR", "username"] # <-- removed "account_type"
def update(self, instance, validated_data):
user = self.context["request"].user
staff_only_fields = ["role", "email_verified", "var_symbol", "is_active"]
if user.role not in ["admin", "cityClerk"]:
unauthorized = [f for f in staff_only_fields if f in validated_data]
if unauthorized:
raise PermissionDenied(f"You are not allowed to modify: {', '.join(unauthorized)}")
return super().update(instance, validated_data)
# Token obtaining Default Serializer
class CustomTokenObtainPairSerializer(TokenObtainPairSerializer):
username_field = User.USERNAME_FIELD
def validate(self, attrs):
login = attrs.get("username")
password = attrs.get("password")
# Allow login by username or email
user = User.objects.filter(email__iexact=login).first() or \
User.objects.filter(username__iexact=login).first()
if user is None or not user.check_password(password):
raise serializers.ValidationError(_("No active account found with the given credentials"))
# Call the parent validation to create token
data = super().validate({
self.username_field: user.username,
"password": password
})
data["user_id"] = user.id
data["username"] = user.username
data["email"] = user.email
return data
# user creating section start ------------------------------------------
class UserRegistrationSerializer(serializers.ModelSerializer):
password = serializers.CharField(
write_only=True,
help_text="Heslo musí mít alespoň 8 znaků, obsahovat velká a malá písmena a číslici."
)
class Meta:
model = User
fields = [
'first_name', 'last_name', 'email', 'phone_number', 'password',
'city', 'street', 'postal_code', 'gdpr'
]
extra_kwargs = {
'first_name': {'required': True, 'help_text': 'Křestní jméno uživatele'},
'last_name': {'required': True, 'help_text': 'Příjmení uživatele'},
'email': {'required': True, 'help_text': 'Emailová adresa uživatele'},
'phone_number': {'required': True, 'help_text': 'Telefonní číslo uživatele'},
'city': {'required': True, 'help_text': 'Město uživatele'},
'street': {'required': True, 'help_text': 'Ulice uživatele'},
'postal_code': {'required': True, 'help_text': 'PSČ uživatele'},
'gdpr': {'required': True, 'help_text': 'Souhlas se zpracováním osobních údajů'},
}
def validate_password(self, value):
if len(value) < 8:
raise serializers.ValidationError("Password must be at least 8 characters long.")
if not re.search(r"[A-Z]", value):
raise serializers.ValidationError("Password must contain at least one uppercase letter.")
if not re.search(r"[a-z]", value):
raise serializers.ValidationError("Password must contain at least one lowercase letter.")
if not re.search(r"\d", value):
raise serializers.ValidationError("Password must contain at least one digit.")
return value
def validate(self, data):
email = data.get("email")
phone = data.get("phone_number")
dgpr = data.get("GDPR")
if not dgpr:
raise serializers.ValidationError({"GDPR": "You must agree to the GDPR to register."})
if User.objects.filter(email=email).exists():
raise serializers.ValidationError({"email": "Account with this email already exists."})
if phone and User.objects.filter(phone_number=phone).exists():
raise serializers.ValidationError({"phone_number": "Account with this phone number already exists."})
return data
def create(self, validated_data):
password = validated_data.pop("password")
username = validated_data.get("username", "")
user = User.objects.create(
username=username,
is_active=False, #uživatel je defaultně deaktivovaný
**validated_data
)
user.set_password(password)
user.save()
return user
class UserActivationSerializer(serializers.Serializer):
user_id = serializers.IntegerField()
var_symbol = serializers.IntegerField(validators=[MinValueValidator(0), MaxValueValidator(9999999999)])
def save(self, **kwargs):
try:
user = User.objects.get(pk=self.validated_data['user_id'])
except User.DoesNotExist:
raise NotFound("User with this ID does not exist.")
user.var_symbol = self.validated_data['var_symbol']
user.is_active = True
user.save()
return user
def to_representation(self, instance):
return {
"id": instance.id,
"email": instance.email,
"var_symbol": instance.var_symbol,
"is_active": instance.is_active,
}
class Meta:
model = User
fields = [
'user_id', 'var_symbol'
]
extra_kwargs = {
'user_id': {'required': True, 'help_text': 'ID uživatele'},
'var_symbol': {'required': True, 'help_text': 'Variablní symbol, zadán úředníkem'},
}
# user creating section end --------------------------------------------
class PasswordResetRequestSerializer(serializers.Serializer):
email = serializers.EmailField(
help_text="E-mail registrovaného a aktivního uživatele, na který bude zaslán reset hesla."
)
def validate_email(self, value):
if not User.objects.filter(email=value, is_active=True).exists():
raise serializers.ValidationError("Účet s tímto emailem neexistuje nebo není aktivní.")
return value
class PasswordResetConfirmSerializer(serializers.Serializer):
password = serializers.CharField(
write_only=True,
help_text="Nové heslo musí mít alespoň 8 znaků, obsahovat velká a malá písmena a číslici."
)
def validate_password(self, value):
import re
if len(value) < 8:
raise serializers.ValidationError("Heslo musí mít alespoň 8 znaků.")
if not re.search(r"[A-Z]", value):
raise serializers.ValidationError("Musí obsahovat velké písmeno.")
if not re.search(r"[a-z]", value):
raise serializers.ValidationError("Musí obsahovat malé písmeno.")
if not re.search(r"\d", value):
raise serializers.ValidationError("Musí obsahovat číslici.")
return value

View File

@@ -1,85 +0,0 @@
from celery import shared_task
from celery.utils.log import get_task_logger
from django.core.mail import send_mail
from django.conf import settings
from django.utils.http import urlsafe_base64_encode
from django.utils.encoding import force_bytes
from django.template.loader import render_to_string
from .tokens import *
from .models import CustomUser
logger = get_task_logger(__name__)
@shared_task
def send_password_reset_email_task(user_id):
try:
user = CustomUser.objects.get(pk=user_id)
except CustomUser.DoesNotExist:
error_msg = f"Task send_password_reset_email has failed. Invalid User ID was sent."
logger.error(error_msg)
raise Exception(error_msg)
uid = urlsafe_base64_encode(force_bytes(user.pk))
token = password_reset_token.make_token(user)
reset_url = f"{settings.FRONTEND_URL}/reset-password/{uid}/{token}"
html_message = render_to_string(
'emails/password_reset.html',
{'reset_url': reset_url}
)
if settings.EMAIL_BACKEND == 'django.core.mail.backends.console.EmailBackend':
logger.debug("\nEMAIL OBSAH:\n", html_message, "\nKONEC OBSAHU")
send_email_with_context(
recipients=user.email,
subject="Obnova hesla",
message=None,
html_message=html_message
)
# Only email verification for user registration
@shared_task
def send_email_verification_task(user_id):
try:
user = CustomUser.objects.get(pk=user_id)
except CustomUser.DoesNotExist:
error_msg = f"Task send_email_verification_task has failed. Invalid User ID was sent."
logger.error(error_msg)
raise Exception(error_msg)
uid = urlsafe_base64_encode(force_bytes(user.pk))
token = account_activation_token.make_token(user)
verification_url = f"{settings.FRONTEND_URL}/email-verification/?uidb64={uid}&token={token}"
html_message = render_to_string(
'emails/email_verification.html',
{'verification_url': verification_url}
)
if settings.EMAIL_BACKEND == 'django.core.mail.backends.console.EmailBackend':
logger.debug("\nEMAIL OBSAH:\n", html_message, "\nKONEC OBSAHU")
send_email_with_context(
recipients=user.email,
subject="Ověření e-mailu",
message=None,
html_message=html_message
)
def send_email_with_context(recipients, subject, message=None, html_message=None):
"""
General function to send emails with a specific context.
"""
if isinstance(recipients, str):
recipients = [recipients]
try:
send_mail(
subject=subject,
message=message if message else '',
from_email=None,
recipient_list=recipients,
fail_silently=False,
html_message=html_message
)
if settings.EMAIL_BACKEND == 'django.core.mail.backends.console.EmailBackend':
logger.debug("\nEMAIL OBSAH:\n", html_message if html_message else message, "\nKONEC OBSAHU")
return True
except Exception as e:
logger.error(f"E-mail se neodeslal: {e}")
return False

View File

@@ -1,19 +0,0 @@
<!DOCTYPE html>
<html lang="cs">
<head>
<meta charset="UTF-8">
<title>Ověření e-mailu</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container mt-5">
<div class="card">
<div class="card-body">
<h2 class="card-title">Ověření e-mailu</h2>
<p class="card-text">Ověřte svůj e-mail kliknutím na odkaz níže:</p>
<a href="{{ verification_url }}" class="btn btn-success">Ověřit e-mail</a>
</div>
</div>
</div>
</body>
</html>

View File

@@ -1,19 +0,0 @@
<!DOCTYPE html>
<html lang="cs">
<head>
<meta charset="UTF-8">
<title>Obnova hesla</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container mt-5">
<div class="card">
<div class="card-body">
<h2 class="card-title">Obnova hesla</h2>
<p class="card-text">Pro obnovu hesla klikněte na následující odkaz:</p>
<a href="{{ reset_url }}" class="btn btn-primary">Obnovit heslo</a>
</div>
</div>
</div>
</body>
</html>

View File

@@ -1,33 +0,0 @@
from django.contrib.auth.tokens import PasswordResetTokenGenerator
# Subclass PasswordResetTokenGenerator to create a separate token generator
# for account activation. This allows future customization specific to activation tokens,
# even though it currently behaves exactly like the base class.
class AccountActivationTokenGenerator(PasswordResetTokenGenerator):
pass # No changes yet; inherits all behavior from PasswordResetTokenGenerator
# Create an instance of AccountActivationTokenGenerator to be used for generating
# and validating account activation tokens throughout the app.
account_activation_token = AccountActivationTokenGenerator()
# Create an instance of the base PasswordResetTokenGenerator to be used
# for password reset tokens.
password_reset_token = PasswordResetTokenGenerator()
from rest_framework_simplejwt.authentication import JWTAuthentication
#NEMĚNIT CUSTOM SBÍRANÍ COOKIE TOKENU
class CookieJWTAuthentication(JWTAuthentication):
def authenticate(self, request):
raw_token = request.COOKIES.get('access_token')
if not raw_token:
return None
validated_token = self.get_validated_token(raw_token)
return self.get_user(validated_token), validated_token

View File

@@ -1,27 +0,0 @@
from django.urls import path
from . import views
from django.urls import path, include
from rest_framework.routers import DefaultRouter
router = DefaultRouter()
router.register(r'users', views.UserView, basename='user')
urlpatterns = [
# Auth endpoints
path('login/', views.CookieTokenObtainPairView.as_view(), name='login'),
path('token/refresh/', views.CookieTokenRefreshView.as_view(), name='token-refresh'),
path('logout/', views.LogoutView.as_view(), name='logout'),
path('user/me/', views.CurrentUserView.as_view(), name='user-detail'),
# Registration & email endpoints
path('register/', views.UserRegistrationViewSet.as_view({'post': 'create'}), name='register'),
path('verify-email/<uidb64>/<token>/', views.EmailVerificationView.as_view(), name='verify-email'),
path('activate/', views.UserActivationViewSet.as_view(), name='activate-user'),
# Password reset endpoints
path('password-reset/', views.PasswordResetRequestView.as_view(), name='password-reset-request'),
path('password-reset-confirm/<uidb64>/<token>/', views.PasswordResetConfirmView.as_view(), name='password-reset-confirm'),
# User CRUD (list, retrieve, update, delete)
path('', include(router.urls)), #/users/
]

View File

@@ -1,421 +0,0 @@
from django.contrib.auth import get_user_model, authenticate
from django.utils.http import urlsafe_base64_encode, urlsafe_base64_decode
from django.utils.encoding import force_bytes, force_str
from .serializers import *
from .permissions import *
from .models import CustomUser
from .tokens import *
from .tasks import send_password_reset_email_task
from django.conf import settings
import logging
logger = logging.getLogger(__name__)
from .filters import UserFilter
from rest_framework import generics, permissions, status, viewsets
from rest_framework.response import Response
from rest_framework_simplejwt.tokens import RefreshToken
from rest_framework.views import APIView
from rest_framework.viewsets import ModelViewSet
from rest_framework.permissions import IsAuthenticated, AllowAny
from rest_framework_simplejwt.tokens import RefreshToken
from rest_framework_simplejwt.exceptions import TokenError, AuthenticationFailed
from django_filters.rest_framework import DjangoFilterBackend
from drf_spectacular.utils import extend_schema, OpenApiResponse, OpenApiExample, OpenApiParameter
User = get_user_model()
#general user view API
from rest_framework_simplejwt.views import TokenObtainPairView
#---------------------------------------------TOKENY------------------------------------------------
# Custom Token obtaining view
@extend_schema(
tags=["Authentication"],
summary="Obtain JWT access and refresh tokens (cookie-based)",
description="Authenticate user and obtain JWT access and refresh tokens. You can use either email or username.",
request=CustomTokenObtainPairSerializer,
responses={
200: OpenApiResponse(response=CustomTokenObtainPairSerializer, description="Tokens returned successfully."),
401: OpenApiResponse(description="Invalid credentials or inactive user."),
},
)
class CookieTokenObtainPairView(TokenObtainPairView):
serializer_class = CustomTokenObtainPairSerializer
def post(self, request, *args, **kwargs):
response = super().post(request, *args, **kwargs)
# Získáme tokeny z odpovědi
access = response.data.get("access")
refresh = response.data.get("refresh")
if not access or not refresh:
return response # Např. při chybě přihlášení
jwt_settings = settings.SIMPLE_JWT
# Access token cookie
response.set_cookie(
key=jwt_settings.get("AUTH_COOKIE", "access_token"),
value=access,
httponly=jwt_settings.get("AUTH_COOKIE_HTTP_ONLY", True),
secure=jwt_settings.get("AUTH_COOKIE_SECURE", not settings.DEBUG),
samesite=jwt_settings.get("AUTH_COOKIE_SAMESITE", "Lax"),
path=jwt_settings.get("AUTH_COOKIE_PATH", "/"),
max_age=5 * 60, # 5 minut
)
# Refresh token cookie
response.set_cookie(
key="refresh_token",
value=refresh,
httponly=True,
secure=not settings.DEBUG,
samesite="Lax",
path="/",
max_age=7 * 24 * 60 * 60, # 7 dní
)
return response
def validate(self, attrs):
username = attrs.get("username")
password = attrs.get("password")
# Přihlaš uživatele ručně
user = authenticate(request=self.context.get('request'), username=username, password=password)
if not user:
raise AuthenticationFailed("Špatné uživatelské jméno nebo heslo.")
if not user.is_active:
raise AuthenticationFailed("Uživatel je deaktivován.")
# Nastav validní uživatele (přebere další logiku ze SimpleJWT)
self.user = user
# Vrátí access a refresh token jako obvykle
return super().validate(attrs)
@extend_schema(
tags=["Authentication"],
summary="Refresh JWT token using cookie",
description="Refresh JWT access and refresh tokens using the refresh token stored in cookie.",
responses={
200: OpenApiResponse(description="Tokens refreshed successfully."),
400: OpenApiResponse(description="Refresh token cookie not found."),
401: OpenApiResponse(description="Invalid refresh token."),
},
)
class CookieTokenRefreshView(APIView):
def post(self, request):
refresh_token = request.COOKIES.get('refresh_token')
if not refresh_token:
return Response({"detail": "Refresh token cookie not found."}, status=status.HTTP_400_BAD_REQUEST)
try:
refresh = RefreshToken(refresh_token)
access_token = str(refresh.access_token)
new_refresh_token = str(refresh) # volitelně nový refresh token
response = Response({
"access": access_token,
"refresh": new_refresh_token,
})
# Nastav nové HttpOnly cookies
# Access token cookie (např. 5 minut platnost)
response.set_cookie(
"access_token",
access_token,
httponly=True,
secure=not settings.DEBUG,
samesite="Lax",
max_age=5 * 60,
path="/",
)
# Refresh token cookie (delší platnost, např. 7 dní)
response.set_cookie(
"refresh_token",
new_refresh_token,
httponly=True,
secure=not settings.DEBUG,
samesite="Lax",
max_age=7 * 24 * 60 * 60,
path="/",
)
return response
except TokenError:
return Response({"detail": "Invalid refresh token."}, status=status.HTTP_401_UNAUTHORIZED)
#---------------------------------------------LOGIN/LOGOUT------------------------------------------------
@extend_schema(
tags=["Authentication"],
summary="Logout user (delete access and refresh token cookies)",
description="Logs out the user by deleting access and refresh token cookies.",
responses={
200: OpenApiResponse(description="Logout successful."),
},
)
class LogoutView(APIView):
permission_classes = [AllowAny]
def post(self, request):
response = Response({"detail": "Logout successful"}, status=status.HTTP_200_OK)
# Smazání cookies
response.delete_cookie("access_token", path="/")
response.delete_cookie("refresh_token", path="/")
return response
#--------------------------------------------------------------------------------------------------------------
@extend_schema(
tags=["User"],
summary="List, retrieve, update, and delete users.",
description="Displays all users with filtering and ordering options. Requires authentication and appropriate role.",
responses={
200: OpenApiResponse(response=CustomUserSerializer, description="User(s) retrieved successfully."),
403: OpenApiResponse(description="Permission denied."),
},
)
class UserView(viewsets.ModelViewSet):
queryset = User.objects.all()
serializer_class = CustomUserSerializer
filter_backends = [DjangoFilterBackend]
filterset_class = UserFilter
# Require authentication and role permission
permission_classes = [IsAuthenticated]
class Meta:
model = CustomUser
extra_kwargs = {
"email": {"help_text": "Unikátní e-mailová adresa uživatele."},
"phone_number": {"help_text": "Telefonní číslo ve formátu +420123456789."},
"role": {"help_text": "Role uživatele určující jeho oprávnění v systému."},
"account_type": {"help_text": "Typ účtu firma nebo fyzická osoba."},
"email_verified": {"help_text": "Určuje, zda je e-mail ověřen."},
"create_time": {"help_text": "Datum a čas registrace uživatele (pouze pro čtení).", "read_only": True},
"var_symbol": {"help_text": "Variabilní symbol pro platby, pokud je vyžadován."},
"bank_account": {"help_text": "Číslo bankovního účtu uživatele."},
"ICO": {"help_text": "IČO firmy, pokud se jedná o firemní účet."},
"RC": {"help_text": "Rodné číslo pro fyzické osoby."},
"city": {"help_text": "Město trvalého pobytu / sídla."},
"street": {"help_text": "Ulice a číslo popisné."},
"PSC": {"help_text": "PSČ místa pobytu / sídla."},
"GDPR": {"help_text": "Souhlas se zpracováním osobních údajů."},
"is_active": {"help_text": "Stav aktivace uživatele."},
}
def get_permissions(self):
# Only admin can list or create users
if self.action in ['list', 'create']:
return [OnlyRolesAllowed("admin")()]
# Only admin or the user themselves can update or delete
elif self.action in ['update', 'partial_update', 'destroy']:
if self.request.user.role == 'admin':
return [OnlyRolesAllowed("admin")()]
elif self.kwargs.get('pk') and str(self.request.user.id) == self.kwargs['pk']:
return [IsAuthenticated()]
else:
# fallback - deny access
return [OnlyRolesAllowed("admin")()]
# Any authenticated user can retrieve (view) any user's profile
elif self.action == 'retrieve':
return [IsAuthenticated()]
return super().get_permissions()
# Get current user data
@extend_schema(
tags=["User"],
summary="Get current authenticated user",
description="Returns details of the currently authenticated user based on JWT token or session.",
responses={
200: OpenApiResponse(response=CustomUserSerializer, description="Current user details."),
401: OpenApiResponse(description="Unauthorized, user is not authenticated."),
}
)
class CurrentUserView(APIView):
permission_classes = [IsAuthenticated]
def get(self, request):
serializer = CustomUserSerializer(request.user)
return Response(serializer.data)
#------------------------------------------------REGISTRACE--------------------------------------------------------------
#1. registration API
@extend_schema(
tags=["User Registration"],
summary="Register a new user (company or individual)",
description="Register a new user (company or individual). The user will receive an email with a verification link.",
request=UserRegistrationSerializer,
responses={
201: OpenApiResponse(response=UserRegistrationSerializer, description="User registered successfully."),
400: OpenApiResponse(description="Invalid registration data."),
},
)
class UserRegistrationViewSet(ModelViewSet):
queryset = CustomUser.objects.all()
serializer_class = UserRegistrationSerializer
http_method_names = ['post']
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
user = serializer.save()
try:
send_email_verification_task.delay(user.id) # posílaní emailu pro potvrzení registrace - CELERY TASK
except Exception as e:
logger.error(f"Celery not available, using fallback. Error: {e}")
send_email_verification_task(user.id) # posílaní emailu pro potvrzení registrace
headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
#2. confirming email
@extend_schema(
tags=["User Registration"],
summary="Verify user email via link",
description="Verify user email using the link with uid and token.",
parameters=[
OpenApiParameter(name='uidb64', type=str, location=OpenApiParameter.PATH, description="User ID encoded in base64 from email link."),
OpenApiParameter(name='token', type=str, location=OpenApiParameter.PATH, description="User token from email link."),
],
responses={
200: OpenApiResponse(description="Email successfully verified."),
400: OpenApiResponse(description="Invalid or expired token."),
},
)
class EmailVerificationView(APIView):
def get(self, request, uidb64, token):
try:
uid = force_str(urlsafe_base64_decode(uidb64))
user = User.objects.get(pk=uid)
except (User.DoesNotExist, ValueError, TypeError):
return Response({"error": "Neplatný odkaz."}, status=400)
if account_activation_token.check_token(user, token):
user.email_verified = True
user.save()
return Response({"detail": "E-mail byl úspěšně ověřen. Účet čeká na schválení."})
else:
return Response({"error": "Token je neplatný nebo expirovaný."}, status=400)
#3. seller activation API (var_symbol)
@extend_schema(
tags=["User Registration"],
summary="Activate user and set variable symbol (admin/cityClerk only)",
description="Activate user and set variable symbol. Only accessible by admin or cityClerk.",
request=UserActivationSerializer,
responses={
200: OpenApiResponse(response=UserActivationSerializer, description="User activated successfully."),
400: OpenApiResponse(description="Invalid activation data."),
404: OpenApiResponse(description="User not found."),
},
)
class UserActivationViewSet(APIView):
permission_classes = [OnlyRolesAllowed('cityClerk', 'admin')]
def patch(self, request, *args, **kwargs):
serializer = UserActivationSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
user = serializer.save()
try:
send_email_clerk_accepted_task.delay(user.id) # posílaní emailu pro informování uživatele o dokončení registrace, uředník doplnil variabilní symbol - CELERY TASK
except Exception as e:
logger.error(f"Celery not available, using fallback. Error: {e}")
send_email_clerk_accepted_task(user.id) # posílaní emailu pro informování uživatele o dokončení registrace, uředník doplnil variabilní symbol
return Response(serializer.to_representation(user), status=status.HTTP_200_OK)
#-------------------------------------------------END REGISTRACE-------------------------------------------------------------
#1. PasswordReset + send Email
@extend_schema(
tags=["User password reset"],
summary="Request password reset (send email)",
description="Request password reset by providing registered email. An email with instructions will be sent.",
request=PasswordResetRequestSerializer,
responses={
200: OpenApiResponse(description="Email with instructions sent."),
400: OpenApiResponse(description="Invalid email or request data."),
},
)
class PasswordResetRequestView(APIView):
def post(self, request):
serializer = PasswordResetRequestSerializer(data=request.data)
if serializer.is_valid():
try:
user = User.objects.get(email=serializer.validated_data['email'])
except User.DoesNotExist:
# Always return 200 even if user doesn't exist to avoid user enumeration
return Response({"detail": "E-mail s odkazem byl odeslán."})
try:
send_password_reset_email_task.delay(user.id) # posílaní emailu pro obnovení hesla - CELERY TASK
except Exception as e:
logger.error(f"Celery not available, using fallback. Error: {e}")
send_password_reset_email_task(user.id) # posílaní emailu pro obnovení hesla registrace
return Response({"detail": "E-mail s odkazem byl odeslán."})
return Response(serializer.errors, status=400)
#2. Confirming reset
@extend_schema(
tags=["User password reset"],
summary="Confirm password reset via token",
description="Confirm password reset using token from email.",
request=PasswordResetConfirmSerializer,
parameters=[
OpenApiParameter(name='uidb64', type=str, location=OpenApiParameter.PATH, description="User ID encoded in base64 from email link."),
OpenApiParameter(name='token', type=str, location=OpenApiParameter.PATH, description="Password reset token from email link."),
],
responses={
200: OpenApiResponse(description="Password changed successfully."),
400: OpenApiResponse(description="Invalid token or request data."),
},
)
class PasswordResetConfirmView(APIView):
def post(self, request, uidb64, token):
try:
uid = force_str(urlsafe_base64_decode(uidb64))
user = User.objects.get(pk=uid)
except (TypeError, ValueError, OverflowError, User.DoesNotExist):
return Response({"error": "Neplatný odkaz."}, status=400)
if not password_reset_token.check_token(user, token):
return Response({"error": "Token je neplatný nebo expirovaný."}, status=400)
serializer = PasswordResetConfirmSerializer(data=request.data)
if serializer.is_valid():
user.set_password(serializer.validated_data['password'])
user.save()
return Response({"detail": "Heslo bylo úspěšně změněno."})
return Response(serializer.errors, status=400)

View File

@@ -1,91 +0,0 @@
# -- BASE --
requests
pip
python-dotenv # .env support
virtualenv #venv
Django
numpy # NumPy je knihovna programovacího jazyka Python, která poskytuje infrastrukturu pro práci s vektory, maticemi a obecně vícerozměrnými poli.
# -- DATABASE --
sqlparse #non-validating SQL parser for Python. It provides support for parsing, splitting and formatting SQL statements.
tzdata #timezone
psycopg[binary] #PostgreSQL database adapter for the Python
django-filter
django-constance #allows you to store and manage settings of page in the Django admin interface!!!!
# -- OBJECT STORAGE --
Pillow #adds image processing capabilities to your Python interpreter
whitenoise #pomáha se spuštěním serveru a načítaní static files
django-cleanup #odstraní zbytečné media soubory které nejsou v databázi/modelu
django-storages # potřeba k S3 bucket storage
boto3
# -- PROTOCOLS (asgi, websockets) --
redis
channels_redis
channels #django channels
#channels requried package
uvicorn[standard]
daphne
gunicorn
# -- REST API --
djangorestframework #REST Framework
djangorestframework-api-key #API key
djangorestframework-simplejwt #JWT authentication for Django REST Framework
PyJWT #JSON Web Token implementation in Python
asgiref #ASGI reference implementation, to be used with Django Channels
pytz
# pytz brings the Olson tz database into Python and allows
# accurate and cross platform timezone calculations.
# It also solves the issue of ambiguous times at the end of daylight saving time.
#documentation for frontend dev
drf-spectacular
# -- APPS --
django-tinymce
django-cors-headers #csfr
celery #slouží k vytvaření asynchoních úkolu (třeba každou hodinu vyčistit cache atd.)
django-celery-beat #slouží k plánování úkolů pro Celery
# -- EDITING photos, gifs, videos --
#aiofiles
#opencv-python #moviepy use this better instead of pillow
#moviepy
#yt-dlp
weasyprint #tvoření PDFek z html dokumentu + css styly
## -- MISCELLANEOUS --
faker #generates fake data for testing purposes
## -- api --
stripe
gopay

View File

@@ -1,3 +0,0 @@
from django.db import models
# Create your models here.

View File

@@ -1,37 +0,0 @@
from rest_framework import serializers
class GoPayCreatePaymentRequestSerializer(serializers.Serializer):
amount = serializers.DecimalField(max_digits=12, decimal_places=2, min_value=0.01)
currency = serializers.CharField(required=False, default="CZK")
order_number = serializers.CharField(required=False, allow_blank=True, default="order-001")
order_description = serializers.CharField(required=False, allow_blank=True, default="Example GoPay payment")
return_url = serializers.URLField(required=False)
notify_url = serializers.URLField(required=False)
preauthorize = serializers.BooleanField(required=False, default=False)
class GoPayPaymentCreatedResponseSerializer(serializers.Serializer):
id = serializers.IntegerField()
state = serializers.CharField()
gw_url = serializers.URLField(required=False, allow_null=True)
class GoPayStatusResponseSerializer(serializers.Serializer):
id = serializers.IntegerField()
state = serializers.CharField()
class GoPayRefundRequestSerializer(serializers.Serializer):
amount = serializers.DecimalField(max_digits=12, decimal_places=2, required=False, min_value=0.01)
class GoPayCaptureRequestSerializer(serializers.Serializer):
amount = serializers.DecimalField(max_digits=12, decimal_places=2, required=False, min_value=0.01)
class GoPayCreateRecurrenceRequestSerializer(serializers.Serializer):
amount = serializers.DecimalField(max_digits=12, decimal_places=2, min_value=0.01)
currency = serializers.CharField(required=False, default="CZK")
order_number = serializers.CharField(required=False, allow_blank=True, default="recur-001")
order_description = serializers.CharField(required=False, allow_blank=True, default="Recurring payment")

View File

@@ -1,20 +0,0 @@
from django.urls import path
from .views import (
GoPayPaymentView,
GoPayPaymentStatusView,
GoPayRefundPaymentView,
GoPayCaptureAuthorizationView,
GoPayVoidAuthorizationView,
GoPayCreateRecurrenceView,
GoPayPaymentInstrumentsView,
)
urlpatterns = [
path('payment/', GoPayPaymentView.as_view(), name='gopay-payment'),
path('payment/<int:payment_id>/status/', GoPayPaymentStatusView.as_view(), name='gopay-payment-status'),
path('payment/<int:payment_id>/refund/', GoPayRefundPaymentView.as_view(), name='gopay-refund-payment'),
path('payment/<int:payment_id>/capture/', GoPayCaptureAuthorizationView.as_view(), name='gopay-capture-authorization'),
path('payment/<int:payment_id>/void/', GoPayVoidAuthorizationView.as_view(), name='gopay-void-authorization'),
path('payment/<int:payment_id>/recurrence/', GoPayCreateRecurrenceView.as_view(), name='gopay-create-recurrence'),
path('payment-instruments/', GoPayPaymentInstrumentsView.as_view(), name='gopay-payment-instruments'),
]

View File

@@ -1,233 +0,0 @@
from django.shortcuts import render
# Create your views here.
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated
import gopay
from gopay.enums import TokenScope, Language
import os
from drf_spectacular.utils import extend_schema, OpenApiResponse, OpenApiExample, OpenApiParameter
from .serializers import (
GoPayCreatePaymentRequestSerializer,
GoPayPaymentCreatedResponseSerializer,
GoPayStatusResponseSerializer,
GoPayRefundRequestSerializer,
GoPayCaptureRequestSerializer,
GoPayCreateRecurrenceRequestSerializer,
)
class GoPayClientMixin:
"""Shared helpers for configuring GoPay client and formatting responses."""
def get_gopay_client(self):
gateway_url = os.getenv("GOPAY_GATEWAY_URL", "https://gw.sandbox.gopay.com/api")
return gopay.payments({
"goid": os.getenv("GOPAY_GOID"),
"client_id": os.getenv("GOPAY_CLIENT_ID"),
"client_secret": os.getenv("GOPAY_CLIENT_SECRET"),
"gateway_url": gateway_url,
"scope": TokenScope.ALL,
"language": Language.CZECH,
})
def _to_response(self, sdk_response):
# The GoPay SDK returns a response object with has_succeed(), json, errors, status_code
try:
if hasattr(sdk_response, "has_succeed") and sdk_response.has_succeed():
return Response(getattr(sdk_response, "json", {}))
status = getattr(sdk_response, "status_code", 400)
errors = getattr(sdk_response, "errors", None)
if errors is None and hasattr(sdk_response, "json"):
errors = sdk_response.json
if errors is None:
errors = {"detail": "GoPay request failed"}
return Response({"errors": errors}, status=status)
except Exception as e:
return Response({"errors": str(e)}, status=500)
class GoPayPaymentView(GoPayClientMixin, APIView):
permission_classes = [IsAuthenticated]
@extend_schema(
tags=["GoPay"],
summary="Create GoPay payment",
description="Creates a GoPay payment and returns gateway URL and payment info.",
request=GoPayCreatePaymentRequestSerializer,
responses={
200: OpenApiResponse(response=GoPayPaymentCreatedResponseSerializer, description="Payment created"),
400: OpenApiResponse(description="Validation error or SDK error"),
},
examples=[
OpenApiExample(
"Create payment",
value={
"amount": 123.45,
"currency": "CZK",
"order_number": "order-001",
"order_description": "Example GoPay payment",
"return_url": "https://yourfrontend.com/success",
"notify_url": "https://yourbackend.com/gopay/notify",
"preauthorize": False,
},
request_only=True,
)
]
)
def post(self, request):
amount = request.data.get("amount")
currency = request.data.get("currency", "CZK")
order_number = request.data.get("order_number", "order-001")
order_description = request.data.get("order_description", "Example GoPay payment")
return_url = request.data.get("return_url", "https://yourfrontend.com/success")
notify_url = request.data.get("notify_url", "https://yourbackend.com/gopay/notify")
preauthorize = bool(request.data.get("preauthorize", False))
if not amount:
return Response({"error": "Amount is required"}, status=400)
payments = self.get_gopay_client()
payment_data = {
"payer": {
"allowed_payment_instruments": ["PAYMENT_CARD"],
"default_payment_instrument": "PAYMENT_CARD",
"allowed_swifts": ["FIOB"],
"contact": {
"first_name": getattr(request.user, "first_name", ""),
"last_name": getattr(request.user, "last_name", ""),
"email": getattr(request.user, "email", ""),
},
},
"amount": int(float(amount) * 100), # GoPay expects amount in cents
"currency": currency,
"order_number": order_number,
"order_description": order_description,
"items": [
{"name": "Example Item", "amount": int(float(amount) * 100)}
],
"callback": {"return_url": return_url, "notify_url": notify_url},
"preauthorize": preauthorize,
}
resp = payments.create_payment(payment_data)
return self._to_response(resp)
class GoPayPaymentStatusView(GoPayClientMixin, APIView):
permission_classes = [IsAuthenticated]
@extend_schema(
tags=["GoPay"],
summary="Get GoPay payment status",
parameters=[OpenApiParameter(name="payment_id", required=True, type=int, location=OpenApiParameter.PATH)],
responses={200: OpenApiResponse(response=GoPayStatusResponseSerializer, description="Payment status")},
)
def get(self, request, payment_id: int):
payments = self.get_gopay_client()
resp = payments.get_status(payment_id)
return self._to_response(resp)
class GoPayRefundPaymentView(GoPayClientMixin, APIView):
permission_classes = [IsAuthenticated]
@extend_schema(
tags=["GoPay"],
summary="Refund GoPay payment",
parameters=[OpenApiParameter(name="payment_id", required=True, type=int, location=OpenApiParameter.PATH)],
request=GoPayRefundRequestSerializer,
responses={200: OpenApiResponse(description="Refund processed")},
)
def post(self, request, payment_id: int):
amount = request.data.get("amount") # optional for full refund
payments = self.get_gopay_client()
if amount is None or amount == "":
# Full refund
resp = payments.refund_payment(payment_id)
else:
resp = payments.refund_payment(payment_id, int(float(amount) * 100))
return self._to_response(resp)
class GoPayCaptureAuthorizationView(GoPayClientMixin, APIView):
permission_classes = [IsAuthenticated]
@extend_schema(
tags=["GoPay"],
summary="Capture GoPay authorization",
parameters=[OpenApiParameter(name="payment_id", required=True, type=int, location=OpenApiParameter.PATH)],
request=GoPayCaptureRequestSerializer,
responses={200: OpenApiResponse(description="Capture processed")},
)
def post(self, request, payment_id: int):
amount = request.data.get("amount") # optional for partial capture
payments = self.get_gopay_client()
if amount is None or amount == "":
resp = payments.capture_authorization(payment_id)
else:
resp = payments.capture_authorization(payment_id, int(float(amount) * 100))
return self._to_response(resp)
class GoPayVoidAuthorizationView(GoPayClientMixin, APIView):
permission_classes = [IsAuthenticated]
@extend_schema(
tags=["GoPay"],
summary="Void GoPay authorization",
parameters=[OpenApiParameter(name="payment_id", required=True, type=int, location=OpenApiParameter.PATH)],
responses={200: OpenApiResponse(description="Authorization voided")},
)
def post(self, request, payment_id: int):
payments = self.get_gopay_client()
resp = payments.void_authorization(payment_id)
return self._to_response(resp)
class GoPayCreateRecurrenceView(GoPayClientMixin, APIView):
permission_classes = [IsAuthenticated]
@extend_schema(
tags=["GoPay"],
summary="Create GoPay recurrence",
parameters=[OpenApiParameter(name="payment_id", required=True, type=int, location=OpenApiParameter.PATH)],
request=GoPayCreateRecurrenceRequestSerializer,
responses={200: OpenApiResponse(description="Recurrence created")},
)
def post(self, request, payment_id: int):
amount = request.data.get("amount")
currency = request.data.get("currency", "CZK")
order_number = request.data.get("order_number", "recur-001")
order_description = request.data.get("order_description", "Recurring payment")
if not amount:
return Response({"error": "Amount is required"}, status=400)
payments = self.get_gopay_client()
recurrence_payload = {
"amount": int(float(amount) * 100),
"currency": currency,
"order_number": order_number,
"order_description": order_description,
}
resp = payments.create_recurrence(payment_id, recurrence_payload)
return self._to_response(resp)
class GoPayPaymentInstrumentsView(GoPayClientMixin, APIView):
permission_classes = [IsAuthenticated]
@extend_schema(
tags=["GoPay"],
summary="Get GoPay payment instruments",
parameters=[OpenApiParameter(name="currency", required=False, type=str, location=OpenApiParameter.QUERY)],
responses={200: OpenApiResponse(description="Available payment instruments returned")},
)
def get(self, request):
currency = request.query_params.get("currency", "CZK")
goid = os.getenv("GOPAY_GOID")
if not goid:
return Response({"error": "GOPAY_GOID is not configured"}, status=500)
payments = self.get_gopay_client()
resp = payments.get_payment_instruments(goid, currency)
return self._to_response(resp)

View File

@@ -1,3 +0,0 @@
from django.contrib import admin
# Register your models here.

View File

@@ -1,3 +0,0 @@
from django.db import models
# Create your models here.

View File

@@ -1,12 +0,0 @@
from rest_framework import serializers
class StripeCheckoutRequestSerializer(serializers.Serializer):
amount = serializers.DecimalField(max_digits=12, decimal_places=2, min_value=0.01)
product_name = serializers.CharField(required=False, default="Example Product")
success_url = serializers.URLField(required=False)
cancel_url = serializers.URLField(required=False)
class StripeCheckoutResponseSerializer(serializers.Serializer):
url = serializers.URLField()

View File

@@ -1,3 +0,0 @@
from django.test import TestCase
# Create your tests here.

View File

@@ -1,6 +0,0 @@
from django.urls import path
from .views import StripeCheckoutCZKView
urlpatterns = [
path('checkout/', StripeCheckoutCZKView.as_view(), name='stripe-checkout-czk'),
]

View File

@@ -1,71 +0,0 @@
import stripe
import os
from rest_framework.views import APIView
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from drf_spectacular.utils import extend_schema, OpenApiResponse, OpenApiExample, OpenApiParameter
from .serializers import (
StripeCheckoutRequestSerializer,
StripeCheckoutResponseSerializer,
)
stripe.api_key = os.getenv("STRIPE_SECRET_KEY")
class StripeCheckoutCZKView(APIView):
permission_classes = [IsAuthenticated]
@extend_schema(
tags=["Stripe"],
summary="Create Stripe Checkout session in CZK",
description="Creates a Stripe Checkout session for payment in Czech Koruna (CZK). Requires authentication.",
request=StripeCheckoutRequestSerializer,
responses={
200: OpenApiResponse(response=StripeCheckoutResponseSerializer, description="Stripe Checkout session URL returned successfully."),
400: OpenApiResponse(description="Amount is required or invalid."),
},
examples=[
OpenApiExample(
"Success",
value={"url": "https://checkout.stripe.com/pay/cs_test_123456"},
response_only=True,
status_codes=["200"],
),
OpenApiExample(
"Missing amount",
value={"error": "Amount is required"},
response_only=True,
status_codes=["400"],
),
]
)
def post(self, request):
serializer = StripeCheckoutRequestSerializer(data=request.data)
if not serializer.is_valid():
return Response(serializer.errors, status=400)
amount = serializer.validated_data.get("amount")
product_name = serializer.validated_data.get("product_name", "Example Product")
success_url = serializer.validated_data.get("success_url", "https://yourfrontend.com/success")
cancel_url = serializer.validated_data.get("cancel_url", "https://yourfrontend.com/cancel")
# Stripe expects amount in the smallest currency unit (haléř = 1/100 CZK)
amount_in_haler = int(amount * 100)
session = stripe.checkout.Session.create(
payment_method_types=['card'],
line_items=[{
'price_data': {
'currency': 'czk',
'product_data': {
'name': product_name,
},
'unit_amount': amount_in_haler,
},
'quantity': 1,
}],
mode='payment',
success_url=success_url,
cancel_url=cancel_url,
customer_email=getattr(request.user, 'email', None)
)
return Response({"url": session.url})

View File

@@ -1,3 +0,0 @@
from django.contrib import admin
# Register your models here.

View File

@@ -1,6 +0,0 @@
from django.apps import AppConfig
class Trading212Config(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'trading212'

View File

@@ -1,3 +0,0 @@
from django.db import models
# Create your models here.

View File

@@ -1,11 +0,0 @@
# thirdparty/trading212/serializers.py
from rest_framework import serializers
class Trading212AccountCashSerializer(serializers.Serializer):
blocked = serializers.FloatField()
free = serializers.FloatField()
invested = serializers.FloatField()
pieCash = serializers.FloatField()
ppl = serializers.FloatField()
result = serializers.FloatField()
total = serializers.FloatField()

View File

@@ -1,3 +0,0 @@
from django.test import TestCase
# Create your tests here.

View File

@@ -1,6 +0,0 @@
from django.urls import path
from .views import YourTrading212View # Replace with actual view class
urlpatterns = [
path('your-endpoint/', YourTrading212View.as_view(), name='trading212-endpoint'),
]

View File

@@ -1,37 +0,0 @@
# thirdparty/trading212/views.py
import os
import requests
from decouple import config
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated
from .serializers import Trading212AccountCashSerializer
from drf_spectacular.utils import extend_schema
class Trading212AccountCashView(APIView):
permission_classes = [IsAuthenticated]
@extend_schema(
summary="Get Trading212 account cash",
responses=Trading212AccountCashSerializer
)
def get(self, request):
api_key = os.getenv("API_KEY_TRADING212")
headers = {
"Authorization": f"Bearer {api_key}",
"Accept": "application/json",
}
url = "https://api.trading212.com/api/v0/equity/account/cash"
try:
resp = requests.get(url, headers=headers, timeout=10)
resp.raise_for_status()
except requests.RequestException as exc:
return Response({"error": str(exc)}, status=400)
data = resp.json()
serializer = Trading212AccountCashSerializer(data=data)
serializer.is_valid(raise_exception=True)
return Response(serializer.data)

View File

@@ -1,3 +0,0 @@
from .celery import app as celery_app
__all__ = ["celery_app"]

View File

@@ -1,8 +0,0 @@
import os
from celery import Celery
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "backend.settings")
app = Celery("backend")
app.config_from_object("django.conf:settings", namespace="CELERY")
app.autodiscover_tasks()

View File

@@ -1,61 +0,0 @@
from django.db import models
from django.utils import timezone
class ActiveManager(models.Manager):
def get_queryset(self):
return super().get_queryset().filter(is_deleted=False)
class AllManager(models.Manager):
def get_queryset(self):
return super().get_queryset()
# How to use custom object Managers: add these fields to your model, to override objects behaviour and all_objects behaviour
# objects = ActiveManager()
# all_objects = AllManager()
class SoftDeleteModel(models.Model):
is_deleted = models.BooleanField(default=False)
deleted_at = models.DateTimeField(null=True, blank=True)
def delete(self, using=None, keep_parents=False):
self.is_deleted = True
self.deleted_at = timezone.now()
self.save()
objects = ActiveManager()
all_objects = AllManager()
class Meta:
abstract = True
def delete(self, *args, **kwargs):
# Soft delete self
self.is_deleted = True
self.deleted_at = timezone.now()
self.save()
def hard_delete(self, using=None, keep_parents=False):
super().delete(using=using, keep_parents=keep_parents)
# SiteSettings model for managing site-wide settings
"""class SiteSettings(models.Model):
bank = models.CharField(max_length=100, blank=True)
support_email = models.EmailField(blank=True)
logo = models.ImageField(upload_to='settings/', blank=True, null=True)
def __str__(self):
return "Site Settings"
class Meta:
verbose_name = "Site Settings"
verbose_name_plural = "Site Settings"
@classmethod
def get_solo(cls):
obj, created = cls.objects.get_or_create(id=1)
return obj
"""

View File

@@ -1,895 +0,0 @@
"""
Django settings for vontor_cz project.
Generated by 'django-admin startproject' using Django 5.1.3.
For more information on this file, see
https://docs.djangoproject.com/en/5.1/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/5.1/ref/settings/
"""
import os
from typing import Dict, Any
from pathlib import Path
from django.core.management.utils import get_random_secret_key
from django.db import OperationalError, connections
from datetime import timedelta
from dotenv import load_dotenv
load_dotenv() # Pouze načte proměnné lokálně, pokud nejsou dostupné
#---------------- ENV VARIABLES USECASE--------------
# v jiné app si to importneš skrz: from django.conf import settings
# a použiješ takto: settings.FRONTEND_URL
FRONTEND_URL = os.getenv("FRONTEND_URL", "http://localhost:3000")
FRONTEND_URL_DEV = os.getenv("FRONTEND_URL_DEV", "http://localhost:5173")
print(f"FRONTEND_URL: {FRONTEND_URL}\nFRONTEND_URL_DEV: {FRONTEND_URL_DEV}\n")
#-------------------------BASE ⚙️------------------------
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
# Pavel
# from django.conf.locale.en import formats as en_formats
DATETIME_INPUT_FORMATS = [
"%Y-%m-%d", # '2025-07-25'
"%Y-%m-%d %H:%M", # '2025-07-25 14:30'
"%Y-%m-%d %H:%M:%S", # '2025-07-25 14:30:59'
"%Y-%m-%dT%H:%M", # '2025-07-25T14:30'
"%Y-%m-%dT%H:%M:%S", # '2025-07-25T14:30:59'
]
LANGUAGE_CODE = 'cs'
TIME_ZONE = 'Europe/Prague'
USE_I18N = True
USE_TZ = True
# SECURITY WARNING: don't run with debug turned on in production!
if os.getenv("DEBUG", "") == "True":
DEBUG = True
else:
DEBUG = False
print(f"\nDEBUG state: {str(DEBUG)}\nDEBUG .env raw: {os.getenv('DEBUG', '')}\n")
#-----------------------BASE END⚙--------------------------
#--------------- URLS 🌐 -------------------
ASGI_APPLICATION = 'vontor_cz.asgi.application' #daphne
ROOT_URLCONF = 'vontor_cz.urls'
LOGIN_URL = '/admin' #nastavení Login adresy
#-----------------------------------------
#----------------------------------- LOGS -------------------------------------------
#slouží pro tisknutí do konzole v dockeru skrz: logger.debug("content")
LOGGING = {
"version": 1,
"disable_existing_loggers": False,
"handlers": {
"console": {
"class": "logging.StreamHandler",
"formatter": "verbose",
},
},
"formatters": {
"verbose": {
"format": "{levelname} {asctime} {name}: {message}",
"style": "{",
},
},
"root": {
"handlers": ["console"],
"level": "DEBUG" if DEBUG else "INFO",
},
}
"""
import logging
# Vytvoř si logger podle názvu souboru (modulu)
logger = logging.getLogger(__name__)
logger.debug("Ladicí zpráva vidíš jen když je DEBUG = True")
logger.info("Informace např. že uživatel klikl na tlačítko")
logger.warning("Varování něco nečekaného, ale ne kritického")
logger.error("Chyba něco se pokazilo, ale aplikace jede dál")
logger.critical("Kritická chyba selhání systému, třeba pád služby")
"""
#---------------------------------- END LOGS ---------------------------------------
#-------------------------------------SECURITY 🔐------------------------------------
if DEBUG:
SECRET_KEY = 'pernament'
else:
SECRET_KEY = os.getenv("DJANGO_SECRET_KEY", get_random_secret_key())
SESSION_COOKIE_AGE = 86400 # one day
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
AUTHENTICATION_BACKENDS = [
#'vontor_cz.backend.EmailOrUsernameModelBackend', #custom backend z authentication aplikace
'django.contrib.auth.backends.ModelBackend',
]
#--------------------------------END SECURITY 🔐-------------------------------------
#-------------------------------------CORS + HOSTs 🌐🔐------------------------------------
ALLOWED_HOSTS = ["*"]
CSRF_TRUSTED_ORIGINS = [
'https://domena.cz',
"https://www.domena.cz",
"http://localhost:3000", #react docker
"http://localhost:5173" #react dev
]
if DEBUG:
CORS_ALLOWED_ORIGINS = [
"http://localhost:5173",
"http://localhost:3000",
]
else:
CORS_ALLOWED_ORIGINS = [
"https://www.domena.cz",
]
CORS_ALLOW_CREDENTIALS = True
CORS_ALLOW_ALL_ORIGINS = False # Tohle musí být false, když používáš credentials
print("CORS_ALLOWED_ORIGINS =", CORS_ALLOWED_ORIGINS)
print("CSRF_TRUSTED_ORIGINS =", CSRF_TRUSTED_ORIGINS)
print("ALLOWED_HOSTS =", ALLOWED_HOSTS)
#--------------------------------END CORS + HOSTs 🌐🔐---------------------------------
#--------------------------------------SSL 🧾------------------------------------
if os.getenv("SSL", "") == "True":
USE_SSL = True
else:
USE_SSL = False
if USE_SSL is True:
print("SSL turned on!")
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
SECURE_SSL_REDIRECT = True
SECURE_BROWSER_XSS_FILTER = True
SECURE_CONTENT_TYPE_NOSNIFF = True
USE_X_FORWARDED_HOST = True
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
else:
SESSION_COOKIE_SECURE = False
CSRF_COOKIE_SECURE = False
SECURE_SSL_REDIRECT = False
SECURE_BROWSER_XSS_FILTER = False
SECURE_CONTENT_TYPE_NOSNIFF = False
USE_X_FORWARDED_HOST = False
print(f"\nUsing SSL: {USE_SSL}\n")
#--------------------------------END-SSL 🧾---------------------------------
#-------------------------------------REST FRAMEWORK 🛠️------------------------------------
# ⬇️ Základní lifetime konfigurace
ACCESS_TOKEN_LIFETIME = timedelta(minutes=15)
REFRESH_TOKEN_LIFETIME = timedelta(days=1)
# ⬇️ Nastavení SIMPLE_JWT podle režimu
if DEBUG:
SIMPLE_JWT = {
"ACCESS_TOKEN_LIFETIME": ACCESS_TOKEN_LIFETIME,
"REFRESH_TOKEN_LIFETIME": REFRESH_TOKEN_LIFETIME,
"AUTH_COOKIE": "access_token",
"AUTH_COOKIE_SECURE": False, # není HTTPS
"AUTH_COOKIE_HTTP_ONLY": True,
"AUTH_COOKIE_PATH": "/",
"AUTH_COOKIE_SAMESITE": "Lax", # není cross-site
"ROTATE_REFRESH_TOKENS": True,
"BLACKLIST_AFTER_ROTATION": True,
}
else:
SIMPLE_JWT = {
"ACCESS_TOKEN_LIFETIME": ACCESS_TOKEN_LIFETIME,
"REFRESH_TOKEN_LIFETIME": REFRESH_TOKEN_LIFETIME,
"AUTH_COOKIE": "access_token",
"AUTH_COOKIE_SECURE": True, # HTTPS only
"AUTH_COOKIE_HTTP_ONLY": True,
"AUTH_COOKIE_PATH": "/",
"AUTH_COOKIE_SAMESITE": "None", # potřebné pro cross-origin
"ROTATE_REFRESH_TOKENS": True,
"BLACKLIST_AFTER_ROTATION": True,
}
REST_FRAMEWORK = {
"DATETIME_FORMAT": "%Y-%m-%d %H:%M", # Pavel
'DEFAULT_AUTHENTICATION_CLASSES': (
'account.tokens.CookieJWTAuthentication',
'rest_framework.authentication.SessionAuthentication',
),
'DEFAULT_PERMISSION_CLASSES': (
'rest_framework.permissions.AllowAny',
),
'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema',
'DEFAULT_FILTER_BACKENDS': ['django_filters.rest_framework.DjangoFilterBackend'],
}
#--------------------------------END REST FRAMEWORK 🛠️-------------------------------------
#-------------------------------------APPS 📦------------------------------------
MY_CREATED_APPS = [
'account',
]
INSTALLED_APPS = [
'daphne', #asgi bude fungovat lokálně (musí být na začátku)
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'corsheaders', #cors
'django_celery_beat', #slouží k plánování úkolů pro Celery
#'chat.apps.GlobalChatCheck', #tohle se spusti při každé django inicializaci (migration, createmigration, runserver)
#'authentication',
'storages',# Adds support for external storage services like Amazon S3 via django-storages
'django_filters',
'channels' ,# django channels
'rest_framework',
'rest_framework_api_key',
'drf_spectacular', #rest framework, grafické zobrazení
#Nastavení stránky
#'constance',
#'constance.backends.database',
'django.contrib.sitemaps',
'tinymce',
#kvůli bugum je lepší to dát na poslední místo v INSTALLED_APPS
'django_cleanup.apps.CleanupConfig', #app která maže nepoužité soubory(media) z databáze na S3
]
#skládaní dohromady INSTALLED_APPS
INSTALLED_APPS = INSTALLED_APPS[:-1] + MY_CREATED_APPS + INSTALLED_APPS[-1:]
# -------------------------------------END APPS 📦------------------------------------
#-------------------------------------MIDDLEWARE 🧩------------------------------------
# Middleware is a framework of hooks into Django's request/response processing.
MIDDLEWARE = [
# Middleware that allows your backend to accept requests from other domains (CORS)
"corsheaders.middleware.CorsMiddleware",
"django.middleware.common.CommonMiddleware",
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
#CUSTOM
#'tools.middleware.CustomMaxUploadSizeMiddleware',
'whitenoise.middleware.WhiteNoiseMiddleware',# díky tomu funguje načítaní static files
]
#--------------------------------END MIDDLEWARE 🧩---------------------------------
#-------------------------------------CACHE + CHANNELS(ws) 📡🗄️------------------------------------
# Caching settings for Redis (using Docker's internal network name for Redis)
if DEBUG is False:
#PRODUCTION
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.redis.RedisCache',
'LOCATION': 'redis://redis:6379/0', # Using the service name `redis` from Docker Compose
'OPTIONS': {
'CLIENT_CLASS': 'django_redis.client.DefaultClient',
'PASSWORD': os.getenv('REDIS_PASSWORD'), # Make sure to set REDIS_PASSWORD in your environment
},
}
}
# WebSockets Channel Layers (using Redis in production)
CHANNEL_LAYERS = {
'default': {
'BACKEND': 'channels_redis.core.RedisChannelLayer',
'CONFIG': {
'hosts': [('redis', 6379)], # Use `redis` service in Docker Compose
},
}
}
else:
#DEVELOPMENT
# Use in-memory channel layer for development (when DEBUG is True)
CHANNEL_LAYERS = {
'default': {
'BACKEND': 'channels.layers.InMemoryChannelLayer',
}
}
# Use in-memory cache for development (when DEBUG is True)
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
}
}
#--------------------------------END CACHE + CHANNELS(ws) 📡🗄️---------------------------------
#-------------------------------------CELERY 📅------------------------------------
# CELERY_BROKER_URL = 'redis://localhost:6379/0'
CELERY_BROKER_URL = os.getenv("CELERY_BROKER_URL")
CELERY_RESULT_BACKEND = os.getenv("CELERY_RESULT_BACKEND")
try:
import redis
# test connection
r = redis.Redis(host='localhost', port=6379, db=0)
r.ping()
except Exception:
CELERY_BROKER_URL = 'memory://'
CELERY_ACCEPT_CONTENT = os.getenv("CELERY_ACCEPT_CONTENT")
CELERY_TASK_SERIALIZER = os.getenv("CELERY_TASK_SERIALIZER")
CELERY_TIMEZONE = os.getenv("CELERY_TIMEZONE")
CELERY_BEAT_SCHEDULER = os.getenv("CELERY_BEAT_SCHEDULER")
# if DEBUG:
# CELERY_BROKER_URL = 'redis://localhost:6379/0'
# try:
# import redis
# # test connection
# r = redis.Redis(host='localhost', port=6379, db=0)
# r.ping()
# except Exception:
# CELERY_BROKER_URL = 'memory://'
# CELERY_ACCEPT_CONTENT = ['json']
# CELERY_TASK_SERIALIZER = 'json'
# CELERY_TIMEZONE = 'Europe/Prague'
# CELERY_BEAT_SCHEDULER = 'django_celery_beat.schedulers:DatabaseScheduler'
# from celery.schedules import crontab
# CELERY_BEAT_SCHEDULE = {
# 'hard_delete_soft_deleted_monthly': {
# 'task': 'vontor_cz.tasks.hard_delete_soft_deleted_records',
# 'schedule': crontab(minute=0, hour=0, day_of_month=1), # každý první den v měsíci o půlnoci
# },
# 'delete_old_reservations_monthly': {
# 'task': 'account.tasks.delete_old_reservations',
# 'schedule': crontab(minute=0, hour=1, day_of_month=1), # každý první den v měsíci v 1:00 ráno
# },
# }
# else:
# # Nebo nastav dummy broker, aby se úlohy neodesílaly
# CELERY_BROKER_URL = 'memory://' # broker v paměti, pro testování bez Redis
#-------------------------------------END CELERY 📅------------------------------------
#-------------------------------------DATABASE 💾------------------------------------
# Nastavuje výchozí typ primárního klíče pro modely.
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
# říka že se úkladá do databáze, místo do cookie
SESSION_ENGINE = 'django.contrib.sessions.backends.db'
USE_PRODUCTION_DB = os.getenv("USE_PRODUCTION_DB", "False") == "True"
if USE_PRODUCTION_DB is False:
# DEVELOPMENT
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3', # Database engine
'NAME': BASE_DIR / 'db.sqlite3', # Path to the SQLite database file
}
}
else:
#PRODUCTION
DATABASES = {
'default': {
'ENGINE': os.getenv('DATABASE_ENGINE'),
'NAME': os.getenv('DATABASE_NAME'),
'USER': os.getenv('DATABASE_USER'),
'PASSWORD': os.getenv('DATABASE_PASSWORD'),
'HOST': os.getenv('DATABASE_HOST', "localhost"),
'PORT': os.getenv('DATABASE_PORT'),
}
}
AUTH_USER_MODEL = 'account.CustomUser' #class CustomUser(AbstractUser) best practice to use AbstractUser
#--------------------------------END DATABASE 💾---------------------------------
#--------------------------------------PAGE SETTINGS -------------------------------------
CONSTANCE_BACKEND = 'constance.backends.database.DatabaseBackend'
# Configuration for Constance(variables)
CONSTANCE_CONFIG = {
'BITCOIN_WALLET': ('', 'Public BTC wallet address'),
'SUPPORT_EMAIL': ('admin@example.com', 'Support email'),
}
#--------------------------------------EMAIL 📧--------------------------------------
if DEBUG:
# DEVELOPMENT
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' # Use console backend for development
# EMAILY SE BUDOU POSÍLAT DO KONZOLE!!!
else:
# PRODUCTION
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_HOST = os.getenv("EMAIL_HOST_DEV")
EMAIL_PORT = int(os.getenv("EMAIL_PORT_DEV", 465))
EMAIL_USE_TLS = True # ❌ Keep this OFF when using SSL
EMAIL_USE_SSL = False # ✅ Must be True for port 465
EMAIL_HOST_USER = os.getenv("EMAIL_USER_DEV")
EMAIL_HOST_PASSWORD = os.getenv("EMAIL_USER_PASSWORD_DEV")
DEFAULT_FROM_EMAIL = EMAIL_HOST_USER
EMAIL_TIMEOUT = 10
print("---------EMAIL----------\nEMAIL_HOST =", os.getenv("EMAIL_HOST_DEV"))
print("EMAIL_PORT =", os.getenv("EMAIL_PORT_DEV"))
print("EMAIL_USER =", os.getenv("EMAIL_USER_DEV"))
print("EMAIL_USER_PASSWORD =", os.getenv("EMAIL_USER_PASSWORD_DEV"), "\n------------------------")
#----------------------------------EMAIL END 📧-------------------------------------
#-------------------------------------TEMPLATES 🗂️------------------------------------
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
"DIRS": [BASE_DIR / 'templates'],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
#--------------------------------END TEMPLATES 🗂️---------------------------------
#-------------------------------------MEDIA + STATIC 🖼️, AWS ☁️------------------------------------
# nastavení složky pro globalstaticfiles (static složky django hledá samo)
STATICFILES_DIRS = [
BASE_DIR / 'globalstaticfiles',
]
if os.getenv("USE_AWS", "") == "True":
USE_AWS = True
else:
USE_AWS = False
print(f"\n-------------- USE_AWS: {USE_AWS} --------------")
if USE_AWS is False:
# DEVELOPMENT
# Development: Use local file system storage for static files
STORAGES = {
"default": {
"BACKEND": "django.core.files.storage.FileSystemStorage",
},
"staticfiles": {
"BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage",
},
}
# Media and Static URL for local dev
MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
STATIC_URL = '/static/'
# Local folder for collected static files
STATIC_ROOT = BASE_DIR / 'collectedstaticfiles'
elif USE_AWS:
# PRODUCTION
AWS_LOCATION = "static"
# Production: Use S3 storage
STORAGES = {
"default": {
"BACKEND" : "storages.backends.s3boto3.S3StaticStorage",
},
"staticfiles": {
"BACKEND" : "storages.backends.s3boto3.S3StaticStorage",
},
}
# Media and Static URL for AWS S3
MEDIA_URL = f'https://{os.getenv("AWS_STORAGE_BUCKET_NAME")}.s3.amazonaws.com/media/'
STATIC_URL = f'https://{os.getenv("AWS_STORAGE_BUCKET_NAME")}.s3.amazonaws.com/static/'
CSRF_TRUSTED_ORIGINS.append(STATIC_URL)
# Static files should be collected to a local directory and then uploaded to S3
STATIC_ROOT = BASE_DIR / 'collectedstaticfiles'
AWS_ACCESS_KEY_ID = os.getenv('AWS_ACCESS_KEY_ID')
AWS_SECRET_ACCESS_KEY = os.getenv('AWS_SECRET_ACCESS_KEY')
AWS_STORAGE_BUCKET_NAME = os.getenv('AWS_STORAGE_BUCKET_NAME')
AWS_S3_REGION_NAME = os.getenv('AWS_S3_REGION_NAME', 'us-east-1') # Default to 'us-east-1' if not set
AWS_S3_SIGNATURE_VERSION = 's3v4' # Use AWS Signature Version 4
AWS_S3_USE_SSL = True
AWS_S3_FILE_OVERWRITE = True
AWS_DEFAULT_ACL = None # Set to None to avoid setting a default ACL
print(f"Static url: {STATIC_URL}\nStatic storage: {STORAGES}\n----------------------------")
#--------------------------------END: MEDIA + STATIC 🖼️, AWS ☁️---------------------------------
#-------------------------------------TINY MCE ✍️------------------------------------
TINYMCE_JS_URL = 'https://cdn.tiny.cloud/1/no-api-key/tinymce/7/tinymce.min.js'
TINYMCE_DEFAULT_CONFIG = {
"height": "320px",
"width": "960px",
"menubar": "file edit view insert format tools table help",
"plugins": "advlist autolink lists link image charmap print preview anchor searchreplace visualblocks code "
"fullscreen insertdatetime media table paste code help wordcount spellchecker",
"toolbar": "undo redo | bold italic underline strikethrough | fontselect fontsizeselect formatselect | alignleft "
"aligncenter alignright alignjustify | outdent indent | numlist bullist checklist | forecolor "
"backcolor casechange permanentpen formatpainter removeformat | pagebreak | charmap emoticons | "
"fullscreen preview save print | insertfile image media pageembed template link anchor codesample | "
"a11ycheck ltr rtl | showcomments addcomment code",
"custom_undo_redo_levels": 10,
}
TINYMCE_SPELLCHECKER = True
TINYMCE_COMPRESSOR = True
#--------------------------------END-TINY-MCE-SECTION ✍️---------------------------------
#-------------------------------------DRF SPECTACULAR 📊------------------------------------
SPECTACULAR_DEFAULTS: Dict[str, Any] = {
# A regex specifying the common denominator for all operation paths. If
# SCHEMA_PATH_PREFIX is set to None, drf-spectacular will attempt to estimate
# a common prefix. Use '' to disable.
# Mainly used for tag extraction, where paths like '/api/v1/albums' with
# a SCHEMA_PATH_PREFIX regex '/api/v[0-9]' would yield the tag 'albums'.
'SCHEMA_PATH_PREFIX': None,
# Remove matching SCHEMA_PATH_PREFIX from operation path. Usually used in
# conjunction with appended prefixes in SERVERS.
'SCHEMA_PATH_PREFIX_TRIM': False,
# Insert a manual path prefix to the operation path, e.g. '/service/backend'.
# Use this for example to align paths when the API is mounted as a sub-resource
# behind a proxy and Django is not aware of that. Alternatively, prefixes can
# also specified via SERVERS, but this makes the operation path more explicit.
'SCHEMA_PATH_PREFIX_INSERT': '',
# Coercion of {pk} to {id} is controlled by SCHEMA_COERCE_PATH_PK. Additionally,
# some libraries (e.g. drf-nested-routers) use "_pk" suffixed path variables.
# This setting globally coerces path variables like "{user_pk}" to "{user_id}".
'SCHEMA_COERCE_PATH_PK_SUFFIX': False,
# Schema generation parameters to influence how components are constructed.
# Some schema features might not translate well to your target.
# Demultiplexing/modifying components might help alleviate those issues.
'DEFAULT_GENERATOR_CLASS': 'drf_spectacular.generators.SchemaGenerator',
# Create separate components for PATCH endpoints (without required list)
'COMPONENT_SPLIT_PATCH': True,
# Split components into request and response parts where appropriate
# This setting is highly recommended to achieve the most accurate API
# description, however it comes at the cost of having more components.
'COMPONENT_SPLIT_REQUEST': True,
# Aid client generator targets that have trouble with read-only properties.
'COMPONENT_NO_READ_ONLY_REQUIRED': False,
# Adds "minLength: 1" to fields that do not allow blank strings. Deactivated
# by default because serializers do not strictly enforce this on responses and
# so "minLength: 1" may not always accurately describe API behavior.
# Gets implicitly enabled by COMPONENT_SPLIT_REQUEST, because this can be
# accurately modeled when request and response components are separated.
'ENFORCE_NON_BLANK_FIELDS': False,
# This version string will end up the in schema header. The default OpenAPI
# version is 3.0.3, which is heavily tested. We now also support 3.1.0,
# which contains the same features and a few mandatory, but minor changes.
'OAS_VERSION': '3.0.3',
# Configuration for serving a schema subset with SpectacularAPIView
'SERVE_URLCONF': None,
# complete public schema or a subset based on the requesting user
'SERVE_PUBLIC': True,
# include schema endpoint into schema
'SERVE_INCLUDE_SCHEMA': True,
# list of authentication/permission classes for spectacular's views.
'SERVE_PERMISSIONS': ['rest_framework.permissions.AllowAny'], #account.permissions.AdminOnly
# None will default to DRF's AUTHENTICATION_CLASSES
'SERVE_AUTHENTICATION': None,
# Dictionary of general configuration to pass to the SwaggerUI({ ... })
# https://swagger.io/docs/open-source-tools/swagger-ui/usage/configuration/
# The settings are serialized with json.dumps(). If you need customized JS, use a
# string instead. The string must then contain valid JS and is passed unchanged.
'SWAGGER_UI_SETTINGS': {
'deepLinking': True,
},
# Initialize SwaggerUI with additional OAuth2 configuration.
# https://swagger.io/docs/open-source-tools/swagger-ui/usage/oauth2/
'SWAGGER_UI_OAUTH2_CONFIG': {},
# Dictionary of general configuration to pass to the Redoc.init({ ... })
# https://redocly.com/docs/redoc/config/#functional-settings
# The settings are serialized with json.dumps(). If you need customized JS, use a
# string instead. The string must then contain valid JS and is passed unchanged.
'REDOC_UI_SETTINGS': {},
# CDNs for swagger and redoc. You can change the version or even host your
# own depending on your requirements. For self-hosting, have a look at
# the sidecar option in the README.
'SWAGGER_UI_DIST': 'https://cdn.jsdelivr.net/npm/swagger-ui-dist@latest',
'SWAGGER_UI_FAVICON_HREF': 'https://cdn.jsdelivr.net/npm/swagger-ui-dist@latest/favicon-32x32.png',
'REDOC_DIST': 'https://cdn.jsdelivr.net/npm/redoc@latest',
# Append OpenAPI objects to path and components in addition to the generated objects
'APPEND_PATHS': {},
'APPEND_COMPONENTS': {},
# Postprocessing functions that run at the end of schema generation.
# must satisfy interface result = hook(generator, request, public, result)
'POSTPROCESSING_HOOKS': [
'drf_spectacular.hooks.postprocess_schema_enums'
],
# Preprocessing functions that run before schema generation.
# must satisfy interface result = hook(endpoints=result) where result
# is a list of Tuples (path, path_regex, method, callback).
# Example: 'drf_spectacular.hooks.preprocess_exclude_path_format'
'PREPROCESSING_HOOKS': [],
# Determines how operations should be sorted. If you intend to do sorting with a
# PREPROCESSING_HOOKS, be sure to disable this setting. If configured, the sorting
# is applied after the PREPROCESSING_HOOKS. Accepts either
# True (drf-spectacular's alpha-sorter), False, or a callable for sort's key arg.
'SORT_OPERATIONS': True,
# enum name overrides. dict with keys "YourEnum" and their choice values "field.choices"
# e.g. {'SomeEnum': ['A', 'B'], 'OtherEnum': 'import.path.to.choices'}
'ENUM_NAME_OVERRIDES': {},
# Adds "blank" and "null" enum choices where appropriate. disable on client generation issues
'ENUM_ADD_EXPLICIT_BLANK_NULL_CHOICE': True,
# Add/Append a list of (``choice value`` - choice name) to the enum description string.
'ENUM_GENERATE_CHOICE_DESCRIPTION': True,
# Optional suffix for generated enum.
# e.g. {'ENUM_SUFFIX': "Type"} would produce an enum name 'StatusType'.
'ENUM_SUFFIX': 'Enum',
# function that returns a list of all classes that should be excluded from doc string extraction
'GET_LIB_DOC_EXCLUDES': 'drf_spectacular.plumbing.get_lib_doc_excludes',
# Function that returns a mocked request for view processing. For CLI usage
# original_request will be None.
# interface: request = build_mock_request(method, path, view, original_request, **kwargs)
'GET_MOCK_REQUEST': 'drf_spectacular.plumbing.build_mock_request',
# Camelize names like "operationId" and path parameter names
# Camelization of the operation schema itself requires the addition of
# 'drf_spectacular.contrib.djangorestframework_camel_case.camelize_serializer_fields'
# to POSTPROCESSING_HOOKS. Please note that the hook depends on
# ``djangorestframework_camel_case``, while CAMELIZE_NAMES itself does not.
'CAMELIZE_NAMES': False,
# Changes the location of the action/method on the generated OperationId. For example,
# "POST": "group_person_list", "group_person_create"
# "PRE": "list_group_person", "create_group_person"
'OPERATION_ID_METHOD_POSITION': 'POST',
# Determines if and how free-form 'additionalProperties' should be emitted in the schema. Some
# code generator targets are sensitive to this. None disables generic 'additionalProperties'.
# allowed values are 'dict', 'bool', None
'GENERIC_ADDITIONAL_PROPERTIES': 'dict',
# Path converter schema overrides (e.g. <int:foo>). Can be used to either modify default
# behavior or provide a schema for custom converters registered with register_converter(...).
# Takes converter labels as keys and either basic python types, OpenApiType, or raw schemas
# as values. Example: {'aint': OpenApiTypes.INT, 'bint': str, 'cint': {'type': ...}}
'PATH_CONVERTER_OVERRIDES': {},
# Determines whether operation parameters should be sorted alphanumerically or just in
# the order they arrived. Accepts either True, False, or a callable for sort's key arg.
'SORT_OPERATION_PARAMETERS': True,
# @extend_schema allows to specify status codes besides 200. This functionality is usually used
# to describe error responses, which rarely make use of list mechanics. Therefore, we suppress
# listing (pagination and filtering) on non-2XX status codes by default. Toggle this to enable
# list responses with ListSerializers/many=True irrespective of the status code.
'ENABLE_LIST_MECHANICS_ON_NON_2XX': False,
# This setting allows you to deviate from the default manager by accessing a different model
# property. We use "objects" by default for compatibility reasons. Using "_default_manager"
# will likely fix most issues, though you are free to choose any name.
"DEFAULT_QUERY_MANAGER": 'objects',
# Controls which authentication methods are exposed in the schema. If not None, will hide
# authentication classes that are not contained in the whitelist. Use full import paths
# like ['rest_framework.authentication.TokenAuthentication', ...].
# Empty list ([]) will hide all authentication methods. The default None will show all.
'AUTHENTICATION_WHITELIST': None,
# Controls which parsers are exposed in the schema. Works analog to AUTHENTICATION_WHITELIST.
# List of allowed parsers or None to allow all.
'PARSER_WHITELIST': None,
# Controls which renderers are exposed in the schema. Works analog to AUTHENTICATION_WHITELIST.
# rest_framework.renderers.BrowsableAPIRenderer is ignored by default if whitelist is None
'RENDERER_WHITELIST': None,
# Option for turning off error and warn messages
'DISABLE_ERRORS_AND_WARNINGS': False,
# Runs exemplary schema generation and emits warnings as part of "./manage.py check --deploy"
'ENABLE_DJANGO_DEPLOY_CHECK': True,
# General schema metadata. Refer to spec for valid inputs
# https://spec.openapis.org/oas/v3.0.3#openapi-object
'TITLE': 'e-Tržnice API',
'DESCRIPTION': 'This is the API documentation for e-Tržnice.',
'TOS': None,
# Optional: MAY contain "name", "url", "email"
'CONTACT': {},
# Optional: MUST contain "name", MAY contain URL
'LICENSE': {},
# Statically set schema version. May also be an empty string. When used together with
# view versioning, will become '0.0.0 (v2)' for 'v2' versioned requests.
# Set VERSION to None if only the request version should be rendered.
'VERSION': '1.0.0',
# Optional list of servers.
# Each entry MUST contain "url", MAY contain "description", "variables"
# e.g. [{'url': 'https://example.com/v1', 'description': 'Text'}, ...]
'SERVERS': [],
# Tags defined in the global scope
'TAGS': [],
# Optional: List of OpenAPI 3.1 webhooks. Each entry should be an import path to an
# OpenApiWebhook instance.
'WEBHOOKS': [],
# Optional: MUST contain 'url', may contain "description"
'EXTERNAL_DOCS': {},
# Arbitrary specification extensions attached to the schema's info object.
# https://swagger.io/specification/#specification-extensions
'EXTENSIONS_INFO': {},
# Arbitrary specification extensions attached to the schema's root object.
# https://swagger.io/specification/#specification-extensions
'EXTENSIONS_ROOT': {},
# Oauth2 related settings. used for example by django-oauth2-toolkit.
# https://spec.openapis.org/oas/v3.0.3#oauth-flows-object
'OAUTH2_FLOWS': [],
'OAUTH2_AUTHORIZATION_URL': None,
'OAUTH2_TOKEN_URL': None,
'OAUTH2_REFRESH_URL': None,
'OAUTH2_SCOPES': None,
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Some files were not shown because too many files have changed in this diff Show More