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.

160
README.md
View File

@ -1,81 +1,81 @@
# Izaac 2.1 Project # Izaac 2.1 Project
This project consists of a Django backend and a React/TypeScript frontend. Below are instructions for setting up and running both parts in development mode. This project consists of a Django backend and a React/TypeScript frontend. Below are instructions for setting up and running both parts in development mode.
## Prerequisites ## Prerequisites
- Python 3.x - Python 3.x
- Node.js (LTS version recommended) - Node.js (LTS version recommended)
- npm or yarn - npm or yarn
- SQLite (included with Python) - SQLite (included with Python)
## Backend Setup ## Backend Setup
1. Navigate to the backend directory: 1. Navigate to the backend directory:
```bash ```bash
cd backend cd backend
``` ```
2. Create and activate a virtual environment: 2. Create and activate a virtual environment:
```bash ```bash
python -m venv venv python -m venv venv
source venv/bin/activate # On Windows use: venv\Scripts\activate source venv/bin/activate # On Windows use: venv\Scripts\activate
``` ```
3. Install dependencies: 3. Install dependencies:
```bash ```bash
pip install -r requirements.txt pip install -r requirements.txt
``` ```
4. Run database migrations: 4. Run database migrations:
```bash ```bash
python manage.py migrate python manage.py migrate
``` ```
5. Start the development server: 5. Start the development server:
```bash ```bash
python manage.py runserver python manage.py runserver
``` ```
The backend will be available at `http://localhost:8000` The backend will be available at `http://localhost:8000`
## Frontend Setup ## Frontend Setup
1. Navigate to the frontend directory: 1. Navigate to the frontend directory:
```bash ```bash
cd frontend cd frontend
``` ```
2. Install dependencies: 2. Install dependencies:
```bash ```bash
npm install npm install
# or if you use yarn: # or if you use yarn:
yarn install yarn install
``` ```
3. Start the development server: 3. Start the development server:
```bash ```bash
npm run dev npm run dev
# or if you use yarn: # or if you use yarn:
yarn dev yarn dev
``` ```
The frontend will be available at `http://localhost:5173` The frontend will be available at `http://localhost:5173`
## Development ## Development
- Backend API documentation is available at `http://localhost:8000/api/docs/` when the server is running - Backend API documentation is available at `http://localhost:8000/api/docs/` when the server is running
- Frontend hot-reloading is enabled by default - Frontend hot-reloading is enabled by default
- Backend hot-reloading is enabled by default with Django's development server - Backend hot-reloading is enabled by default with Django's development server
## Project Structure ## Project Structure
- `/backend` - Django backend application - `/backend` - Django backend application
- `/frontend` - React/TypeScript frontend application - `/frontend` - React/TypeScript frontend application
- `/nginx` - Nginx configuration files - `/nginx` - Nginx configuration files
## Additional Notes ## Additional Notes
- Make sure both backend and frontend servers are running simultaneously for full functionality - Make sure both backend and frontend servers are running simultaneously for full functionality
- The backend uses SQLite as the database by default - The backend uses SQLite as the database by default
- Frontend is built with Vite and uses Tailwind CSS for styling - Frontend is built with Vite and uses Tailwind CSS for styling

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.

21
backend/.gitignore vendored
View File

@ -1,9 +1,12 @@
# Created by venv; see https://docs.python.org/3/library/venv.html
/bin #Ignorowanie plików i katalogów w projekcie backend
/lib # Created by venv; see https://docs.python.org/3/library/venv.html
/include /bin
/share /lib
/man /include
/doc /share
/html /man
pyvenv.cfg /doc
/html
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 @@
from django.contrib import admin #Obsługa panelu administracyjnego dla aplikacji content
from django.contrib import admin
# Register your models here.
# Register your models here.

View File

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

View File

@ -1,59 +1,59 @@
# Generated by Django 4.2 on 2025-05-01 18:16 # Generated by Django 4.2 on 2025-05-01 18:16
from django.db import migrations, models from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
initial = True initial = True
dependencies = [ dependencies = [
] ]
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name='Category', name='Category',
fields=[ fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255)), ('name', models.CharField(max_length=255)),
('slug', models.SlugField(unique=True)), ('slug', models.SlugField(unique=True)),
], ],
), ),
migrations.CreateModel( migrations.CreateModel(
name='Comment', name='Comment',
fields=[ fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('content', models.TextField(max_length=2000)), ('content', models.TextField(max_length=2000)),
('approved', models.BooleanField(default=False)), ('approved', models.BooleanField(default=False)),
('created_at', models.DateTimeField(auto_now_add=True)), ('created_at', models.DateTimeField(auto_now_add=True)),
], ],
), ),
migrations.CreateModel( migrations.CreateModel(
name='Page', name='Page',
fields=[ fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=255)), ('title', models.CharField(max_length=255)),
('content', models.TextField()), ('content', models.TextField()),
('status', models.CharField(choices=[('draft', 'Draft'), ('pending', 'Pending Review'), ('published', 'Published')], default='draft', max_length=10)), ('status', models.CharField(choices=[('draft', 'Draft'), ('pending', 'Pending Review'), ('published', 'Published')], default='draft', max_length=10)),
('created_at', models.DateTimeField(auto_now_add=True)), ('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)), ('updated_at', models.DateTimeField(auto_now=True)),
], ],
options={ options={
'abstract': False, 'abstract': False,
}, },
), ),
migrations.CreateModel( migrations.CreateModel(
name='Post', name='Post',
fields=[ fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=255)), ('title', models.CharField(max_length=255)),
('content', models.TextField()), ('content', models.TextField()),
('status', models.CharField(choices=[('draft', 'Draft'), ('pending', 'Pending Review'), ('published', 'Published')], default='draft', max_length=10)), ('status', models.CharField(choices=[('draft', 'Draft'), ('pending', 'Pending Review'), ('published', 'Published')], default='draft', max_length=10)),
('created_at', models.DateTimeField(auto_now_add=True)), ('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)), ('updated_at', models.DateTimeField(auto_now=True)),
], ],
options={ options={
'abstract': False, 'abstract': False,
}, },
), ),
] ]

