Compare commits

..

No commits in common. "main" and "zmiany-mariusz" have entirely different histories.

307 changed files with 13759 additions and 31686 deletions

BIN
.DS_Store vendored

Binary file not shown.

106
backend.log Normal file
View File

@ -0,0 +1,106 @@
Watching for file changes with StatReloader
[28/May/2025 18:21:16] "GET /api/content/posts/ HTTP/1.1" 200 1074
[28/May/2025 18:21:16] "GET /api/content/posts/ HTTP/1.1" 200 1074
[28/May/2025 18:21:18] "GET /api/content/posts/ HTTP/1.1" 200 1074
[28/May/2025 18:21:18] "GET /api/content/posts/ HTTP/1.1" 200 1074
[28/May/2025 18:21:22] "OPTIONS /api/users/login/ HTTP/1.1" 200 0
[28/May/2025 18:21:25] "OPTIONS /api/users/login/ HTTP/1.1" 200 0
[28/May/2025 18:21:28] "OPTIONS /api/users/login/ HTTP/1.1" 200 0
[28/May/2025 18:21:31] "OPTIONS /api/users/login/ HTTP/1.1" 200 0
[28/May/2025 18:21:44] "OPTIONS /api/users/login/ HTTP/1.1" 200 0
[28/May/2025 18:24:59] "POST /api/users/refresh/ HTTP/1.1" 200 483
Unauthorized: /api/users/refresh/
[28/May/2025 18:24:59] "POST /api/users/refresh/ HTTP/1.1" 401 58
[28/May/2025 18:24:59] "GET /api/users/me/ HTTP/1.1" 200 101
[28/May/2025 18:24:59] "POST /api/users/refresh/ HTTP/1.1" 200 483
Unauthorized: /api/users/refresh/
[28/May/2025 18:24:59] "POST /api/users/refresh/ HTTP/1.1" 401 58
[28/May/2025 18:25:07] "GET /api/content/posts/ HTTP/1.1" 200 1074
[28/May/2025 18:25:07] "GET /api/content/posts/ HTTP/1.1" 200 1074
[28/May/2025 18:25:12] "OPTIONS /api/users/login/ HTTP/1.1" 200 0
[28/May/2025 18:25:12] "POST /api/users/login/ HTTP/1.1" 200 483
[28/May/2025 18:25:13] "GET /api/users/me/ HTTP/1.1" 200 101
[28/May/2025 18:27:05] "POST /api/users/refresh/ HTTP/1.1" 200 483
[28/May/2025 18:27:05] "GET /api/users/me/ HTTP/1.1" 200 101
[28/May/2025 18:27:05] "POST /api/users/refresh/ HTTP/1.1" 200 483
[28/May/2025 18:27:05] "GET /api/users/me/ HTTP/1.1" 200 101
[28/May/2025 18:27:15] "GET /api/content/posts/ HTTP/1.1" 200 1074
[28/May/2025 18:27:15] "GET /api/content/posts/ HTTP/1.1" 200 1074
[28/May/2025 18:27:46] "POST /api/users/refresh/ HTTP/1.1" 200 483
[28/May/2025 18:27:46] "GET /api/users/me/ HTTP/1.1" 200 101
[28/May/2025 18:27:46] "POST /api/users/refresh/ HTTP/1.1" 200 483
[28/May/2025 18:27:46] "GET /api/users/me/ HTTP/1.1" 200 101
[28/May/2025 18:34:05] "POST /api/users/refresh/ HTTP/1.1" 200 483
[28/May/2025 18:34:05] "GET /api/users/me/ HTTP/1.1" 200 101
[28/May/2025 18:34:05] "POST /api/users/refresh/ HTTP/1.1" 200 483
[28/May/2025 18:34:05] "GET /api/users/me/ HTTP/1.1" 200 101
[28/May/2025 18:38:05] "POST /api/users/refresh/ HTTP/1.1" 200 483
[28/May/2025 18:38:05] "GET /api/users/me/ HTTP/1.1" 200 101
[28/May/2025 18:38:05] "POST /api/users/refresh/ HTTP/1.1" 200 483
[28/May/2025 18:38:05] "GET /api/users/me/ HTTP/1.1" 200 101
[28/May/2025 18:38:08] "POST /api/users/refresh/ HTTP/1.1" 200 483
[28/May/2025 18:38:08] "GET /api/users/me/ HTTP/1.1" 200 101
[28/May/2025 18:38:08] "POST /api/users/refresh/ HTTP/1.1" 200 483
[28/May/2025 18:38:08] "GET /api/users/me/ HTTP/1.1" 200 101
[28/May/2025 18:38:08] "POST /api/users/refresh/ HTTP/1.1" 200 483
Unauthorized: /api/users/refresh/
[28/May/2025 18:38:08] "POST /api/users/refresh/ HTTP/1.1" 401 58
[28/May/2025 18:38:08] "GET /api/users/me/ HTTP/1.1" 200 101
[28/May/2025 18:38:08] "POST /api/users/refresh/ HTTP/1.1" 200 483
Unauthorized: /api/users/refresh/
[28/May/2025 18:38:09] "POST /api/users/refresh/ HTTP/1.1" 401 58
[28/May/2025 18:38:15] "POST /api/users/login/ HTTP/1.1" 200 483
[28/May/2025 18:38:15] "GET /api/users/me/ HTTP/1.1" 200 101
[28/May/2025 18:39:29] "POST /api/users/refresh/ HTTP/1.1" 200 483
Unauthorized: /api/users/refresh/
[28/May/2025 18:39:29] "POST /api/users/refresh/ HTTP/1.1" 401 58
[28/May/2025 18:39:29] "GET /api/users/me/ HTTP/1.1" 200 101
[28/May/2025 18:39:30] "POST /api/users/refresh/ HTTP/1.1" 200 483
Unauthorized: /api/users/refresh/
[28/May/2025 18:39:30] "POST /api/users/refresh/ HTTP/1.1" 401 58
[28/May/2025 18:39:36] "POST /api/users/login/ HTTP/1.1" 200 483
[28/May/2025 18:39:36] "GET /api/users/me/ HTTP/1.1" 200 101
[28/May/2025 18:39:38] "GET /api/content/posts/ HTTP/1.1" 200 1074
[28/May/2025 18:39:38] "GET /api/content/posts/ HTTP/1.1" 200 1074
[28/May/2025 18:40:25] "POST /api/users/refresh/ HTTP/1.1" 200 483
[28/May/2025 18:40:25] "GET /api/users/me/ HTTP/1.1" 200 101
[28/May/2025 18:40:25] "POST /api/users/refresh/ HTTP/1.1" 200 483
[28/May/2025 18:40:25] "GET /api/users/me/ HTTP/1.1" 200 101
[28/May/2025 18:40:27] "POST /api/users/refresh/ HTTP/1.1" 200 483
[28/May/2025 18:40:27] "GET /api/users/me/ HTTP/1.1" 200 101
[28/May/2025 18:40:27] "POST /api/users/refresh/ HTTP/1.1" 200 483
[28/May/2025 18:40:27] "GET /api/users/me/ HTTP/1.1" 200 101
[28/May/2025 18:40:30] "POST /api/users/refresh/ HTTP/1.1" 200 483
[28/May/2025 18:40:30] "GET /api/users/me/ HTTP/1.1" 200 101
[28/May/2025 18:40:30] "POST /api/users/refresh/ HTTP/1.1" 200 483
[28/May/2025 18:40:30] "GET /api/users/me/ HTTP/1.1" 200 101
[28/May/2025 18:40:38] "POST /api/users/login/ HTTP/1.1" 200 483
[28/May/2025 18:40:38] "GET /api/users/me/ HTTP/1.1" 200 101
[28/May/2025 18:41:05] "POST /api/users/refresh/ HTTP/1.1" 200 483
[28/May/2025 18:41:05] "GET /api/users/me/ HTTP/1.1" 200 101
[28/May/2025 18:41:05] "POST /api/users/refresh/ HTTP/1.1" 200 483
[28/May/2025 18:41:05] "GET /api/users/me/ HTTP/1.1" 200 101
[28/May/2025 18:41:22] "POST /api/users/refresh/ HTTP/1.1" 200 483
[28/May/2025 18:41:22] "GET /api/users/me/ HTTP/1.1" 200 101
[28/May/2025 18:41:22] "POST /api/users/refresh/ HTTP/1.1" 200 483
[28/May/2025 18:41:22] "GET /api/users/me/ HTTP/1.1" 200 101
[28/May/2025 18:41:41] "GET /api/content/posts/ HTTP/1.1" 200 1074
[28/May/2025 18:41:41] "GET /api/content/posts/ HTTP/1.1" 200 1074
[28/May/2025 18:41:42] "GET /api/content/posts/ HTTP/1.1" 200 1074
[28/May/2025 18:41:42] "GET /api/content/posts/ HTTP/1.1" 200 1074
[28/May/2025 18:42:12] "POST /api/users/refresh/ HTTP/1.1" 200 483
[28/May/2025 18:42:12] "GET /api/users/me/ HTTP/1.1" 200 101
[28/May/2025 18:42:12] "POST /api/users/refresh/ HTTP/1.1" 200 483
[28/May/2025 18:42:12] "GET /api/users/me/ HTTP/1.1" 200 101
[28/May/2025 18:43:37] "POST /api/users/refresh/ HTTP/1.1" 200 483
[28/May/2025 18:43:37] "GET /api/users/me/ HTTP/1.1" 200 101
[28/May/2025 18:43:37] "POST /api/users/refresh/ HTTP/1.1" 200 483
[28/May/2025 18:43:37] "GET /api/users/me/ HTTP/1.1" 200 101
[28/May/2025 18:49:14] "POST /api/users/refresh/ HTTP/1.1" 200 483
[28/May/2025 18:49:14] "GET /api/users/me/ HTTP/1.1" 200 101
[28/May/2025 18:49:14] "POST /api/users/refresh/ HTTP/1.1" 200 483
[28/May/2025 18:49:14] "GET /api/users/me/ HTTP/1.1" 200 101
[28/May/2025 18:53:49] "POST /api/users/refresh/ HTTP/1.1" 200 483
[28/May/2025 18:53:49] "POST /api/users/refresh/ HTTP/1.1" 200 483
[28/May/2025 18:53:49] "GET /api/users/me/ HTTP/1.1" 200 101
[28/May/2025 18:53:49] "GET /api/users/me/ HTTP/1.1" 200 101

