This commit is contained in:
2025-10-02 00:54:34 +02:00
commit 84b34c9615
200 changed files with 42048 additions and 0 deletions

View File

@@ -0,0 +1,3 @@
from .celery import app as celery_app
__all__ = ('celery_app',)

48
backend/trznice/admin.py Normal file
View File

@@ -0,0 +1,48 @@
from django.contrib.admin import AdminSite
from django.contrib import admin
from django_celery_beat.models import PeriodicTask, IntervalSchedule, CrontabSchedule, SolarSchedule, ClockedSchedule
class RoleBasedAdminSite(AdminSite):
site_header = "Tržiště Admin"
site_title = "Tržiště Admin"
index_title = "Přehled"
def get_app_list(self, request):
app_list = super().get_app_list(request)
if not hasattr(request.user, "role"):
return []
role = request.user.role
# define allowed models per role
role_model_access = {
"squareManager": ["Square", "Event", "MarketSlot", "Product", "EventProduct"],
"cityClerk": ["CustomUser", "Event", "MarketSlot", "Reservation", "Product", "EventProduct", "ServiceTicket"],
# admin will see everything
}
# only restrict if user has limited access
if role in role_model_access:
allowed = role_model_access[role]
for app in app_list:
app["models"] = [
model for model in app["models"]
if model["object_name"] in allowed
]
return app_list
# Initialize the custom admin site
custom_admin_site = RoleBasedAdminSite(name='custom_admin')
# # Register your models to the custom admin site
custom_admin_site.register(PeriodicTask)
custom_admin_site.register(IntervalSchedule)
custom_admin_site.register(CrontabSchedule)
custom_admin_site.register(SolarSchedule)
custom_admin_site.register(ClockedSchedule)

16
backend/trznice/asgi.py Normal file
View File

@@ -0,0 +1,16 @@
"""
ASGI config for trznice project.
It exposes the ASGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/5.2/howto/deployment/asgi/
"""
import os
from django.core.asgi import get_asgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'trznice.settings')
application = get_asgi_application()

18
backend/trznice/celery.py Normal file
View File

@@ -0,0 +1,18 @@
import os
from celery import Celery
from django.conf import settings
# Nastav environment variable pro Django settings modul
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'trznice.settings')
app = Celery('trznice')
# Načti konfiguraci z Django settings (prefix "CELERY_")
app.config_from_object('django.conf:settings', namespace='CELERY')
# Automaticky najdi tasks.py ve všech appkách
# app.autodiscover_tasks()
app.autodiscover_tasks(lambda: settings.INSTALLED_APPS)
# Optional but recommended for beat to use DB scheduler
# from django_celery_beat.schedulers import DatabaseScheduler

61
backend/trznice/models.py Normal file
View File

@@ -0,0 +1,61 @@
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
"""

951
backend/trznice/settings.py Normal file
View File

@@ -0,0 +1,951 @@
"""
Django settings for trznice 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:5173")
#-------------------------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 = 'trznice.asgi.application' #daphne
ROOT_URLCONF = 'trznice.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())
# Honor reverse proxy host/port even without SSL
USE_X_FORWARDED_HOST = True
# Optionally honor proto if you terminate SSL at proxy
# SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
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 = [
#'trznice.backend.EmailOrUsernameModelBackend', #custom backend z authentication aplikace
'django.contrib.auth.backends.ModelBackend',
]
#--------------------------------END SECURITY 🔐-------------------------------------
#-------------------------------------CORS + HOSTs 🌐🔐------------------------------------
ALLOWED_HOSTS = ["*"]
from urllib.parse import urlparse
parsed = urlparse(FRONTEND_URL)
CSRF_TRUSTED_ORIGINS = [
f"{parsed.scheme}://{parsed.hostname}:{parsed.port or (443 if parsed.scheme=='https' else 80)}",
"http://192.168.67.98",
"https://itsolutions.vontor.cz",
"https://react.vontor.cz",
"http://localhost:5173",
"http://localhost:3000",
"http://127.0.0.1:5173",
"http://127.0.0.1:3000",
#server
"http://192.168.67.98",
"https://itsolutions.vontor.cz",
"https://react.vontor.cz",
#nginx docker (local)
"http://localhost",
"http://localhost:80",
"http://127.0.0.1",
]
if DEBUG:
CORS_ALLOWED_ORIGINS = [
f"{parsed.scheme}://{parsed.hostname}:{parsed.port or (443 if parsed.scheme=='https' else 80)}",
"http://localhost:5173",
"http://localhost:3000",
"http://127.0.0.1:5173",
"http://127.0.0.1:3000",
#server
"http://192.168.67.98",
"https://itsolutions.vontor.cz",
"https://react.vontor.cz",
#nginx docker (local)
"http://localhost",
"http://localhost:80",
"http://127.0.0.1",
]
else:
CORS_ALLOWED_ORIGINS = [
"http://192.168.67.98",
"https://itsolutions.vontor.cz",
"https://react.vontor.cz",
]
CORS_ALLOW_CREDENTIALS = True
CORS_ALLOW_ALL_ORIGINS = False # Tohle musí být false, když používáš credentials
SESSION_COOKIE_SAMESITE = None
CSRF_COOKIE_SAMESITE = None
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 stays True (set above)
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 stays True (set above)
print(f"\nUsing SSL: {USE_SSL}\n")
#--------------------------------END-SSL 🧾---------------------------------
#-------------------------------------REST FRAMEWORK 🛠️------------------------------------
# ⬇️ Základní lifetime konfigurace
ACCESS_TOKEN_LIFETIME = timedelta(minutes=60)
REFRESH_TOKEN_LIFETIME = timedelta(days=5)
# ⬇️ 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_REFRESH": "refresh_token",
"AUTH_COOKIE_DOMAIN": None,
"AUTH_COOKIE_SECURE": False,
"AUTH_COOKIE_HTTP_ONLY": True,
"ROTATE_REFRESH_TOKENS": False,
"BLACKLIST_AFTER_ROTATION": False,
"AUTH_COOKIE_PATH": "/",
"AUTH_COOKIE_SAMESITE": "Lax", # change to "None" only if you serve via HTTPS; keep Lax if using same-origin
# ...existing code...
}
else:
SIMPLE_JWT = {
"ACCESS_TOKEN_LIFETIME": ACCESS_TOKEN_LIFETIME,
"REFRESH_TOKEN_LIFETIME": REFRESH_TOKEN_LIFETIME,
"AUTH_COOKIE": "access_token",
"AUTH_COOKIE_REFRESH": "refresh_token", # ensure refresh cookie is recognized/used
"AUTH_COOKIE_DOMAIN": None,
"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",
'DEFAULT_AUTHENTICATION_CLASSES': (
'account.tokens.CookieJWTAuthentication',
'rest_framework_simplejwt.authentication.JWTAuthentication', # <-- allow Bearer Authorization
'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',
'booking',
'product',
'servicedesk',
'commerce',
'configuration',
]
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',
'rest_framework_simplejwt.token_blacklist',
'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': 'trznice.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_DOCKER_DB = os.getenv("USE_DOCKER_DB", "False") in ["True", "true", "1", True]
if USE_DOCKER_DB is False:
# DEV
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3', # Database engine
'NAME': BASE_DIR / 'db.sqlite3', # Path to the SQLite database file
}
}
else:
#DOCKER
DATABASES = {
'default': {
'ENGINE': os.getenv('DATABASE_ENGINE'),
'NAME': os.getenv('POSTGRES_DB'),
'USER': os.getenv('POSTGRES_USER'),
'PASSWORD': os.getenv('POSTGRES_PASSWORD'),
'HOST': os.getenv('DATABASE_HOST'),
'PORT': os.getenv('DATABASE_PORT'),
}
}
print(f"\nUsing Docker DB: {USE_DOCKER_DB}\nDatabase settings: {DATABASES}\n")
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 = os.getenv("MEDIA_URL", "/media/") # URL prefix for media files
MEDIA_ROOT = os.path.join(BASE_DIR, 'media') # Local folder for user-uploaded files
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,
}