View File

@ -1,53 +1,53 @@
# Generated by Django 4.2 on 2025-05-01 18:16 # Generated by Django 4.2 on 2025-05-01 18:16
from django.conf import settings from django.conf import settings
from django.db import migrations, models from django.db import migrations, models
import django.db.models.deletion import django.db.models.deletion
class Migration(migrations.Migration): class Migration(migrations.Migration):
initial = True initial = True
dependencies = [ dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL), migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('content', '0001_initial'), ('content', '0001_initial'),
] ]
operations = [ operations = [
migrations.AddField( migrations.AddField(
model_name='post', model_name='post',
name='author', name='author',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
), ),
migrations.AddField( migrations.AddField(
model_name='post', model_name='post',
name='category', name='category',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='content.category'), field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='content.category'),
), ),
migrations.AddField( migrations.AddField(
model_name='page', model_name='page',
name='author', name='author',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
), ),
migrations.AddField( migrations.AddField(
model_name='page', model_name='page',
name='category', name='category',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='content.category'), field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='content.category'),
), ),
migrations.AddField( migrations.AddField(
model_name='comment', model_name='comment',
name='author', name='author',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
), ),
migrations.AddField( migrations.AddField(
model_name='comment', model_name='comment',
name='page', name='page',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='content.page'), field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='content.page'),
), ),
migrations.AddField( migrations.AddField(
model_name='comment', model_name='comment',
name='post', name='post',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='content.post'), field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='content.post'),
), ),
] ]

View File

@ -1,30 +1,30 @@
from django.db import migrations from django.db import migrations
def create_initial_categories(apps, schema_editor): def create_initial_categories(apps, schema_editor):
Category = apps.get_model('content', 'Category') Category = apps.get_model('content', 'Category')
default_categories = [ default_categories = [
{'name': 'General', 'slug': 'general'}, {'name': 'General', 'slug': 'general'},
{'name': 'Technology', 'slug': 'technology'}, {'name': 'Technology', 'slug': 'technology'},
{'name': 'Science', 'slug': 'science'}, {'name': 'Science', 'slug': 'science'},
{'name': 'Programming', 'slug': 'programming'}, {'name': 'Programming', 'slug': 'programming'},
{'name': 'News', 'slug': 'news'}, {'name': 'News', 'slug': 'news'},
] ]
for category in default_categories: for category in default_categories:
Category.objects.get_or_create( Category.objects.get_or_create(
name=category['name'], name=category['name'],
slug=category['slug'] slug=category['slug']
) )
def remove_initial_categories(apps, schema_editor): def remove_initial_categories(apps, schema_editor):
Category = apps.get_model('content', 'Category') Category = apps.get_model('content', 'Category')
Category.objects.filter(slug__in=['general', 'technology', 'science', 'programming', 'news']).delete() Category.objects.filter(slug__in=['general', 'technology', 'science', 'programming', 'news']).delete()
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('content', '0001_initial'), ('content', '0001_initial'),
] ]
operations = [ operations = [
migrations.RunPython(create_initial_categories, remove_initial_categories), migrations.RunPython(create_initial_categories, remove_initial_categories),
] ]

View File

@ -1,31 +1,31 @@
# Generated by Django 4.2 on 2025-05-01 19:05 # Generated by Django 4.2 on 2025-05-01 19:05
from django.conf import settings from django.conf import settings
import django.core.validators import django.core.validators
from django.db import migrations, models from django.db import migrations, models
import django.db.models.deletion import django.db.models.deletion
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL), migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('content', '0002_initial'), ('content', '0002_initial'),
] ]
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name='UploadedImage', name='UploadedImage',
fields=[ fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('image', models.ImageField(upload_to='uploads/%Y/%m/%d/', validators=[django.core.validators.FileExtensionValidator(allowed_extensions=['jpg', 'jpeg', 'png', 'gif', 'webp'])])), ('image', models.ImageField(upload_to='uploads/%Y/%m/%d/', validators=[django.core.validators.FileExtensionValidator(allowed_extensions=['jpg', 'jpeg', 'png', 'gif', 'webp'])])),
('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()), ('file_size', models.PositiveIntegerField()),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
], ],
options={ options={
'ordering': ['-uploaded_at'], 'ordering': ['-uploaded_at'],
}, },
), ),
] ]

View File

@ -1,14 +1,14 @@
# Generated by Django 4.2 on 2025-05-01 19:19 # Generated by Django 4.2 on 2025-05-01 19:19
from django.db import migrations from django.db import migrations
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('content', '0002_initial_categories'), ('content', '0002_initial_categories'),
('content', '0003_uploadedimage'), ('content', '0003_uploadedimage'),
] ]
operations = [ operations = [
] ]