BIN
backend/.DS_Store vendored Normal file

Binary file not shown.

3
backend/.gitignore vendored
View File

@ -1,3 +1,5 @@
#Ignorowanie plików i katalogów w projekcie backend
# Created by venv; see https://docs.python.org/3/library/venv.html # Created by venv; see https://docs.python.org/3/library/venv.html
/bin /bin
/lib /lib
@ -7,3 +9,4 @@
/doc /doc
/html /html
pyvenv.cfg pyvenv.cfg
/venv

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -1,3 +1,4 @@
#Obsługa panelu administracyjnego dla aplikacji content
from django.contrib import admin from django.contrib import admin
# Register your models here. # Register your models here.

View File

@ -1,6 +1,7 @@
#Ogbługa aplikacji content w Django
from django.apps import AppConfig from django.apps import AppConfig
# Definicja klasy konfiguracyjnej aplikacji Django dla modułu content.
class ContentConfig(AppConfig): class ContentConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField' default_auto_field = 'django.db.models.BigAutoField'
name = 'content' name = 'content'

View File

@ -0,0 +1,71 @@
# Generated by Django 4.2 on 2025-07-09 20:01
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
class Migration(migrations.Migration):
dependencies = [
('content', '0006_page_slug_post_slug'),
]
operations = [
migrations.AddField(
model_name='category',
name='created_at',
field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now),
preserve_default=False,
),
migrations.AddField(
model_name='category',
name='description',
field=models.TextField(blank=True, null=True),
),
migrations.AddField(
model_name='category',
name='meta_description',
field=models.CharField(blank=True, help_text='Opis dla wyszukiwarek (SEO)', max_length=160, null=True),
),
migrations.AddField(
model_name='category',
name='parent_category',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='subcategories', to='content.category'),
),
migrations.AddField(
model_name='page',
name='meta_description',
field=models.CharField(blank=True, help_text='Opis dla wyszukiwarek (SEO)', max_length=160, null=True),
),
migrations.AddField(
model_name='page',
name='meta_image',
field=models.ImageField(blank=True, help_text='Obrazek do podglądu w linkach (np. na Facebooku).', null=True, upload_to='seo/'),
),
migrations.AddField(
model_name='page',
name='meta_title',
field=models.CharField(blank=True, help_text='Tytuł wyświetlany w wyszukiwarkach. Domyślnie będzie użyty title.', max_length=70),
),
migrations.AddField(
model_name='post',
name='meta_description',
field=models.CharField(blank=True, help_text='Opis dla wyszukiwarek (SEO)', max_length=160, null=True),
),
migrations.AddField(
model_name='post',
name='meta_image',
field=models.ImageField(blank=True, help_text='Obrazek do podglądu w linkach (np. na Facebooku).', null=True, upload_to='seo/'),
),
migrations.AddField(
model_name='post',
name='meta_title',
field=models.CharField(blank=True, help_text='Tytuł wyświetlany w wyszukiwarkach. Domyślnie będzie użyty title.', max_length=70),
),
migrations.AlterField(
model_name='category',
name='slug',
field=models.SlugField(blank=True, unique=True),
),
]

