Compare commits

..

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

307 changed files with 31686 additions and 13759 deletions

BIN
.DS_Store vendored

Binary file not shown.

View File

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

Binary file not shown.

3
backend/.gitignore vendored
View File

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

View File

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

View File

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

View File

@ -1,71 +0,0 @@
# 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

@ -1,23 +0,0 @@
# 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

@ -1,24 +0,0 @@
# 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,30 +1,16 @@
#Modele do obsługi treści: takie jak posty, strony, komentarze i przesłane obrazy.
from django.db import models
from users.models import User
from django.conf import settings
from django.utils import timezone
from django.core.validators import FileExtensionValidator
from django.utils.text import slugify
#Obsługa kategorii
class Category(models.Model):
name = models.CharField(max_length=255)
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)
slug = models.SlugField(unique=True)
def __str__(self):
return self.name
#Obsługa treści
class AbstractContent(models.Model):
STATUS_CHOICES = (
('draft', 'Draft'),
@ -40,24 +26,18 @@ class AbstractContent(models.Model):
updated_at = models.DateTimeField(auto_now=True)
category = models.ForeignKey('Category', on_delete=models.CASCADE)
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:
abstract = True
def __str__(self):
return self.title
#Obsługa postów i stron
class Post(AbstractContent):
pass
class Page(AbstractContent):
pass
#Obsługa komentarzy
class Comment(models.Model):
content = models.TextField(max_length=2000)
approved = models.BooleanField(default=False)
@ -66,7 +46,6 @@ class Comment(models.Model):
page = models.ForeignKey(Page, null=True, blank=True, on_delete=models.CASCADE)
created_at = models.DateTimeField(auto_now_add=True)
#Obsługa przesłanych obrazów
class UploadedImage(models.Model):
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
image = models.ImageField(
@ -76,8 +55,6 @@ class UploadedImage(models.Model):
title = models.CharField(max_length=255)
uploaded_at = models.DateTimeField(auto_now_add=True)
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:
ordering = ['-uploaded_at']
@ -104,7 +81,6 @@ class UploadedImage(models.Model):
uploaded_at__lt=tomorrow
).aggregate(total_size=models.Sum('file_size'))['total_size'] or 0
#Obsługa tagów
class Tag(models.Model):
name = models.CharField(max_length=255)
slug = models.SlugField(unique=True)

View File

@ -1,40 +1,25 @@
import os
from django.utils.text import slugify
from rest_framework import serializers
from .models import Post, Comment, Page, Category, UploadedImage, Tag
from users.models import User
from .models import (
UploadedImage,
Category,
Post,
Comment,
Page,
Tag,
)
# Serialized do tagów
class TagSerializer(serializers.ModelSerializer):
class Meta:
model = Tag
fields = ['id', 'name', 'slug']
read_only_fields = ['id']
# Serialized do kategorii
class CategorySerializer(serializers.ModelSerializer):
class Meta:
model = Category
fields = ['id', 'name', 'slug', 'description', 'parent_category', 'created_at']
read_only_fields = ['id', 'created_at']
fields = ['id', 'name', 'slug']
read_only_fields = ['id']
# Serialized do użytkowników
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ['id', 'username', 'first_name', 'last_name']
read_only_fields = ['id']
# Serialized do komentarzy
class CommentSerializer(serializers.ModelSerializer):
author = UserSerializer(read_only=True)
@ -43,7 +28,6 @@ class CommentSerializer(serializers.ModelSerializer):
fields = ['id', 'content', 'author', 'post', 'page', 'approved', 'created_at']
read_only_fields = ['id', 'created_at', 'author']
# Serialized do postów
class PostSerializer(serializers.ModelSerializer):
author = UserSerializer(read_only=True)
category = CategorySerializer(read_only=True)
@ -63,34 +47,19 @@ class PostSerializer(serializers.ModelSerializer):
)
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:
model = Post
fields = [
'id', 'title', 'content', 'status', 'author',
'category', 'category_id', 'comments',
'created_at', 'updated_at', 'tags', 'tag_ids', 'slug',
'meta_title', 'meta_description', 'meta_image',]
'created_at', 'updated_at', 'tags', 'tag_ids', 'slug'
]
read_only_fields = ['id', 'created_at', 'updated_at', 'author', 'slug']
lookup_field = 'slug'
extra_kwargs = {
'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):
author = UserSerializer(read_only=True)
category = CategorySerializer(read_only=True)
@ -101,25 +70,19 @@ class PageSerializer(serializers.ModelSerializer):
)
comments = CommentSerializer(many=True, read_only=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:
model = Page
fields = [
'id', 'title', 'content', 'status', 'author',
'category', 'category_id', 'comments',
'created_at', 'updated_at', 'slug',
'meta_title', 'meta_description', 'meta_image']
'created_at', 'updated_at', 'slug'
]
read_only_fields = ['id', 'created_at', 'updated_at', 'author', 'slug']
lookup_field = 'slug'
extra_kwargs = {
'url': {'lookup_field': 'slug'}
}
# Serialized do przesłanych obrazów
class UploadedImageSerializer(serializers.ModelSerializer):
url = serializers.SerializerMethodField()
file_size_display = serializers.SerializerMethodField()
@ -127,45 +90,9 @@ class UploadedImageSerializer(serializers.ModelSerializer):
class Meta:
model = UploadedImage
fields = [
'id',
'title',
'image',
'url',
'uploaded_at',
'file_size',
'file_size_display',
'category',
'description',
]
fields = ['id', 'title', 'image', 'url', 'uploaded_at', 'file_size', 'file_size_display']
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):
request = self.context.get('request')
if obj.image and hasattr(obj.image, 'url'):
@ -185,7 +112,4 @@ class UploadedImageSerializer(serializers.ModelSerializer):
raise serializers.ValidationError("Image file is required.")
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,4 +1,3 @@
#Obsługa testów dla aplikacji content
from django.test import TestCase
# Create your tests here.