View File

@ -1,31 +1,31 @@
# Generated by Django 4.2 on 2025-05-04 10:54 # Generated by Django 4.2 on 2025-05-04 10:54
from django.db import migrations, models from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('content', '0004_merge_0002_initial_categories_0003_uploadedimage'), ('content', '0004_merge_0002_initial_categories_0003_uploadedimage'),
] ]
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name='Tag', name='Tag',
fields=[ fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255)), ('name', models.CharField(max_length=255)),
('slug', models.SlugField(unique=True)), ('slug', models.SlugField(unique=True)),
], ],
), ),
migrations.AddField( migrations.AddField(
model_name='page', model_name='page',
name='tags', name='tags',
field=models.ManyToManyField(blank=True, to='content.tag'), field=models.ManyToManyField(blank=True, to='content.tag'),
), ),
migrations.AddField( migrations.AddField(
model_name='post', model_name='post',
name='tags', name='tags',
field=models.ManyToManyField(blank=True, to='content.tag'), field=models.ManyToManyField(blank=True, to='content.tag'),
), ),
] ]

View File

@ -1,23 +1,23 @@
# Generated by Django 4.2 on 2025-05-10 23:27 # Generated by Django 4.2 on 2025-05-10 23:27
from django.db import migrations, models from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('content', '0005_tag_page_tags_post_tags'), ('content', '0005_tag_page_tags_post_tags'),
] ]
operations = [ operations = [
migrations.AddField( migrations.AddField(
model_name='page', model_name='page',
name='slug', name='slug',
field=models.SlugField(blank=True, unique=True), field=models.SlugField(blank=True, unique=True),
), ),
migrations.AddField( migrations.AddField(
model_name='post', model_name='post',
name='slug', name='slug',
field=models.SlugField(blank=True, unique=True), field=models.SlugField(blank=True, unique=True),
), ),
] ]

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,88 +1,112 @@
from django.db import models #Modele do obsługi treści: takie jak posty, strony, komentarze i przesłane obrazy.
from users.models import User
from django.conf import settings from django.db import models
from django.utils import timezone from users.models import User
from django.core.validators import FileExtensionValidator from django.conf import settings
from django.utils import timezone
class Category(models.Model): from django.core.validators import FileExtensionValidator
name = models.CharField(max_length=255) from django.utils.text import slugify
slug = models.SlugField(unique=True)
#Obsługa kategorii
def __str__(self): class Category(models.Model):
return self.name name = models.CharField(max_length=255)
slug = models.SlugField(unique=True, blank=True)
class AbstractContent(models.Model): description = models.TextField(blank=True, null=True)
STATUS_CHOICES = ( parent_category = models.ForeignKey('self', null=True, blank=True, on_delete=models.CASCADE, related_name='subcategories')
('draft', 'Draft'), created_at = models.DateTimeField(auto_now_add=True)
('pending', 'Pending Review'), meta_description = models.CharField(max_length=160, blank=True, null=True, help_text="Opis dla wyszukiwarek (SEO)")
('published', 'Published'),
) def save(self, *args, **kwargs):
title = models.CharField(max_length=255) if not self.slug:
slug = models.SlugField(unique=True, blank=True) self.slug = slugify(self.name)
content = models.TextField() super().save(*args, **kwargs)
status = models.CharField(max_length=10, choices=STATUS_CHOICES, default='draft')
author = models.ForeignKey(User, on_delete=models.CASCADE) def __str__(self):
created_at = models.DateTimeField(auto_now_add=True) return self.name
updated_at = models.DateTimeField(auto_now=True)
category = models.ForeignKey('Category', on_delete=models.CASCADE) #Obsługa treści
tags = models.ManyToManyField('Tag', blank=True) class AbstractContent(models.Model):
class Meta: STATUS_CHOICES = (
abstract = True ('draft', 'Draft'),
('pending', 'Pending Review'),
def __str__(self): ('published', 'Published'),
return self.title )
title = models.CharField(max_length=255)
class Post(AbstractContent): slug = models.SlugField(unique=True, blank=True)
pass content = models.TextField()
status = models.CharField(max_length=10, choices=STATUS_CHOICES, default='draft')
class Page(AbstractContent): author = models.ForeignKey(User, on_delete=models.CASCADE)
pass created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Comment(models.Model): category = models.ForeignKey('Category', on_delete=models.CASCADE)
content = models.TextField(max_length=2000) tags = models.ManyToManyField('Tag', blank=True)
approved = models.BooleanField(default=False) meta_title = models.CharField(max_length=70, blank=True, help_text="Tytuł wyświetlany w wyszukiwarkach. Domyślnie będzie użyty title.")
author = models.ForeignKey(User, on_delete=models.CASCADE) meta_description = models.CharField(max_length=160, blank=True, null=True, help_text="Opis dla wyszukiwarek (SEO)")
post = models.ForeignKey(Post, null=True, blank=True, on_delete=models.CASCADE) meta_image = models.URLField(blank=True, null=True, help_text="Obrazek do podglądu w linkach (np. na Facebooku)")
page = models.ForeignKey(Page, null=True, blank=True, on_delete=models.CASCADE)
created_at = models.DateTimeField(auto_now_add=True) class Meta:
abstract = True
class UploadedImage(models.Model):
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) def __str__(self):
image = models.ImageField( return self.title
upload_to='uploads/%Y/%m/%d/',
validators=[FileExtensionValidator(allowed_extensions=['jpg', 'jpeg', 'png', 'gif', 'webp'])] #Obsługa postów i stron
) class Post(AbstractContent):
title = models.CharField(max_length=255) pass
uploaded_at = models.DateTimeField(auto_now_add=True)
file_size = models.PositiveIntegerField() # Size in bytes class Page(AbstractContent):
pass
class Meta:
ordering = ['-uploaded_at'] #Obsługa komentarzy
class Comment(models.Model):
def __str__(self): content = models.TextField(max_length=2000)
return f"{self.title} - {self.user.username}" approved = models.BooleanField(default=False)
author = models.ForeignKey(User, on_delete=models.CASCADE)
@property post = models.ForeignKey(Post, null=True, blank=True, on_delete=models.CASCADE)
def url(self): page = models.ForeignKey(Page, null=True, blank=True, on_delete=models.CASCADE)
return self.image.url if self.image else None created_at = models.DateTimeField(auto_now_add=True)
def save(self, *args, **kwargs): #Obsługa przesłanych obrazów
if not self.file_size and self.image: class UploadedImage(models.Model):
self.file_size = self.image.size user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
super().save(*args, **kwargs) image = models.ImageField(
upload_to='uploads/%Y/%m/%d/',
@classmethod validators=[FileExtensionValidator(allowed_extensions=['jpg', 'jpeg', 'png', 'gif', 'webp'])]
def get_user_daily_upload_size(cls, user): )
today = timezone.now().date() title = models.CharField(max_length=255)
tomorrow = today + timezone.timedelta(days=1) uploaded_at = models.DateTimeField(auto_now_add=True)
return cls.objects.filter( file_size = models.PositiveIntegerField() # Size in bytes
user=user, category = models.ForeignKey('Category', on_delete=models.CASCADE, default=1)
uploaded_at__gte=today, description = models.TextField(blank=True, null=True, help_text="Opis obrazka")
uploaded_at__lt=tomorrow
).aggregate(total_size=models.Sum('file_size'))['total_size'] or 0 class Meta:
ordering = ['-uploaded_at']
class Tag(models.Model):
name = models.CharField(max_length=255) def __str__(self):
slug = models.SlugField(unique=True) return f"{self.title} - {self.user.username}"
def __str__(self):
@property
def url(self):
return self.image.url if self.image else None
def save(self, *args, **kwargs):
if not self.file_size and self.image:
self.file_size = self.image.size
super().save(*args, **kwargs)
@classmethod
def get_user_daily_upload_size(cls, user):
today = timezone.now().date()
tomorrow = today + timezone.timedelta(days=1)
return cls.objects.filter(
user=user,
uploaded_at__gte=today,
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)
def __str__(self):
return self.name return self.name