View File

@ -0,0 +1,23 @@
# Generated by Django 4.2 on 2025-07-10 20:08
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('content', '0007_category_created_at_category_description_and_more'),
]
operations = [
migrations.AlterField(
model_name='page',
name='meta_image',
field=models.URLField(blank=True, help_text='Obrazek do podglądu w linkach (np. na Facebooku)', null=True),
),
migrations.AlterField(
model_name='post',
name='meta_image',
field=models.URLField(blank=True, help_text='Obrazek do podglądu w linkach (np. na Facebooku)', null=True),
),
]

View File

@ -0,0 +1,24 @@
# Generated by Django 4.2 on 2025-08-07 07:57
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('content', '0008_alter_page_meta_image_alter_post_meta_image'),
]
operations = [
migrations.AddField(
model_name='uploadedimage',
name='category',
field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='content.category'),
),
migrations.AddField(
model_name='uploadedimage',
name='description',
field=models.TextField(blank=True, help_text='Opis obrazka', null=True),
),
]

View File

@ -1,16 +1,30 @@
#Modele do obsługi treści: takie jak posty, strony, komentarze i przesłane obrazy.
from django.db import models from django.db import models
from users.models import User from users.models import User
from django.conf import settings from django.conf import settings
from django.utils import timezone from django.utils import timezone
from django.core.validators import FileExtensionValidator from django.core.validators import FileExtensionValidator
from django.utils.text import slugify
#Obsługa kategorii
class Category(models.Model): class Category(models.Model):
name = models.CharField(max_length=255) name = models.CharField(max_length=255)
slug = models.SlugField(unique=True) slug = models.SlugField(unique=True, blank=True)
description = models.TextField(blank=True, null=True)
parent_category = models.ForeignKey('self', null=True, blank=True, on_delete=models.CASCADE, related_name='subcategories')
created_at = models.DateTimeField(auto_now_add=True)
meta_description = models.CharField(max_length=160, blank=True, null=True, help_text="Opis dla wyszukiwarek (SEO)")
def save(self, *args, **kwargs):
if not self.slug:
self.slug = slugify(self.name)
super().save(*args, **kwargs)
def __str__(self): def __str__(self):
return self.name return self.name
#Obsługa treści
class AbstractContent(models.Model): class AbstractContent(models.Model):
STATUS_CHOICES = ( STATUS_CHOICES = (
('draft', 'Draft'), ('draft', 'Draft'),
@ -26,18 +40,24 @@ class AbstractContent(models.Model):
updated_at = models.DateTimeField(auto_now=True) updated_at = models.DateTimeField(auto_now=True)
category = models.ForeignKey('Category', on_delete=models.CASCADE) category = models.ForeignKey('Category', on_delete=models.CASCADE)
tags = models.ManyToManyField('Tag', blank=True) tags = models.ManyToManyField('Tag', blank=True)
meta_title = models.CharField(max_length=70, blank=True, help_text="Tytuł wyświetlany w wyszukiwarkach. Domyślnie będzie użyty title.")
meta_description = models.CharField(max_length=160, blank=True, null=True, help_text="Opis dla wyszukiwarek (SEO)")
meta_image = models.URLField(blank=True, null=True, help_text="Obrazek do podglądu w linkach (np. na Facebooku)")
class Meta: class Meta:
abstract = True abstract = True
def __str__(self): def __str__(self):
return self.title return self.title
#Obsługa postów i stron
class Post(AbstractContent): class Post(AbstractContent):
pass pass
class Page(AbstractContent): class Page(AbstractContent):
pass pass
#Obsługa komentarzy
class Comment(models.Model): class Comment(models.Model):
content = models.TextField(max_length=2000) content = models.TextField(max_length=2000)
approved = models.BooleanField(default=False) approved = models.BooleanField(default=False)
@ -46,6 +66,7 @@ class Comment(models.Model):
page = models.ForeignKey(Page, null=True, blank=True, on_delete=models.CASCADE) page = models.ForeignKey(Page, null=True, blank=True, on_delete=models.CASCADE)
created_at = models.DateTimeField(auto_now_add=True) created_at = models.DateTimeField(auto_now_add=True)
#Obsługa przesłanych obrazów
class UploadedImage(models.Model): class UploadedImage(models.Model):
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
image = models.ImageField( image = models.ImageField(
@ -55,6 +76,8 @@ class UploadedImage(models.Model):
title = models.CharField(max_length=255) title = models.CharField(max_length=255)
uploaded_at = models.DateTimeField(auto_now_add=True) uploaded_at = models.DateTimeField(auto_now_add=True)
file_size = models.PositiveIntegerField() # Size in bytes file_size = models.PositiveIntegerField() # Size in bytes
category = models.ForeignKey('Category', on_delete=models.CASCADE, default=1)
description = models.TextField(blank=True, null=True, help_text="Opis obrazka")
class Meta: class Meta:
ordering = ['-uploaded_at'] ordering = ['-uploaded_at']
@ -81,6 +104,7 @@ class UploadedImage(models.Model):
uploaded_at__lt=tomorrow uploaded_at__lt=tomorrow
).aggregate(total_size=models.Sum('file_size'))['total_size'] or 0 ).aggregate(total_size=models.Sum('file_size'))['total_size'] or 0
#Obsługa tagów
class Tag(models.Model): class Tag(models.Model):
name = models.CharField(max_length=255) name = models.CharField(max_length=255)
slug = models.SlugField(unique=True) slug = models.SlugField(unique=True)

View File

@ -1,25 +1,40 @@
from rest_framework import serializers import os
from .models import Post, Comment, Page, Category, UploadedImage, Tag
from users.models import User
from django.utils.text import slugify
from rest_framework import serializers
from users.models import User
from .models import (
UploadedImage,
Category,
Post,
Comment,
Page,
Tag,
)
# Serialized do tagów
class TagSerializer(serializers.ModelSerializer): class TagSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = Tag model = Tag
fields = ['id', 'name', 'slug'] fields = ['id', 'name', 'slug']
read_only_fields = ['id'] read_only_fields = ['id']
# Serialized do kategorii
class CategorySerializer(serializers.ModelSerializer): class CategorySerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = Category model = Category
fields = ['id', 'name', 'slug'] fields = ['id', 'name', 'slug', 'description', 'parent_category', 'created_at']
read_only_fields = ['id'] read_only_fields = ['id', 'created_at']
# Serialized do użytkowników
class UserSerializer(serializers.ModelSerializer): class UserSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = User model = User
fields = ['id', 'username', 'first_name', 'last_name'] fields = ['id', 'username', 'first_name', 'last_name']
read_only_fields = ['id'] read_only_fields = ['id']
# Serialized do komentarzy
class CommentSerializer(serializers.ModelSerializer): class CommentSerializer(serializers.ModelSerializer):
author = UserSerializer(read_only=True) author = UserSerializer(read_only=True)
@ -28,6 +43,7 @@ class CommentSerializer(serializers.ModelSerializer):
fields = ['id', 'content', 'author', 'post', 'page', 'approved', 'created_at'] fields = ['id', 'content', 'author', 'post', 'page', 'approved', 'created_at']
read_only_fields = ['id', 'created_at', 'author'] read_only_fields = ['id', 'created_at', 'author']
# Serialized do postów
class PostSerializer(serializers.ModelSerializer): class PostSerializer(serializers.ModelSerializer):
author = UserSerializer(read_only=True) author = UserSerializer(read_only=True)
category = CategorySerializer(read_only=True) category = CategorySerializer(read_only=True)
@ -47,19 +63,34 @@ class PostSerializer(serializers.ModelSerializer):
) )
slug = serializers.SlugField(required=True) slug = serializers.SlugField(required=True)
meta_title = serializers.CharField(required=False, allow_blank=True)
meta_description = serializers.CharField(required=False, allow_blank=True)
meta_image = serializers.CharField(required=False, allow_null=True)
class Meta: class Meta:
model = Post model = Post
fields = [ fields = [
'id', 'title', 'content', 'status', 'author', 'id', 'title', 'content', 'status', 'author',
'category', 'category_id', 'comments', 'category', 'category_id', 'comments',
'created_at', 'updated_at', 'tags', 'tag_ids', 'slug' 'created_at', 'updated_at', 'tags', 'tag_ids', 'slug',
] 'meta_title', 'meta_description', 'meta_image',]
read_only_fields = ['id', 'created_at', 'updated_at', 'author', 'slug'] read_only_fields = ['id', 'created_at', 'updated_at', 'author', 'slug']
lookup_field = 'slug' lookup_field = 'slug'
extra_kwargs = { extra_kwargs = {
'url': {'lookup_field': 'slug'} 'url': {'lookup_field': 'slug'}
} }
def create(self, validated_data):
if not validated_data.get('slug'):
validated_data['slug'] = slugify(validated_data.get('title', ''))
return super().create(validated_data)
def update(self, instance, validated_data):
if not validated_data.get('slug') and validated_data.get('title'):
validated_data['slug'] = slugify(validated_data['title'])
return super().update(instance, validated_data)
# Serialized do stron
class PageSerializer(serializers.ModelSerializer): class PageSerializer(serializers.ModelSerializer):
author = UserSerializer(read_only=True) author = UserSerializer(read_only=True)
category = CategorySerializer(read_only=True) category = CategorySerializer(read_only=True)
@ -70,19 +101,25 @@ class PageSerializer(serializers.ModelSerializer):
) )
comments = CommentSerializer(many=True, read_only=True) comments = CommentSerializer(many=True, read_only=True)
slug = serializers.SlugField(required=True) slug = serializers.SlugField(required=True)
meta_title = serializers.CharField(required=False, allow_blank=True)
meta_description = serializers.CharField(required=False, allow_blank=True)
meta_image = serializers.ImageField(required=False, allow_null=True)
class Meta: class Meta:
model = Page model = Page
fields = [ fields = [
'id', 'title', 'content', 'status', 'author', 'id', 'title', 'content', 'status', 'author',
'category', 'category_id', 'comments', 'category', 'category_id', 'comments',
'created_at', 'updated_at', 'slug' 'created_at', 'updated_at', 'slug',
] 'meta_title', 'meta_description', 'meta_image']
read_only_fields = ['id', 'created_at', 'updated_at', 'author', 'slug'] read_only_fields = ['id', 'created_at', 'updated_at', 'author', 'slug']
lookup_field = 'slug' lookup_field = 'slug'
extra_kwargs = { extra_kwargs = {
'url': {'lookup_field': 'slug'} 'url': {'lookup_field': 'slug'}
} }
# Serialized do przesłanych obrazów
class UploadedImageSerializer(serializers.ModelSerializer): class UploadedImageSerializer(serializers.ModelSerializer):
url = serializers.SerializerMethodField() url = serializers.SerializerMethodField()
file_size_display = serializers.SerializerMethodField() file_size_display = serializers.SerializerMethodField()
@ -90,9 +127,45 @@ class UploadedImageSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = UploadedImage model = UploadedImage
fields = ['id', 'title', 'image', 'url', 'uploaded_at', 'file_size', 'file_size_display'] fields = [
'id',
'title',
'image',
'url',
'uploaded_at',
'file_size',
'file_size_display',
'category',
'description',
]
read_only_fields = ['file_size', 'uploaded_at', 'url'] read_only_fields = ['file_size', 'uploaded_at', 'url']
def get_url(self, obj):
request = self.context.get('request')
if obj.image and hasattr(obj.image, 'url'):
return request.build_absolute_uri(obj.image.url) if request else obj.image.url
return None
def get_file_size_display(self, obj):
print(f"Rozmiar pliku: {obj.file_size}")
size = obj.file_size
if size is None:
return "0 B" # albo return None
for unit in ['B', 'KB', 'MB', 'GB']:
if size < 1024.0:
return f"{size:.1f} {unit}"
size /= 1024.0
return f"{size:.1f} TB"
def validate_image(self, value):
if not value:
raise serializers.ValidationError("Image file is required.")
return value
url = serializers.SerializerMethodField()
file_size_display = serializers.SerializerMethodField()
image = serializers.ImageField(required=True)
def get_url(self, obj): def get_url(self, obj):
request = self.context.get('request') request = self.context.get('request')
if obj.image and hasattr(obj.image, 'url'): if obj.image and hasattr(obj.image, 'url'):
@ -112,4 +185,7 @@ class UploadedImageSerializer(serializers.ModelSerializer):
raise serializers.ValidationError("Image file is required.") raise serializers.ValidationError("Image file is required.")
return value return value
def validate_category(self, value):
if not Category.objects.filter(id=value.id if isinstance(value, Category) else value).exists():
raise serializers.ValidationError("Wybrana kategoria nie istnieje.")
return value