View File

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

View File

@ -1,48 +1,44 @@
#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 import viewsets, status, filters
from rest_framework.response import Response
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 .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):
"""
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()
serializer_class = PostSerializer
permission_classes = [IsAdminOrReadOnly]
permission_classes = [IsAuthorOrReadOnly]
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
filterset_fields = ['status', 'author', 'category']
search_fields = ['title', 'content']
@ -67,8 +63,7 @@ class PostViewSet(viewsets.ModelViewSet):
def perform_update(self, serializer):
print(serializer.validated_data)
return super().perform_update(serializer)
# Obsługa komentarzy do postów i stron
class CommentViewSet(viewsets.ModelViewSet):
"""
API endpoint for managing comments on posts and pages.
@ -94,7 +89,6 @@ class PostViewSet(viewsets.ModelViewSet):
destroy:
Delete a specific comment. Requires authentication and appropriate permissions.
"""
class CommentViewSet(viewsets.ModelViewSet):
queryset = Comment.objects.all()
serializer_class = CommentSerializer
permission_classes = [IsAuthorOrReadOnly]
@ -118,7 +112,6 @@ class CommentViewSet(viewsets.ModelViewSet):
)
return queryset
#obsługa widoków dla stron
class PageViewSet(viewsets.ModelViewSet):
queryset = Page.objects.all()
serializer_class = PageSerializer
@ -144,7 +137,6 @@ class PageViewSet(viewsets.ModelViewSet):
)
return queryset
# Obsługa kategorii
class CategoryViewSet(viewsets.ModelViewSet):
queryset = Category.objects.all()
serializer_class = CategorySerializer
@ -155,11 +147,9 @@ class CategoryViewSet(viewsets.ModelViewSet):
def get_queryset(self):
return Category.objects.all()
# Obsługa przesyłania obrazów
class UploadedImageViewSet(viewsets.ModelViewSet):
serializer_class = UploadedImageSerializer
parser_classes = (JSONParser, MultiPartParser, FormParser)
permission_classes = [IsAuthenticated]
parser_classes = (MultiPartParser, FormParser)
def get_queryset(self):
return UploadedImage.objects.filter(user=self.request.user)
@ -170,51 +160,28 @@ class UploadedImageViewSet(viewsets.ModelViewSet):
return context
def perform_create(self, serializer):
user = self.request.user
# Check daily upload quota (5MB = 5 * 1024 * 1024 bytes)
DAILY_UPLOAD_LIMIT = 5 * 1024 * 1024 # 5MB in bytes
# Admin? zero limitów
if not user.is_staff:
# Limit: 5MB dziennie
DAILY_UPLOAD_LIMIT = 5 * 1024 * 1024
current_size = UploadedImage.get_user_daily_upload_size(user)
# Get current daily upload size
current_size = UploadedImage.get_user_daily_upload_size(self.request.user)
file_size = self.request.FILES['image'].size
# Check if upload would exceed daily limit
if current_size + file_size > DAILY_UPLOAD_LIMIT:
raise serializers.ValidationError({
'image': 'Daily upload limit (5MB) exceeded. Try again tomorrow.'
'image': 'Daily upload limit (5MB) exceeded. Please try again tomorrow.'
})
# Limit 1 plik / minutę
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.'
})
try:
instance = serializer.save(user=self.request.user, file_size=file_size)
serializer.save(user=user, file_size=self.request.FILES['image'].size)
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)
except Exception as e:
raise
@action(detail=False, methods=['get'])
def quota(self, request):
DAILY_UPLOAD_LIMIT = 5 * 1024 * 1024
DAILY_UPLOAD_LIMIT = 5 * 1024 * 1024 # 5MB in bytes
current_size = UploadedImage.get_user_daily_upload_size(request.user)
remaining = max(0, DAILY_UPLOAD_LIMIT - current_size)
@ -227,62 +194,9 @@ class UploadedImageViewSet(viewsets.ModelViewSet):
'remaining_display': f"{remaining / (1024 * 1024):.1f}MB"
})
# Obsługa tagów
class TagViewSet(viewsets.ModelViewSet):
queryset = Tag.objects.all()
serializer_class = TagSerializer
permission_classes = [IsAuthenticatedOrReadOnly]
filter_backends = [filters.SearchFilter]
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.

View File

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

View File

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

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

View File

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

View File

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

View File

@ -1,65 +0,0 @@
# 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

@ -1,30 +0,0 @@
# 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

@ -1,22 +0,0 @@
# 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

@ -1,31 +0,0 @@
# 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

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

View File

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

@ -1,17 +0,0 @@
# 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

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

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

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

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

View File

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

View File

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

@ -1,8 +0,0 @@
# 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

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

View File

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

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

View File

@ -1,11 +0,0 @@
# 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

@ -1,40 +0,0 @@
# 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

@ -1,21 +0,0 @@
# 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

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

View File

@ -47,7 +47,7 @@ SECRET_KEY = 'django-insecure-^+2p2e93g2_1h-@u&l&j+(9n&6z02-e$pirm6(@@(4$+-zyhxr
ALLOWED_HOSTS = ["localhost", "127.0.0.1"]
ALLOWED_HOSTS = []
CORS_ALLOWED_ORIGINS = [
"http://localhost:5173",
@ -56,14 +56,9 @@ CORS_ALLOWED_ORIGINS = [
CORS_ALLOW_CREDENTIALS = True
CSRF_TRUSTED_ORIGINS = [
"http://localhost:5173",
"http://127.0.0.1:5173",
]
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'users.authentication.VersionedJWTAuthentication',
'rest_framework_simplejwt.authentication.JWTAuthentication',
),
'DEFAULT_PERMISSION_CLASSES': (
'rest_framework.permissions.IsAuthenticated',
@ -73,16 +68,6 @@ REST_FRAMEWORK = {
'rest_framework.renderers.JSONRenderer',
'rest_framework.renderers.BrowsableAPIRenderer',
),
'DEFAULT_THROTTLE_CLASSES': [
'rest_framework.throttling.ScopedRateThrottle',
],
'DEFAULT_THROTTLE_RATES': {
'login': '5/min', # ⬅️ scope do TokenObtainPair
'reset_password': '3/min',
'avatar_upload': '3/minute',
'avatar_get': '100/minute' # ⬅️ scope do resetu hasła
},
}
SIMPLE_JWT = {
@ -131,7 +116,6 @@ INSTALLED_APPS = [
'core',
'users',
'content',
'formulas',
]
MIDDLEWARE = [

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