View File

@ -1,115 +1,191 @@
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
class TagSerializer(serializers.ModelSerializer):
class Meta: from users.models import User
model = Tag from .models import (
fields = ['id', 'name', 'slug'] UploadedImage,
read_only_fields = ['id'] Category,
Post,
class CategorySerializer(serializers.ModelSerializer): Comment,
class Meta: Page,
model = Category Tag,
fields = ['id', 'name', 'slug'] )
read_only_fields = ['id']
# Serialized do tagów
class UserSerializer(serializers.ModelSerializer): class TagSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = User model = Tag
fields = ['id', 'username', 'first_name', 'last_name'] fields = ['id', 'name', 'slug']
read_only_fields = ['id'] read_only_fields = ['id']
class CommentSerializer(serializers.ModelSerializer): # Serialized do kategorii
author = UserSerializer(read_only=True) class CategorySerializer(serializers.ModelSerializer):
class Meta:
class Meta: model = Category
model = Comment fields = ['id', 'name', 'slug', 'description', 'parent_category', 'created_at']
fields = ['id', 'content', 'author', 'post', 'page', 'approved', 'created_at'] read_only_fields = ['id', 'created_at']
read_only_fields = ['id', 'created_at', 'author']
# Serialized do użytkowników
class PostSerializer(serializers.ModelSerializer): class UserSerializer(serializers.ModelSerializer):
author = UserSerializer(read_only=True) class Meta:
category = CategorySerializer(read_only=True) model = User
category_id = serializers.PrimaryKeyRelatedField( fields = ['id', 'username', 'first_name', 'last_name']
queryset=Category.objects.all(), read_only_fields = ['id']
source='category',
write_only=True # Serialized do komentarzy
) class CommentSerializer(serializers.ModelSerializer):
comments = CommentSerializer(many=True, read_only=True) author = UserSerializer(read_only=True)
tags = TagSerializer(many=True, read_only=True)
tag_ids = serializers.PrimaryKeyRelatedField( class Meta:
queryset=Tag.objects.all(), model = Comment
source='tags', fields = ['id', 'content', 'author', 'post', 'page', 'approved', 'created_at']
many=True, read_only_fields = ['id', 'created_at', 'author']
write_only=True,
required=False # Serialized do postów
) class PostSerializer(serializers.ModelSerializer):
slug = serializers.SlugField(required=True) author = UserSerializer(read_only=True)
category = CategorySerializer(read_only=True)
class Meta: category_id = serializers.PrimaryKeyRelatedField(
model = Post queryset=Category.objects.all(),
fields = [ source='category',
'id', 'title', 'content', 'status', 'author', write_only=True
'category', 'category_id', 'comments', )
'created_at', 'updated_at', 'tags', 'tag_ids', 'slug' comments = CommentSerializer(many=True, read_only=True)
] tags = TagSerializer(many=True, read_only=True)
read_only_fields = ['id', 'created_at', 'updated_at', 'author', 'slug'] tag_ids = serializers.PrimaryKeyRelatedField(
lookup_field = 'slug' queryset=Tag.objects.all(),
extra_kwargs = { source='tags',
'url': {'lookup_field': 'slug'} many=True,
} write_only=True,
required=False
class PageSerializer(serializers.ModelSerializer): )
author = UserSerializer(read_only=True) slug = serializers.SlugField(required=True)
category = CategorySerializer(read_only=True)
category_id = serializers.PrimaryKeyRelatedField( meta_title = serializers.CharField(required=False, allow_blank=True)
queryset=Category.objects.all(), meta_description = serializers.CharField(required=False, allow_blank=True)
source='category', meta_image = serializers.CharField(required=False, allow_null=True)
write_only=True
) class Meta:
comments = CommentSerializer(many=True, read_only=True) model = Post
slug = serializers.SlugField(required=True) fields = [
class Meta: 'id', 'title', 'content', 'status', 'author',
model = Page 'category', 'category_id', 'comments',
fields = [ 'created_at', 'updated_at', 'tags', 'tag_ids', 'slug',
'id', 'title', 'content', 'status', 'author', 'meta_title', 'meta_description', 'meta_image',]
'category', 'category_id', 'comments', read_only_fields = ['id', 'created_at', 'updated_at', 'author', 'slug']
'created_at', 'updated_at', 'slug' lookup_field = 'slug'
] extra_kwargs = {
read_only_fields = ['id', 'created_at', 'updated_at', 'author', 'slug'] 'url': {'lookup_field': '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', ''))
class UploadedImageSerializer(serializers.ModelSerializer): return super().create(validated_data)
url = serializers.SerializerMethodField()
file_size_display = serializers.SerializerMethodField() def update(self, instance, validated_data):
image = serializers.ImageField(required=True) if not validated_data.get('slug') and validated_data.get('title'):
validated_data['slug'] = slugify(validated_data['title'])
class Meta: return super().update(instance, validated_data)
model = UploadedImage
fields = ['id', 'title', 'image', 'url', 'uploaded_at', 'file_size', 'file_size_display'] # Serialized do stron
read_only_fields = ['file_size', 'uploaded_at', 'url'] class PageSerializer(serializers.ModelSerializer):
author = UserSerializer(read_only=True)
def get_url(self, obj): category = CategorySerializer(read_only=True)
request = self.context.get('request') category_id = serializers.PrimaryKeyRelatedField(
if obj.image and hasattr(obj.image, 'url'): queryset=Category.objects.all(),
return request.build_absolute_uri(obj.image.url) if request else obj.image.url source='category',
return None write_only=True
)
def get_file_size_display(self, obj): comments = CommentSerializer(many=True, read_only=True)
"""Convert bytes to human readable format""" slug = serializers.SlugField(required=True)
for unit in ['B', 'KB', 'MB', 'GB']:
if obj.file_size < 1024.0: meta_title = serializers.CharField(required=False, allow_blank=True)
return f"{obj.file_size:.1f} {unit}" meta_description = serializers.CharField(required=False, allow_blank=True)
obj.file_size /= 1024.0 meta_image = serializers.ImageField(required=False, allow_null=True)
return f"{obj.file_size:.1f} TB"
class Meta:
def validate_image(self, value): model = Page
if not value: fields = [
raise serializers.ValidationError("Image file is required.") 'id', 'title', 'content', 'status', 'author',
return value 'category', 'category_id', 'comments',
'created_at', 'updated_at', 'slug',
'meta_title', 'meta_description', 'meta_image']
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()
image = serializers.ImageField(required=True)
class Meta:
model = UploadedImage
fields = [
'id',
'title',
'image',
'url',
'uploaded_at',
'file_size',
'file_size_display',
'category',
'description',
]
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'):
return request.build_absolute_uri(obj.image.url) if request else obj.image.url
return None
def get_file_size_display(self, obj):
"""Convert bytes to human readable format"""
for unit in ['B', 'KB', 'MB', 'GB']:
if obj.file_size < 1024.0:
return f"{obj.file_size:.1f} {unit}"
obj.file_size /= 1024.0
return f"{obj.file_size:.1f} TB"
def validate_image(self, value):
if not value:
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,3 +1,4 @@
from django.test import TestCase #Obsługa testów dla aplikacji content
from django.test import TestCase
# Create your tests here.
# Create your tests here.