View File

@ -1,3 +1,4 @@
#Obsługa testów dla aplikacji content
from django.test import TestCase from django.test import TestCase
# Create your tests here. # Create your tests here.

View File

@ -1,6 +1,8 @@
#Obsługa ścieżek dla aplikacji content.
from rest_framework.routers import DefaultRouter from rest_framework.routers import DefaultRouter
from django.urls import path, include from django.urls import path, include
from .views import PostViewSet, CommentViewSet, PageViewSet, CategoryViewSet, UploadedImageViewSet, TagViewSet from .views import PostViewSet, CommentViewSet, PageViewSet, CategoryViewSet, UploadedImageViewSet, TagViewSet, SearchView
router = DefaultRouter() router = DefaultRouter()
router.register(r'posts', PostViewSet, basename='post') router.register(r'posts', PostViewSet, basename='post')
@ -9,6 +11,8 @@ router.register(r'pages', PageViewSet, basename='page')
router.register(r'categories', CategoryViewSet, basename='category') router.register(r'categories', CategoryViewSet, basename='category')
router.register(r'images', UploadedImageViewSet, basename='image') router.register(r'images', UploadedImageViewSet, basename='image')
router.register(r'tags', TagViewSet, basename='tag') router.register(r'tags', TagViewSet, basename='tag')
urlpatterns = [ urlpatterns = [
path('', include(router.urls)), path('', include(router.urls)),
path('search/', SearchView.as_view(), name='search'),
] ]