59
backend/trznice/urls.py Normal file
View File

@@ -0,0 +1,59 @@
"""
URL configuration for trznice project.
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/5.2/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: path('', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import path, include
from django.contrib.auth import views as auth_views
from django.conf.urls.static import static
from django.conf import settings
from rest_framework import permissions
from . import views
from drf_spectacular.views import (
SpectacularAPIView,
SpectacularSwaggerView,
SpectacularRedocView,
)
from .admin import custom_admin_site
urlpatterns = [
path('login/', auth_views.LoginView.as_view(), name='login'), # pro Swagger
path('logout/', auth_views.LogoutView.as_view(), name='logout'),
# path('admin/', admin.site.urls),
path("admin/", custom_admin_site.urls), # override default admin
path('api/account/', include('account.urls')),
path('api/booking/', include('booking.urls')),
path('api/', include('product.urls')),
path('api/service-tickets/', include('servicedesk.urls')),
path('api/commerce/', include('commerce.urls')),
path('api/config/', include('configuration.urls')),
#rest framework, map of api
path("api/schema/", SpectacularAPIView.as_view(), name="schema"),
path("swagger/", SpectacularSwaggerView.as_view(url_name="schema"), name="swagger-ui"),
path("redoc/", SpectacularRedocView.as_view(url_name="schema"), name="redoc"),
path('', views.index, name='index'),
path('test/email', views.test_mail, name='test-email')
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

12
backend/trznice/utils.py Normal file
View File

@@ -0,0 +1,12 @@
from rest_framework.fields import DateTimeField
from datetime import datetime
def truncate_to_minutes(dt: datetime) -> datetime:
return dt.replace(second=0, microsecond=0)
class RoundedDateTimeField(DateTimeField):
def to_internal_value(self, value):
dt = super().to_internal_value(value)
return truncate_to_minutes(dt)

38
backend/trznice/views.py Normal file
View File

@@ -0,0 +1,38 @@
from django.shortcuts import render, redirect
from django.core.mail import send_mail
from django.http import JsonResponse
from django.views.decorators.csrf import csrf_exempt
import json
def index(request):
return render(request, "html/index.html", context={'user': request.user})
@csrf_exempt
def test_mail(request):
if request.method != "POST":
return JsonResponse({"error": "Only POST allowed"}, status=405)
try:
data = json.loads(request.body)
recipient = data.get("recipient")
if not recipient:
return JsonResponse({"error": "Missing recipient"}, status=400)
send_mail(
subject="Test",
message="Django test mail",
from_email=None, # použije defaultní FROM_EMAIL ze settings
recipient_list=[recipient],
fail_silently=False,
)
return JsonResponse({"success": f"E-mail sent to {recipient}"})
except Exception as e:
import traceback
traceback.print_exc() # vypíše do konzole
return JsonResponse({"error": str(e)}, status=500)

16
backend/trznice/wsgi.py Normal file
View File

@@ -0,0 +1,16 @@
"""
WSGI config for trznice project.
It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/5.2/howto/deployment/wsgi/
"""
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'trznice.settings')
application = get_wsgi_application()