View File

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

View File

@ -1,202 +1,288 @@
from rest_framework import viewsets, status, filters #Obsługa widoków dla aplikacji content.
from rest_framework.response import Response import os
from rest_framework.decorators import action
from django_filters.rest_framework import DjangoFilterBackend from django.conf import settings
from .models import Post, Comment, Page, Category, UploadedImage, Tag from django.contrib.auth import get_user_model
from .serializers import PostSerializer, CommentSerializer, PageSerializer, CategorySerializer, UploadedImageSerializer, TagSerializer from django.db.models import Q
from users.permissions import IsAuthorOrReadOnly, IsEditorOrAdmin from django.shortcuts import get_object_or_404
from rest_framework.permissions import IsAuthenticatedOrReadOnly from django.utils import timezone
from rest_framework.parsers import MultiPartParser, FormParser
from django.conf import settings from rest_framework import viewsets, status, filters, serializers
from rest_framework import serializers from rest_framework.views import APIView
from rest_framework.response import Response
class PostViewSet(viewsets.ModelViewSet): from rest_framework.decorators import action
""" from rest_framework.permissions import (
API endpoint for managing blog posts. IsAuthenticated,
IsAuthenticatedOrReadOnly,
list: AllowAny,
Return a list of all posts. Results can be filtered by: )
- status (draft/pending/published) from rest_framework.parsers import JSONParser, MultiPartParser, FormParser
- author
- category from django_filters.rest_framework import DjangoFilterBackend
- created_at (date range)
from .models import (
create: Post,
Create a new post. Requires authentication and appropriate permissions. Comment,
Page,
retrieve: Category,
Return the details of a specific post by ID. UploadedImage,
Tag,
update: )
Update all fields of a specific post. Requires authentication and appropriate permissions. from .serializers import (
PostSerializer,
partial_update: CommentSerializer,
Update one or more fields of a specific post. Requires authentication and appropriate permissions. PageSerializer,
CategorySerializer,
destroy: UploadedImageSerializer,
Delete a specific post. Requires authentication and appropriate permissions. TagSerializer,
""" )
queryset = Post.objects.all() from users.permissions import IsAuthorOrReadOnly, IsEditorOrAdmin, IsAdminOrReadOnly
serializer_class = PostSerializer
permission_classes = [IsAuthorOrReadOnly] # Obsługa logiczna widoku dla postów
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter] class PostViewSet(viewsets.ModelViewSet):
filterset_fields = ['status', 'author', 'category'] queryset = Post.objects.all()
search_fields = ['title', 'content'] serializer_class = PostSerializer
ordering_fields = ['created_at', 'updated_at', 'title'] permission_classes = [IsAdminOrReadOnly]
ordering = ['-created_at'] filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
lookup_field = 'slug' filterset_fields = ['status', 'author', 'category']
search_fields = ['title', 'content']
def perform_create(self, serializer): ordering_fields = ['created_at', 'updated_at', 'title']
serializer.save(author=self.request.user) ordering = ['-created_at']
lookup_field = 'slug'
def get_queryset(self):
queryset = Post.objects.all() def perform_create(self, serializer):
if not self.request.user.is_authenticated: serializer.save(author=self.request.user)
queryset = queryset.filter(status='published')
elif not self.request.user.role in ['admin', 'moderator']: def get_queryset(self):
queryset = queryset.filter( queryset = Post.objects.all()
status='published' if not self.request.user.is_authenticated:
) | Post.objects.filter( queryset = queryset.filter(status='published')
author=self.request.user elif not self.request.user.role in ['admin', 'moderator']:
) queryset = queryset.filter(
return queryset status='published'
def perform_update(self, serializer): ) | Post.objects.filter(
print(serializer.validated_data) author=self.request.user
return super().perform_update(serializer) )
class CommentViewSet(viewsets.ModelViewSet): return queryset
""" def perform_update(self, serializer):
API endpoint for managing comments on posts and pages. print(serializer.validated_data)
return super().perform_update(serializer)
list:
Return a list of all comments. Results can be filtered by: # Obsługa komentarzy do postów i stron
- post_id """
- page_id API endpoint for managing comments on posts and pages.
- author
- approved status list:
Return a list of all comments. Results can be filtered by:
create: - post_id
Create a new comment. Requires authentication. - page_id
- author
retrieve: - approved status
Return the details of a specific comment by ID.
create:
update: Create a new comment. Requires authentication.
Update all fields of a specific comment. Requires authentication and appropriate permissions.
retrieve:
partial_update: Return the details of a specific comment by ID.
Update one or more fields of a specific comment. Requires authentication and appropriate permissions.
update:
destroy: Update all fields of a specific comment. Requires authentication and appropriate permissions.
Delete a specific comment. Requires authentication and appropriate permissions.
""" partial_update:
queryset = Comment.objects.all() Update one or more fields of a specific comment. Requires authentication and appropriate permissions.
serializer_class = CommentSerializer
permission_classes = [IsAuthorOrReadOnly] destroy:
filter_backends = [DjangoFilterBackend, filters.OrderingFilter] Delete a specific comment. Requires authentication and appropriate permissions.
filterset_fields = ['post', 'page', 'author', 'approved'] """
ordering_fields = ['created_at'] class CommentViewSet(viewsets.ModelViewSet):
ordering = ['-created_at'] queryset = Comment.objects.all()
serializer_class = CommentSerializer
def perform_create(self, serializer): permission_classes = [IsAuthorOrReadOnly]
serializer.save(author=self.request.user) filter_backends = [DjangoFilterBackend, filters.OrderingFilter]
filterset_fields = ['post', 'page', 'author', 'approved']
def get_queryset(self): ordering_fields = ['created_at']
queryset = Comment.objects.all() ordering = ['-created_at']
if not self.request.user.is_authenticated:
queryset = queryset.filter(approved=True) def perform_create(self, serializer):
elif not self.request.user.role in ['admin', 'moderator']: serializer.save(author=self.request.user)
queryset = queryset.filter(
approved=True def get_queryset(self):
) | Comment.objects.filter( queryset = Comment.objects.all()
author=self.request.user if not self.request.user.is_authenticated:
) queryset = queryset.filter(approved=True)
return queryset elif not self.request.user.role in ['admin', 'moderator']:
queryset = queryset.filter(
class PageViewSet(viewsets.ModelViewSet): approved=True
queryset = Page.objects.all() ) | Comment.objects.filter(
serializer_class = PageSerializer author=self.request.user
permission_classes = [IsEditorOrAdmin] )
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter] return queryset
filterset_fields = ['status', 'author', 'category']
search_fields = ['title', 'content'] #obsługa widoków dla stron
ordering_fields = ['created_at', 'updated_at', 'title'] class PageViewSet(viewsets.ModelViewSet):
ordering = ['-created_at'] queryset = Page.objects.all()
serializer_class = PageSerializer
def perform_create(self, serializer): permission_classes = [IsEditorOrAdmin]
serializer.save(author=self.request.user) filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
filterset_fields = ['status', 'author', 'category']
def get_queryset(self): search_fields = ['title', 'content']
queryset = Page.objects.all() ordering_fields = ['created_at', 'updated_at', 'title']
if not self.request.user.is_authenticated: ordering = ['-created_at']
queryset = queryset.filter(status='published')
elif not self.request.user.role in ['admin', 'moderator']: def perform_create(self, serializer):
queryset = queryset.filter( serializer.save(author=self.request.user)
status='published'
) | Page.objects.filter( def get_queryset(self):
author=self.request.user queryset = Page.objects.all()
) if not self.request.user.is_authenticated:
return queryset queryset = queryset.filter(status='published')
elif not self.request.user.role in ['admin', 'moderator']:
class CategoryViewSet(viewsets.ModelViewSet): queryset = queryset.filter(
queryset = Category.objects.all() status='published'
serializer_class = CategorySerializer ) | Page.objects.filter(
permission_classes = [IsAuthenticatedOrReadOnly] author=self.request.user
filter_backends = [filters.SearchFilter] )
search_fields = ['name', 'slug'] return queryset
def get_queryset(self): # Obsługa kategorii
return Category.objects.all() class CategoryViewSet(viewsets.ModelViewSet):
queryset = Category.objects.all()
class UploadedImageViewSet(viewsets.ModelViewSet): serializer_class = CategorySerializer
serializer_class = UploadedImageSerializer permission_classes = [IsAuthenticatedOrReadOnly]
parser_classes = (MultiPartParser, FormParser) filter_backends = [filters.SearchFilter]
search_fields = ['name', 'slug']
def get_queryset(self):
return UploadedImage.objects.filter(user=self.request.user) def get_queryset(self):
return Category.objects.all()
def get_serializer_context(self):
context = super().get_serializer_context() # Obsługa przesyłania obrazów
context['request'] = self.request class UploadedImageViewSet(viewsets.ModelViewSet):
return context serializer_class = UploadedImageSerializer
parser_classes = (JSONParser, MultiPartParser, FormParser)
def perform_create(self, serializer): permission_classes = [IsAuthenticated]
# Check daily upload quota (5MB = 5 * 1024 * 1024 bytes)
DAILY_UPLOAD_LIMIT = 5 * 1024 * 1024 # 5MB in bytes def get_queryset(self):
return UploadedImage.objects.filter(user=self.request.user)
# Get current daily upload size
current_size = UploadedImage.get_user_daily_upload_size(self.request.user) def get_serializer_context(self):
file_size = self.request.FILES['image'].size context = super().get_serializer_context()
context['request'] = self.request
# Check if upload would exceed daily limit return context
if current_size + file_size > DAILY_UPLOAD_LIMIT:
raise serializers.ValidationError({ def perform_create(self, serializer):
'image': 'Daily upload limit (5MB) exceeded. Please try again tomorrow.' user = self.request.user
})
# Admin? zero limitów
try: if not user.is_staff:
instance = serializer.save(user=self.request.user, file_size=file_size) # Limit: 5MB dziennie
DAILY_UPLOAD_LIMIT = 5 * 1024 * 1024
except Exception as e: current_size = UploadedImage.get_user_daily_upload_size(user)
raise file_size = self.request.FILES['image'].size
@action(detail=False, methods=['get']) if current_size + file_size > DAILY_UPLOAD_LIMIT:
def quota(self, request): raise serializers.ValidationError({
DAILY_UPLOAD_LIMIT = 5 * 1024 * 1024 # 5MB in bytes 'image': 'Daily upload limit (5MB) exceeded. Try again tomorrow.'
current_size = UploadedImage.get_user_daily_upload_size(request.user) })
remaining = max(0, DAILY_UPLOAD_LIMIT - current_size)
# Limit 1 plik / minutę
return Response({ one_minute_ago = timezone.now() - timezone.timedelta(minutes=1)
'daily_limit': DAILY_UPLOAD_LIMIT, if UploadedImage.objects.filter(user=user, uploaded_at__gte=one_minute_ago).exists():
'used': current_size, raise serializers.ValidationError({
'remaining': remaining, 'image': 'Upload limit: 1 image per minute.'
'daily_limit_display': f"{DAILY_UPLOAD_LIMIT / (1024 * 1024):.1f}MB", })
'used_display': f"{current_size / (1024 * 1024):.1f}MB",
'remaining_display': f"{remaining / (1024 * 1024):.1f}MB" serializer.save(user=user, file_size=self.request.FILES['image'].size)
})
def perform_update(self, serializer):
class TagViewSet(viewsets.ModelViewSet): instance = serializer.instance
queryset = Tag.objects.all() if instance.user != self.request.user and not self.request.user.is_staff:
serializer_class = TagSerializer raise serializers.ValidationError("Brak uprawnień do edycji tego pliku.")
permission_classes = [IsAuthenticatedOrReadOnly] serializer.save()
filter_backends = [filters.SearchFilter]
search_fields = ['name', 'slug'] 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'])
def quota(self, request):
DAILY_UPLOAD_LIMIT = 5 * 1024 * 1024
current_size = UploadedImage.get_user_daily_upload_size(request.user)
remaining = max(0, DAILY_UPLOAD_LIMIT - current_size)
return Response({
'daily_limit': DAILY_UPLOAD_LIMIT,
'used': current_size,
'remaining': remaining,
'daily_limit_display': f"{DAILY_UPLOAD_LIMIT / (1024 * 1024):.1f}MB",
'used_display': f"{current_size / (1024 * 1024):.1f}MB",
'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.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

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

View File

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

View File

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

View File

@ -1,60 +1,60 @@
INSTALLED_APPS = [ INSTALLED_APPS = [
'django.contrib.admin', 'django.contrib.admin',
'django.contrib.auth', 'django.contrib.auth',
'django.contrib.contenttypes', 'django.contrib.contenttypes',
'django.contrib.sessions', 'django.contrib.sessions',
'django.contrib.messages', 'django.contrib.messages',
'django.contrib.staticfiles', 'django.contrib.staticfiles',
'rest_framework', 'rest_framework',
'rest_framework_simplejwt', 'rest_framework_simplejwt',
'rest_framework_simplejwt.token_blacklist', 'rest_framework_simplejwt.token_blacklist',
'corsheaders', 'corsheaders',
'django_filters', 'django_filters',
'users', 'users',
'content', 'content',
] ]
REST_FRAMEWORK = { REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [ 'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework_simplejwt.authentication.JWTAuthentication', 'rest_framework_simplejwt.authentication.JWTAuthentication',
], ],
'DEFAULT_PERMISSION_CLASSES': [ 'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticated', 'rest_framework.permissions.IsAuthenticated',
], ],
'DEFAULT_FILTER_BACKENDS': [ 'DEFAULT_FILTER_BACKENDS': [
'django_filters.rest_framework.DjangoFilterBackend', 'django_filters.rest_framework.DjangoFilterBackend',
'rest_framework.filters.SearchFilter', 'rest_framework.filters.SearchFilter',
'rest_framework.filters.OrderingFilter', 'rest_framework.filters.OrderingFilter',
], ],
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination', 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
'PAGE_SIZE': 10 'PAGE_SIZE': 10
} }
from datetime import timedelta from datetime import timedelta
SIMPLE_JWT = { SIMPLE_JWT = {
'ACCESS_TOKEN_LIFETIME': timedelta(minutes=60), 'ACCESS_TOKEN_LIFETIME': timedelta(minutes=60),
'REFRESH_TOKEN_LIFETIME': timedelta(days=1), 'REFRESH_TOKEN_LIFETIME': timedelta(days=1),
'ROTATE_REFRESH_TOKENS': True, 'ROTATE_REFRESH_TOKENS': True,
'BLACKLIST_AFTER_ROTATION': True, 'BLACKLIST_AFTER_ROTATION': True,
'UPDATE_LAST_LOGIN': False, 'UPDATE_LAST_LOGIN': False,
'ALGORITHM': 'HS256', 'ALGORITHM': 'HS256',
'SIGNING_KEY': SECRET_KEY, 'SIGNING_KEY': SECRET_KEY,
'VERIFYING_KEY': None, 'VERIFYING_KEY': None,
'AUDIENCE': None, 'AUDIENCE': None,
'ISSUER': None, 'ISSUER': None,
'JWK_URL': None, 'JWK_URL': None,
'LEEWAY': 0, 'LEEWAY': 0,
'AUTH_HEADER_TYPES': ('Bearer',), 'AUTH_HEADER_TYPES': ('Bearer',),
'AUTH_HEADER_NAME': 'HTTP_AUTHORIZATION', 'AUTH_HEADER_NAME': 'HTTP_AUTHORIZATION',
'USER_ID_FIELD': 'id', 'USER_ID_FIELD': 'id',
'USER_ID_CLAIM': 'user_id', 'USER_ID_CLAIM': 'user_id',
'USER_AUTHENTICATION_RULE': 'rest_framework_simplejwt.authentication.default_user_authentication_rule', 'USER_AUTHENTICATION_RULE': 'rest_framework_simplejwt.authentication.default_user_authentication_rule',
'AUTH_TOKEN_CLASSES': ('rest_framework_simplejwt.tokens.AccessToken',), 'AUTH_TOKEN_CLASSES': ('rest_framework_simplejwt.tokens.AccessToken',),
'TOKEN_TYPE_CLAIM': 'token_type', 'TOKEN_TYPE_CLAIM': 'token_type',
'TOKEN_USER_CLASS': 'rest_framework_simplejwt.models.TokenUser', 'TOKEN_USER_CLASS': 'rest_framework_simplejwt.models.TokenUser',
'JTI_CLAIM': 'jti', 'JTI_CLAIM': 'jti',
} }

View File

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

View File

@ -1,3 +1,3 @@
from django.shortcuts import render from django.shortcuts import render
# Create your views here. # Create your views here.

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

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