View File

@ -1,44 +1,48 @@
from rest_framework import viewsets, status, filters #Obsługa widoków dla aplikacji content.
import os
from django.conf import settings
from django.contrib.auth import get_user_model
from django.db.models import Q
from django.shortcuts import get_object_or_404
from django.utils import timezone
from rest_framework import viewsets, status, filters, serializers
from rest_framework.views import APIView
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.decorators import action from rest_framework.decorators import action
from rest_framework.permissions import (
IsAuthenticated,
IsAuthenticatedOrReadOnly,
AllowAny,
)
from rest_framework.parsers import JSONParser, MultiPartParser, FormParser
from django_filters.rest_framework import DjangoFilterBackend from django_filters.rest_framework import DjangoFilterBackend
from .models import Post, Comment, Page, Category, UploadedImage, Tag
from .serializers import PostSerializer, CommentSerializer, PageSerializer, CategorySerializer, UploadedImageSerializer, TagSerializer
from users.permissions import IsAuthorOrReadOnly, IsEditorOrAdmin
from rest_framework.permissions import IsAuthenticatedOrReadOnly
from rest_framework.parsers import MultiPartParser, FormParser
from django.conf import settings
from rest_framework import serializers
from .models import (
Post,
Comment,
Page,
Category,
UploadedImage,
Tag,
)
from .serializers import (
PostSerializer,
CommentSerializer,
PageSerializer,
CategorySerializer,
UploadedImageSerializer,
TagSerializer,
)
from users.permissions import IsAuthorOrReadOnly, IsEditorOrAdmin, IsAdminOrReadOnly
# Obsługa logiczna widoku dla postów
class PostViewSet(viewsets.ModelViewSet): class PostViewSet(viewsets.ModelViewSet):
"""
API endpoint for managing blog posts.
list:
Return a list of all posts. Results can be filtered by:
- status (draft/pending/published)
- author
- category
- created_at (date range)
create:
Create a new post. Requires authentication and appropriate permissions.
retrieve:
Return the details of a specific post by ID.
update:
Update all fields of a specific post. Requires authentication and appropriate permissions.
partial_update:
Update one or more fields of a specific post. Requires authentication and appropriate permissions.
destroy:
Delete a specific post. Requires authentication and appropriate permissions.
"""
queryset = Post.objects.all() queryset = Post.objects.all()
serializer_class = PostSerializer serializer_class = PostSerializer
permission_classes = [IsAuthorOrReadOnly] permission_classes = [IsAdminOrReadOnly]
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter] filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
filterset_fields = ['status', 'author', 'category'] filterset_fields = ['status', 'author', 'category']
search_fields = ['title', 'content'] search_fields = ['title', 'content']
@ -63,7 +67,8 @@ class PostViewSet(viewsets.ModelViewSet):
def perform_update(self, serializer): def perform_update(self, serializer):
print(serializer.validated_data) print(serializer.validated_data)
return super().perform_update(serializer) return super().perform_update(serializer)
class CommentViewSet(viewsets.ModelViewSet):
# Obsługa komentarzy do postów i stron
""" """
API endpoint for managing comments on posts and pages. API endpoint for managing comments on posts and pages.
@ -89,6 +94,7 @@ class CommentViewSet(viewsets.ModelViewSet):
destroy: destroy:
Delete a specific comment. Requires authentication and appropriate permissions. Delete a specific comment. Requires authentication and appropriate permissions.
""" """
class CommentViewSet(viewsets.ModelViewSet):
queryset = Comment.objects.all() queryset = Comment.objects.all()
serializer_class = CommentSerializer serializer_class = CommentSerializer
permission_classes = [IsAuthorOrReadOnly] permission_classes = [IsAuthorOrReadOnly]
@ -112,6 +118,7 @@ class CommentViewSet(viewsets.ModelViewSet):
) )
return queryset return queryset
#obsługa widoków dla stron
class PageViewSet(viewsets.ModelViewSet): class PageViewSet(viewsets.ModelViewSet):
queryset = Page.objects.all() queryset = Page.objects.all()
serializer_class = PageSerializer serializer_class = PageSerializer
@ -137,6 +144,7 @@ class PageViewSet(viewsets.ModelViewSet):
) )
return queryset return queryset
# Obsługa kategorii
class CategoryViewSet(viewsets.ModelViewSet): class CategoryViewSet(viewsets.ModelViewSet):
queryset = Category.objects.all() queryset = Category.objects.all()
serializer_class = CategorySerializer serializer_class = CategorySerializer
@ -147,9 +155,11 @@ class CategoryViewSet(viewsets.ModelViewSet):
def get_queryset(self): def get_queryset(self):
return Category.objects.all() return Category.objects.all()
# Obsługa przesyłania obrazów
class UploadedImageViewSet(viewsets.ModelViewSet): class UploadedImageViewSet(viewsets.ModelViewSet):
serializer_class = UploadedImageSerializer serializer_class = UploadedImageSerializer
parser_classes = (MultiPartParser, FormParser) parser_classes = (JSONParser, MultiPartParser, FormParser)
permission_classes = [IsAuthenticated]
def get_queryset(self): def get_queryset(self):
return UploadedImage.objects.filter(user=self.request.user) return UploadedImage.objects.filter(user=self.request.user)
@ -160,28 +170,51 @@ class UploadedImageViewSet(viewsets.ModelViewSet):
return context return context
def perform_create(self, serializer): def perform_create(self, serializer):
# Check daily upload quota (5MB = 5 * 1024 * 1024 bytes) user = self.request.user
DAILY_UPLOAD_LIMIT = 5 * 1024 * 1024 # 5MB in bytes
# Get current daily upload size # Admin? zero limitów
current_size = UploadedImage.get_user_daily_upload_size(self.request.user) if not user.is_staff:
# Limit: 5MB dziennie
DAILY_UPLOAD_LIMIT = 5 * 1024 * 1024
current_size = UploadedImage.get_user_daily_upload_size(user)
file_size = self.request.FILES['image'].size file_size = self.request.FILES['image'].size
# Check if upload would exceed daily limit
if current_size + file_size > DAILY_UPLOAD_LIMIT: if current_size + file_size > DAILY_UPLOAD_LIMIT:
raise serializers.ValidationError({ raise serializers.ValidationError({
'image': 'Daily upload limit (5MB) exceeded. Please try again tomorrow.' 'image': 'Daily upload limit (5MB) exceeded. Try again tomorrow.'
}) })
try: # Limit 1 plik / minutę
instance = serializer.save(user=self.request.user, file_size=file_size) one_minute_ago = timezone.now() - timezone.timedelta(minutes=1)
if UploadedImage.objects.filter(user=user, uploaded_at__gte=one_minute_ago).exists():
raise serializers.ValidationError({
'image': 'Upload limit: 1 image per minute.'
})
except Exception as e: serializer.save(user=user, file_size=self.request.FILES['image'].size)
raise
def perform_update(self, serializer):
instance = serializer.instance
if instance.user != self.request.user and not self.request.user.is_staff:
raise serializers.ValidationError("Brak uprawnień do edycji tego pliku.")
serializer.save()
def destroy(self, request, *args, **kwargs):
obj = self.get_object()
if obj.user != request.user and not request.user.is_staff:
return Response({'detail': 'Brak uprawnień do usunięcia.'}, status=403)
# Usuń fizyczny plik z dysku
image_path = obj.image.path
if os.path.isfile(image_path):
os.remove(image_path)
obj.delete()
return Response({'detail': 'Plik usunięty.'}, status=200)
@action(detail=False, methods=['get']) @action(detail=False, methods=['get'])
def quota(self, request): def quota(self, request):
DAILY_UPLOAD_LIMIT = 5 * 1024 * 1024 # 5MB in bytes DAILY_UPLOAD_LIMIT = 5 * 1024 * 1024
current_size = UploadedImage.get_user_daily_upload_size(request.user) current_size = UploadedImage.get_user_daily_upload_size(request.user)
remaining = max(0, DAILY_UPLOAD_LIMIT - current_size) remaining = max(0, DAILY_UPLOAD_LIMIT - current_size)
@ -194,9 +227,62 @@ class UploadedImageViewSet(viewsets.ModelViewSet):
'remaining_display': f"{remaining / (1024 * 1024):.1f}MB" 'remaining_display': f"{remaining / (1024 * 1024):.1f}MB"
}) })
# Obsługa tagów
class TagViewSet(viewsets.ModelViewSet): class TagViewSet(viewsets.ModelViewSet):
queryset = Tag.objects.all() queryset = Tag.objects.all()
serializer_class = TagSerializer serializer_class = TagSerializer
permission_classes = [IsAuthenticatedOrReadOnly] permission_classes = [IsAuthenticatedOrReadOnly]
filter_backends = [filters.SearchFilter] filter_backends = [filters.SearchFilter]
search_fields = ['name', 'slug'] search_fields = ['name', 'slug']
class SearchView(APIView):
permission_classes = [AllowAny]
def get(self, request):
query = request.GET.get('q', '').strip()
print("🔎 query:", query)
if not query:
return Response([], status=status.HTTP_200_OK)
results = []
# Szukaj w postach
post_matches = Post.objects.filter(
Q(title__icontains=query) | Q(content__icontains=query)
)[:5]
for post in post_matches:
results.append({
'id': post.id,
'title': post.title,
'slug': post.slug,
'type': 'post'
})
# Szukaj w tagach
tag_matches = Tag.objects.filter(
Q(name__icontains=query)
)[:5]
for tag in tag_matches:
results.append({
'id': tag.id,
'title': tag.name,
'slug': tag.slug,
'type': 'tag'
})
# Szukaj w użytkownikach
User = get_user_model()
user_matches = User.objects.filter(
Q(username__icontains=query) | Q(email__icontains=query)
)[:5]
for user in user_matches:
results.append({
'id': user.id,
'title': user.username,
'slug': str(user.id), # lub `username` jeśli masz slug
'type': 'user'
})
return Response(results)

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,3 @@
from .formula import FormulaAdmin
from .symbol import SymbolAdmin
from .category import FormulaCategoryAdmin

View File

@ -0,0 +1,8 @@
from django.contrib import admin
from formulas.models.category import FormulaCategory
@admin.register(FormulaCategory)
class FormulaCategoryAdmin(admin.ModelAdmin):
list_display = ("id", "prefix", "name")
ordering = ("prefix",)
search_fields = ("name",)

View File

@ -0,0 +1,10 @@
from django.contrib import admin
from formulas.models.formula import Formula
@admin.register(Formula)
class FormulaAdmin(admin.ModelAdmin):
list_display = ("formula_id", "title", "code", "revision", "category")
list_filter = ("category", "revision")
search_fields = ("formula_id", "title", "code", "description")
readonly_fields = ("code", "created_at")
ordering = ("code",)

Binary file not shown.

View File

@ -0,0 +1,10 @@
from django.contrib import admin
from formulas.models.symbol import Symbol
@admin.register(Symbol)
class SymbolAdmin(admin.ModelAdmin):
list_display = ("symbol_id", "name", "unit", "code", "revision", "category")
list_filter = ("category", "revision")
search_fields = ("symbol_id", "name", "code", "description")
readonly_fields = ("code",)
ordering = ("code",)

6
backend/formulas/apps.py Normal file
View File

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

View File

@ -0,0 +1,65 @@
# Generated by Django 4.2 on 2025-06-22 11:14
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='FormulaCategory',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100)),
('prefix', models.PositiveIntegerField(unique=True)),
],
options={
'verbose_name': 'Kategoria wzoru',
'verbose_name_plural': 'Kategorie wzorów',
},
),
migrations.CreateModel(
name='Symbol',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('symbol_id', models.CharField(max_length=10)),
('name', models.CharField(max_length=255)),
('unit', models.CharField(max_length=50)),
('description', models.TextField(blank=True)),
('tags', models.JSONField(blank=True, default=list)),
('revision', models.PositiveIntegerField(default=1)),
('code', models.CharField(editable=False, max_length=10, unique=True)),
('meta', models.TextField(blank=True)),
('category', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='formulas.formulacategory')),
],
),
migrations.CreateModel(
name='Formula',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('formula_id', models.CharField(max_length=100)),
('title', models.CharField(max_length=255)),
('description', models.TextField(blank=True)),
('meta', models.TextField(blank=True)),
('tags', models.JSONField(default=list)),
('latex', models.TextField()),
('raw_latex', models.TextField()),
('variables', models.JSONField(default=list)),
('calculator', models.CharField(max_length=100)),
('revision', models.PositiveIntegerField(default=1)),
('code', models.CharField(editable=False, max_length=10, unique=True)),
('created_at', models.DateTimeField(default=django.utils.timezone.now)),
('category', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='formulas.formulacategory')),
('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)),
],
),
]

View File

@ -0,0 +1,30 @@
# Generated by Django 4.2 on 2025-07-13 19:15
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('formulas', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='FavoriteFormula',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('added_at', models.DateTimeField(auto_now_add=True)),
('formula', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='formulas.formula')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name': 'Ulubiony wzór',
'verbose_name_plural': 'Ulubione wzory',
'unique_together': {('user', 'formula')},
},
),
]

View File

@ -0,0 +1,22 @@
# Generated by Django 4.2 on 2025-07-26 08:17
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('formulas', '0002_favoriteformula'),
]
operations = [
migrations.RemoveField(
model_name='formula',
name='variables',
),
migrations.AddField(
model_name='formula',
name='symbols',
field=models.ManyToManyField(blank=True, related_name='formulas', to='formulas.symbol'),
),
]

View File

@ -0,0 +1,31 @@
# Generated by Django 4.2 on 2025-08-07 07:57
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('formulas', '0003_remove_formula_variables_formula_symbols'),
]
operations = [
migrations.AlterField(
model_name='formula',
name='code',
field=models.CharField(max_length=10, unique=True),
),
migrations.AlterField(
model_name='formula',
name='created_by',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL),
),
migrations.AlterField(
model_name='formula',
name='symbols',
field=models.ManyToManyField(related_name='formulas', to='formulas.symbol'),
),
]

View File

View File

@ -0,0 +1,9 @@
from .formula import Formula
from .symbol import Symbol
from .category import FormulaCategory
__all__ = [
"Formula",
"Symbol",
"FormulaCategory",
]

View File

@ -0,0 +1,12 @@
from django.db import models
class FormulaCategory(models.Model):
name = models.CharField(max_length=100)
prefix = models.PositiveIntegerField(unique=True) # cyfra `x` w kodzie xyz
class Meta:
verbose_name = "Kategoria wzoru"
verbose_name_plural = "Kategorie wzorów"
def __str__(self):
return f"{self.prefix} - {self.name}"

View File

@ -0,0 +1,17 @@
# backend/formulas/models/favorite_formula.py
from django.db import models
from django.conf import settings
from formulas.models.formula import Formula
class FavoriteFormula(models.Model):
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
formula = models.ForeignKey(Formula, on_delete=models.CASCADE)
added_at = models.DateTimeField(auto_now_add=True)
class Meta:
unique_together = ('user', 'formula')
verbose_name = "Ulubiony wzór"
verbose_name_plural = "Ulubione wzory"
def __str__(self):
return f"{self.user.username} ❤️ {self.formula.code}"

View File

@ -0,0 +1,38 @@
from django.db import models
from .category import FormulaCategory
from users.models import User
from django.utils import timezone
from .symbol import Symbol
class Formula(models.Model):
formula_id = models.CharField(max_length=100)
title = models.CharField(max_length=255)
description = models.TextField(blank=True)
meta = models.TextField(blank=True)
category = models.ForeignKey(FormulaCategory, on_delete=models.PROTECT)
tags = models.JSONField(default=list)
latex = models.TextField()
raw_latex = models.TextField()
symbols = models.ManyToManyField(Symbol, related_name='formulas')
calculator = models.CharField(max_length=100)
revision = models.PositiveIntegerField(default=1)
code = models.CharField(max_length=10, unique=True)
created_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True)
created_at = models.DateTimeField(default=timezone.now)
def save(self, *args, **kwargs):
if not self.code:
base_prefix = f"{self.category.prefix:02d}"
existing_codes = Formula.objects.filter(category=self.category).values_list('code', flat=True)
index = 1
while True:
proposed_code = f"{base_prefix}{index:02d}{self.revision:02d}"
if proposed_code not in existing_codes:
self.code = proposed_code
break
index += 1
super().save(*args, **kwargs)
def __str__(self):
return f"{self.title} ({self.code})"

View File

@ -0,0 +1,25 @@
from django.db import models
from .category import FormulaCategory
class Symbol(models.Model):
symbol_id = models.CharField(max_length=10) # np. F
name = models.CharField(max_length=255)
unit = models.CharField(max_length=50)
description = models.TextField(blank=True)
category = models.ForeignKey(FormulaCategory, on_delete=models.PROTECT)
tags = models.JSONField(default=list, blank=True)
revision = models.PositiveIntegerField(default=1)
code = models.CharField(max_length=10, unique=True, editable=False)
meta = models.TextField(blank=True) # np. meta-opis dla SEO / edytora
#Zaspis zmiennej z code
def save(self, *args, **kwargs):
if not self.code:
count = Symbol.objects.filter(category=self.category).count() + 1
self.code = f"{self.category.prefix:02d}{count:02d}{self.revision:02d}"
super().save(*args, **kwargs)
#Definiowanie czym jest "self"
def __str__(self):
return f"{self.name} ({self.symbol_id}) [{self.code}]"

View File

@ -0,0 +1,9 @@
from rest_framework.permissions import BasePermission
class IsAdminOrEditor(BasePermission):
def has_permission(self, request, view):
return request.method in ("GET", "HEAD", "OPTIONS") or (
request.user and request.user.is_authenticated and (
request.user.is_superuser or getattr(request.user, "role", None) == "editor"
)
)

View File

@ -0,0 +1,9 @@
from .formula import FormulaSerializer
from .symbol import SymbolSerializer
from .category import FormulaCategorySerializer
__all__ = [
"FormulaSerializer",
"SymbolSerializer",
"FormulaCategorySerializer",
]

View File

@ -0,0 +1,7 @@
from rest_framework import serializers
from formulas.models.category import FormulaCategory
class FormulaCategorySerializer(serializers.ModelSerializer):
class Meta:
model = FormulaCategory
fields = '__all__'

View File

@ -0,0 +1,8 @@
from rest_framework import serializers
from formulas.models.formula import Formula
class FormulaSerializer(serializers.ModelSerializer):
class Meta:
model = Formula
fields = "__all__"
read_only_fields = ('code', 'created_by', 'created_at')

View File

@ -0,0 +1,8 @@
# backend/formulas/serializers/symbol.py
from rest_framework import serializers
from formulas.models.symbol import Symbol
class SymbolSerializer(serializers.ModelSerializer):
class Meta:
model = Symbol
fields = "__all__"

View File

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

19
backend/formulas/urls.py Normal file
View File

@ -0,0 +1,19 @@
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from .views import SymbolViewSet, FormulaViewSet, FormulaCategoryViewSet
# 🔧 Router z obsługą lookup_field='code'
class LookupByCodeRouter(DefaultRouter):
def get_lookup_regex(self, viewset, lookup_prefix=''):
if hasattr(viewset, 'lookup_field') and viewset.lookup_field == 'code':
return r'(?P<code>[^/.]+)'
return super().get_lookup_regex(viewset, lookup_prefix)
router = LookupByCodeRouter()
router.register(r'symbols', SymbolViewSet, basename='symbol')
router.register(r'formulas', FormulaViewSet, basename='formula')
router.register(r'formulascategories', FormulaCategoryViewSet, basename='category')
urlpatterns = [
path('', include(router.urls)),
]

View File

@ -0,0 +1,11 @@
from .formula import FormulaViewSet
from .symbol import SymbolViewSet
from .category import FormulaCategoryViewSet
from .favorite import FavoriteFormulaViewSet
__all__ = [
"FormulaViewSet",
"SymbolViewSet",
"FormulaCategoryViewSet",
"FavoriteFormulaViewSet",
]

View File

@ -0,0 +1,11 @@
# backend/formulas/views/category.py
from rest_framework.viewsets import ModelViewSet
from formulas.models.category import FormulaCategory
from formulas.serializers.category import FormulaCategorySerializer
from formulas.permissions import IsAdminOrEditor
from rest_framework.permissions import IsAuthenticatedOrReadOnly
class FormulaCategoryViewSet(ModelViewSet):
queryset = FormulaCategory.objects.all()
serializer_class = FormulaCategorySerializer
permission_classes = [IsAuthenticatedOrReadOnly]

Binary file not shown.

View File

@ -0,0 +1,40 @@
# backend/formulas/views/favorite.py
from rest_framework import status, viewsets
from rest_framework.decorators import action
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from formulas.models.formula import Formula
from formulas.models.favorite import FavoriteFormula
from formulas.serializers.formula import FormulaSerializer
class FavoriteFormulaViewSet(viewsets.ViewSet):
permission_classes = [IsAuthenticated]
@action(detail=False, methods=['get'])
def list(self, request):
favorites = FavoriteFormula.objects.filter(user=request.user)
formulas = [fav.formula for fav in favorites]
serializer = FormulaSerializer(formulas, many=True)
return Response(serializer.data)
@action(detail=True, methods=['post'])
def add(self, request, pk=None):
try:
formula = Formula.objects.get(pk=pk)
except Formula.DoesNotExist:
return Response({"detail": "Formula not found."}, status=404)
fav, created = FavoriteFormula.objects.get_or_create(user=request.user, formula=formula)
if not created:
return Response({"detail": "Already in favorites."}, status=400)
return Response({"detail": "Added to favorites."}, status=200)
@action(detail=True, methods=['delete'])
def remove(self, request, pk=None):
try:
fav = FavoriteFormula.objects.get(user=request.user, formula_id=pk)
fav.delete()
return Response({"detail": "Removed from favorites."}, status=204)
except FavoriteFormula.DoesNotExist:
return Response({"detail": "Not in favorites."}, status=404)

View File

@ -0,0 +1,21 @@
# backend/formulas/views/formula.py
from rest_framework.viewsets import ModelViewSet
from formulas.models.formula import Formula
from formulas.serializers.formula import FormulaSerializer
from formulas.permissions import IsAdminOrEditor
from rest_framework.decorators import action
from rest_framework.response import Response
from formulas.serializers import SymbolSerializer
class FormulaViewSet(ModelViewSet):
queryset = Formula.objects.all()
serializer_class = FormulaSerializer
permission_classes = [IsAdminOrEditor]
lookup_field = "code"
@action(detail=True, methods=['get'], url_path='symbols')
def get_symbols(self, request, code=None):
formula = self.get_object()
symbols = formula.symbols.all()
serializer = SymbolSerializer(symbols, many=True)
return Response(serializer.data)

View File

@ -0,0 +1,10 @@
from rest_framework.viewsets import ModelViewSet
from formulas.models.symbol import Symbol
from formulas.serializers.symbol import SymbolSerializer
from formulas.permissions import IsAdminOrEditor
class SymbolViewSet(ModelViewSet):
queryset = Symbol.objects.all()
serializer_class = SymbolSerializer
permission_classes = [IsAdminOrEditor]
lookup_field = "code"

Binary file not shown.

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