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
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
- Python 3.x
- Node.js (LTS version recommended)
- npm or yarn
- SQLite (included with Python)
## Backend Setup
1. Navigate to the backend directory:
```bash
cd backend
```
2. Create and activate a virtual environment:
```bash
python -m venv venv
source venv/bin/activate # On Windows use: venv\Scripts\activate
```
3. Install dependencies:
```bash
pip install -r requirements.txt
```
4. Run database migrations:
```bash
python manage.py migrate
```
5. Start the development server:
```bash
python manage.py runserver
```
The backend will be available at `http://localhost:8000`
## Frontend Setup
1. Navigate to the frontend directory:
```bash
cd frontend
```
2. Install dependencies:
```bash
npm install
# or if you use yarn:
yarn install
```
3. Start the development server:
```bash
npm run dev
# or if you use yarn:
yarn dev
```
The frontend will be available at `http://localhost:5173`
## Development
- Backend API documentation is available at `http://localhost:8000/api/docs/` when the server is running
- Frontend hot-reloading is enabled by default
- Backend hot-reloading is enabled by default with Django's development server
## Project Structure
- `/backend` - Django backend application
- `/frontend` - React/TypeScript frontend application
- `/nginx` - Nginx configuration files
## Additional Notes
- Make sure both backend and frontend servers are running simultaneously for full functionality
- The backend uses SQLite as the database by default
# 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.
## Prerequisites
- Python 3.x
- Node.js (LTS version recommended)
- npm or yarn
- SQLite (included with Python)
## Backend Setup
1. Navigate to the backend directory:
```bash
cd backend
```
2. Create and activate a virtual environment:
```bash
python -m venv venv
source venv/bin/activate # On Windows use: venv\Scripts\activate
```
3. Install dependencies:
```bash
pip install -r requirements.txt
```
4. Run database migrations:
```bash
python manage.py migrate
```
5. Start the development server:
```bash
python manage.py runserver
```
The backend will be available at `http://localhost:8000`
## Frontend Setup
1. Navigate to the frontend directory:
```bash
cd frontend
```
2. Install dependencies:
```bash
npm install
# or if you use yarn:
yarn install
```
3. Start the development server:
```bash
npm run dev
# or if you use yarn:
yarn dev
```
The frontend will be available at `http://localhost:5173`
## Development
- Backend API documentation is available at `http://localhost:8000/api/docs/` when the server is running
- Frontend hot-reloading is enabled by default
- Backend hot-reloading is enabled by default with Django's development server
## Project Structure
- `/backend` - Django backend application
- `/frontend` - React/TypeScript frontend application
- `/nginx` - Nginx configuration files
## Additional Notes
- Make sure both backend and frontend servers are running simultaneously for full functionality
- The backend uses SQLite as the database by default
- 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
/lib
/include
/share
/man
/doc
/html
pyvenv.cfg
#Ignorowanie plików i katalogów w projekcie backend
# Created by venv; see https://docs.python.org/3/library/venv.html
/bin
/lib
/include
/share
/man
/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
# Register your models here.
#Obsługa panelu administracyjnego dla aplikacji content
from django.contrib import admin
# Register your models here.

View File

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

View File

@ -1,53 +1,53 @@
# Generated by Django 4.2 on 2025-05-01 18:16
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('content', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='post',
name='author',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='post',
name='category',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='content.category'),
),
migrations.AddField(
model_name='page',
name='author',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='page',
name='category',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='content.category'),
),
migrations.AddField(
model_name='comment',
name='author',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='comment',
name='page',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='content.page'),
),
migrations.AddField(
model_name='comment',
name='post',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='content.post'),
),
]
# Generated by Django 4.2 on 2025-05-01 18:16
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('content', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='post',
name='author',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='post',
name='category',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='content.category'),
),
migrations.AddField(
model_name='page',
name='author',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='page',
name='category',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='content.category'),
),
migrations.AddField(
model_name='comment',
name='author',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='comment',
name='page',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='content.page'),
),
migrations.AddField(
model_name='comment',
name='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
def create_initial_categories(apps, schema_editor):
Category = apps.get_model('content', 'Category')
default_categories = [
{'name': 'General', 'slug': 'general'},
{'name': 'Technology', 'slug': 'technology'},
{'name': 'Science', 'slug': 'science'},
{'name': 'Programming', 'slug': 'programming'},
{'name': 'News', 'slug': 'news'},
]
for category in default_categories:
Category.objects.get_or_create(
name=category['name'],
slug=category['slug']
)
def remove_initial_categories(apps, schema_editor):
Category = apps.get_model('content', 'Category')
Category.objects.filter(slug__in=['general', 'technology', 'science', 'programming', 'news']).delete()
class Migration(migrations.Migration):
dependencies = [
('content', '0001_initial'),
]
operations = [
migrations.RunPython(create_initial_categories, remove_initial_categories),
from django.db import migrations
def create_initial_categories(apps, schema_editor):
Category = apps.get_model('content', 'Category')
default_categories = [
{'name': 'General', 'slug': 'general'},
{'name': 'Technology', 'slug': 'technology'},
{'name': 'Science', 'slug': 'science'},
{'name': 'Programming', 'slug': 'programming'},
{'name': 'News', 'slug': 'news'},
]
for category in default_categories:
Category.objects.get_or_create(
name=category['name'],
slug=category['slug']
)
def remove_initial_categories(apps, schema_editor):
Category = apps.get_model('content', 'Category')
Category.objects.filter(slug__in=['general', 'technology', 'science', 'programming', 'news']).delete()
class Migration(migrations.Migration):
dependencies = [
('content', '0001_initial'),
]
operations = [
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
from django.conf import settings
import django.core.validators
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('content', '0002_initial'),
]
operations = [
migrations.CreateModel(
name='UploadedImage',
fields=[
('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'])])),
('title', models.CharField(max_length=255)),
('uploaded_at', models.DateTimeField(auto_now_add=True)),
('file_size', models.PositiveIntegerField()),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
options={
'ordering': ['-uploaded_at'],
},
),
]
# Generated by Django 4.2 on 2025-05-01 19:05
from django.conf import settings
import django.core.validators
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('content', '0002_initial'),
]
operations = [
migrations.CreateModel(
name='UploadedImage',
fields=[
('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'])])),
('title', models.CharField(max_length=255)),
('uploaded_at', models.DateTimeField(auto_now_add=True)),
('file_size', models.PositiveIntegerField()),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
options={
'ordering': ['-uploaded_at'],
},
),
]

View File

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

View File

@ -1,31 +1,31 @@
# Generated by Django 4.2 on 2025-05-04 10:54
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('content', '0004_merge_0002_initial_categories_0003_uploadedimage'),
]
operations = [
migrations.CreateModel(
name='Tag',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255)),
('slug', models.SlugField(unique=True)),
],
),
migrations.AddField(
model_name='page',
name='tags',
field=models.ManyToManyField(blank=True, to='content.tag'),
),
migrations.AddField(
model_name='post',
name='tags',
field=models.ManyToManyField(blank=True, to='content.tag'),
),
]
# Generated by Django 4.2 on 2025-05-04 10:54
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('content', '0004_merge_0002_initial_categories_0003_uploadedimage'),
]
operations = [
migrations.CreateModel(
name='Tag',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255)),
('slug', models.SlugField(unique=True)),
],
),
migrations.AddField(
model_name='page',
name='tags',
field=models.ManyToManyField(blank=True, to='content.tag'),
),
migrations.AddField(
model_name='post',
name='tags',
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
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('content', '0005_tag_page_tags_post_tags'),
]
operations = [
migrations.AddField(
model_name='page',
name='slug',
field=models.SlugField(blank=True, unique=True),
),
migrations.AddField(
model_name='post',
name='slug',
field=models.SlugField(blank=True, unique=True),
),
]
# Generated by Django 4.2 on 2025-05-10 23:27
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('content', '0005_tag_page_tags_post_tags'),
]
operations = [
migrations.AddField(
model_name='page',
name='slug',
field=models.SlugField(blank=True, unique=True),
),
migrations.AddField(
model_name='post',
name='slug',
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
from users.models import User
from django.conf import settings
from django.utils import timezone
from django.core.validators import FileExtensionValidator
class Category(models.Model):
name = models.CharField(max_length=255)
slug = models.SlugField(unique=True)
def __str__(self):
return self.name
class AbstractContent(models.Model):
STATUS_CHOICES = (
('draft', 'Draft'),
('pending', 'Pending Review'),
('published', 'Published'),
)
title = models.CharField(max_length=255)
slug = models.SlugField(unique=True, blank=True)
content = models.TextField()
status = models.CharField(max_length=10, choices=STATUS_CHOICES, default='draft')
author = models.ForeignKey(User, on_delete=models.CASCADE)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
category = models.ForeignKey('Category', on_delete=models.CASCADE)
tags = models.ManyToManyField('Tag', blank=True)
class Meta:
abstract = True
def __str__(self):
return self.title
class Post(AbstractContent):
pass
class Page(AbstractContent):
pass
class Comment(models.Model):
content = models.TextField(max_length=2000)
approved = models.BooleanField(default=False)
author = models.ForeignKey(User, on_delete=models.CASCADE)
post = models.ForeignKey(Post, null=True, blank=True, on_delete=models.CASCADE)
page = models.ForeignKey(Page, null=True, blank=True, on_delete=models.CASCADE)
created_at = models.DateTimeField(auto_now_add=True)
class UploadedImage(models.Model):
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
image = models.ImageField(
upload_to='uploads/%Y/%m/%d/',
validators=[FileExtensionValidator(allowed_extensions=['jpg', 'jpeg', 'png', 'gif', 'webp'])]
)
title = models.CharField(max_length=255)
uploaded_at = models.DateTimeField(auto_now_add=True)
file_size = models.PositiveIntegerField() # Size in bytes
class Meta:
ordering = ['-uploaded_at']
def __str__(self):
return f"{self.title} - {self.user.username}"
@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
class Tag(models.Model):
name = models.CharField(max_length=255)
slug = models.SlugField(unique=True)
def __str__(self):
#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)
def __str__(self):
return self.name
#Obsługa treści
class AbstractContent(models.Model):
STATUS_CHOICES = (
('draft', 'Draft'),
('pending', 'Pending Review'),
('published', 'Published'),
)
title = models.CharField(max_length=255)
slug = models.SlugField(unique=True, blank=True)
content = models.TextField()
status = models.CharField(max_length=10, choices=STATUS_CHOICES, default='draft')
author = models.ForeignKey(User, on_delete=models.CASCADE)
created_at = models.DateTimeField(auto_now_add=True)
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)
author = models.ForeignKey(User, on_delete=models.CASCADE)
post = models.ForeignKey(Post, null=True, blank=True, on_delete=models.CASCADE)
page = models.ForeignKey(Page, null=True, blank=True, on_delete=models.CASCADE)
created_at = models.DateTimeField(auto_now_add=True)
#Obsługa przesłanych obrazów
class UploadedImage(models.Model):
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
image = models.ImageField(
upload_to='uploads/%Y/%m/%d/',
validators=[FileExtensionValidator(allowed_extensions=['jpg', 'jpeg', 'png', 'gif', 'webp'])]
)
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']
def __str__(self):
return f"{self.title} - {self.user.username}"
@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

View File

@ -1,115 +1,191 @@
from rest_framework import serializers
from .models import Post, Comment, Page, Category, UploadedImage, Tag
from users.models import User
class TagSerializer(serializers.ModelSerializer):
class Meta:
model = Tag
fields = ['id', 'name', 'slug']
read_only_fields = ['id']
class CategorySerializer(serializers.ModelSerializer):
class Meta:
model = Category
fields = ['id', 'name', 'slug']
read_only_fields = ['id']
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ['id', 'username', 'first_name', 'last_name']
read_only_fields = ['id']
class CommentSerializer(serializers.ModelSerializer):
author = UserSerializer(read_only=True)
class Meta:
model = Comment
fields = ['id', 'content', 'author', 'post', 'page', 'approved', 'created_at']
read_only_fields = ['id', 'created_at', 'author']
class PostSerializer(serializers.ModelSerializer):
author = UserSerializer(read_only=True)
category = CategorySerializer(read_only=True)
category_id = serializers.PrimaryKeyRelatedField(
queryset=Category.objects.all(),
source='category',
write_only=True
)
comments = CommentSerializer(many=True, read_only=True)
tags = TagSerializer(many=True, read_only=True)
tag_ids = serializers.PrimaryKeyRelatedField(
queryset=Tag.objects.all(),
source='tags',
many=True,
write_only=True,
required=False
)
slug = serializers.SlugField(required=True)
class Meta:
model = Post
fields = [
'id', 'title', 'content', 'status', 'author',
'category', 'category_id', 'comments',
'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'}
}
class PageSerializer(serializers.ModelSerializer):
author = UserSerializer(read_only=True)
category = CategorySerializer(read_only=True)
category_id = serializers.PrimaryKeyRelatedField(
queryset=Category.objects.all(),
source='category',
write_only=True
)
comments = CommentSerializer(many=True, read_only=True)
slug = serializers.SlugField(required=True)
class Meta:
model = Page
fields = [
'id', 'title', 'content', 'status', 'author',
'category', 'category_id', 'comments',
'created_at', 'updated_at', 'slug'
]
read_only_fields = ['id', 'created_at', 'updated_at', 'author', 'slug']
lookup_field = 'slug'
extra_kwargs = {
'url': {'lookup_field': 'slug'}
}
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']
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):
"""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
import os
from django.utils.text import slugify
from rest_framework import serializers
from users.models import User
from .models import (
UploadedImage,
Category,
Post,
Comment,
Page,
Tag,
)
# Serialized do tagów
class TagSerializer(serializers.ModelSerializer):
class 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']
# 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)
class Meta:
model = Comment
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)
category_id = serializers.PrimaryKeyRelatedField(
queryset=Category.objects.all(),
source='category',
write_only=True
)
comments = CommentSerializer(many=True, read_only=True)
tags = TagSerializer(many=True, read_only=True)
tag_ids = serializers.PrimaryKeyRelatedField(
queryset=Tag.objects.all(),
source='tags',
many=True,
write_only=True,
required=False
)
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',]
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)
category_id = serializers.PrimaryKeyRelatedField(
queryset=Category.objects.all(),
source='category',
write_only=True
)
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']
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
# Create your tests here.
#Obsługa testów dla aplikacji content
from django.test import TestCase
# Create your tests here.

View File

@ -1,14 +1,18 @@
from rest_framework.routers import DefaultRouter
from django.urls import path, include
from .views import PostViewSet, CommentViewSet, PageViewSet, CategoryViewSet, UploadedImageViewSet, TagViewSet
router = DefaultRouter()
router.register(r'posts', PostViewSet, basename='post')
router.register(r'comments', CommentViewSet, basename='comment')
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)),
]
#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
router = DefaultRouter()
router.register(r'posts', PostViewSet, basename='post')
router.register(r'comments', CommentViewSet, basename='comment')
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,202 +1,288 @@
from rest_framework import viewsets, status, filters
from rest_framework.response import Response
from rest_framework.decorators import action
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
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 = [IsAuthorOrReadOnly]
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
filterset_fields = ['status', 'author', 'category']
search_fields = ['title', 'content']
ordering_fields = ['created_at', 'updated_at', 'title']
ordering = ['-created_at']
lookup_field = 'slug'
def perform_create(self, serializer):
serializer.save(author=self.request.user)
def get_queryset(self):
queryset = Post.objects.all()
if not self.request.user.is_authenticated:
queryset = queryset.filter(status='published')
elif not self.request.user.role in ['admin', 'moderator']:
queryset = queryset.filter(
status='published'
) | Post.objects.filter(
author=self.request.user
)
return queryset
def perform_update(self, serializer):
print(serializer.validated_data)
return super().perform_update(serializer)
class CommentViewSet(viewsets.ModelViewSet):
"""
API endpoint for managing comments on posts and pages.
list:
Return a list of all comments. Results can be filtered by:
- post_id
- page_id
- author
- approved status
create:
Create a new comment. Requires authentication.
retrieve:
Return the details of a specific comment by ID.
update:
Update all fields of a specific comment. Requires authentication and appropriate permissions.
partial_update:
Update one or more fields of a specific comment. Requires authentication and appropriate permissions.
destroy:
Delete a specific comment. Requires authentication and appropriate permissions.
"""
queryset = Comment.objects.all()
serializer_class = CommentSerializer
permission_classes = [IsAuthorOrReadOnly]
filter_backends = [DjangoFilterBackend, filters.OrderingFilter]
filterset_fields = ['post', 'page', 'author', 'approved']
ordering_fields = ['created_at']
ordering = ['-created_at']
def perform_create(self, serializer):
serializer.save(author=self.request.user)
def get_queryset(self):
queryset = Comment.objects.all()
if not self.request.user.is_authenticated:
queryset = queryset.filter(approved=True)
elif not self.request.user.role in ['admin', 'moderator']:
queryset = queryset.filter(
approved=True
) | Comment.objects.filter(
author=self.request.user
)
return queryset
class PageViewSet(viewsets.ModelViewSet):
queryset = Page.objects.all()
serializer_class = PageSerializer
permission_classes = [IsEditorOrAdmin]
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
filterset_fields = ['status', 'author', 'category']
search_fields = ['title', 'content']
ordering_fields = ['created_at', 'updated_at', 'title']
ordering = ['-created_at']
def perform_create(self, serializer):
serializer.save(author=self.request.user)
def get_queryset(self):
queryset = Page.objects.all()
if not self.request.user.is_authenticated:
queryset = queryset.filter(status='published')
elif not self.request.user.role in ['admin', 'moderator']:
queryset = queryset.filter(
status='published'
) | Page.objects.filter(
author=self.request.user
)
return queryset
class CategoryViewSet(viewsets.ModelViewSet):
queryset = Category.objects.all()
serializer_class = CategorySerializer
permission_classes = [IsAuthenticatedOrReadOnly]
filter_backends = [filters.SearchFilter]
search_fields = ['name', 'slug']
def get_queryset(self):
return Category.objects.all()
class UploadedImageViewSet(viewsets.ModelViewSet):
serializer_class = UploadedImageSerializer
parser_classes = (MultiPartParser, FormParser)
def get_queryset(self):
return UploadedImage.objects.filter(user=self.request.user)
def get_serializer_context(self):
context = super().get_serializer_context()
context['request'] = self.request
return context
def perform_create(self, serializer):
# Check daily upload quota (5MB = 5 * 1024 * 1024 bytes)
DAILY_UPLOAD_LIMIT = 5 * 1024 * 1024 # 5MB in bytes
# 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. Please try again tomorrow.'
})
try:
instance = serializer.save(user=self.request.user, file_size=file_size)
except Exception as e:
raise
@action(detail=False, methods=['get'])
def quota(self, request):
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)
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"
})
class TagViewSet(viewsets.ModelViewSet):
queryset = Tag.objects.all()
serializer_class = TagSerializer
permission_classes = [IsAuthenticatedOrReadOnly]
filter_backends = [filters.SearchFilter]
search_fields = ['name', 'slug']
#Obsługa widoków dla aplikacji content.
import os
from django.conf import settings
from django.contrib.auth import get_user_model
from django.db.models import Q
from django.shortcuts import get_object_or_404
from django.utils import timezone
from rest_framework import viewsets, status, filters, serializers
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.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, IsAdminOrReadOnly
# Obsługa logiczna widoku dla postów
class PostViewSet(viewsets.ModelViewSet):
queryset = Post.objects.all()
serializer_class = PostSerializer
permission_classes = [IsAdminOrReadOnly]
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
filterset_fields = ['status', 'author', 'category']
search_fields = ['title', 'content']
ordering_fields = ['created_at', 'updated_at', 'title']
ordering = ['-created_at']
lookup_field = 'slug'
def perform_create(self, serializer):
serializer.save(author=self.request.user)
def get_queryset(self):
queryset = Post.objects.all()
if not self.request.user.is_authenticated:
queryset = queryset.filter(status='published')
elif not self.request.user.role in ['admin', 'moderator']:
queryset = queryset.filter(
status='published'
) | Post.objects.filter(
author=self.request.user
)
return queryset
def perform_update(self, serializer):
print(serializer.validated_data)
return super().perform_update(serializer)
# Obsługa komentarzy do postów i stron
"""
API endpoint for managing comments on posts and pages.
list:
Return a list of all comments. Results can be filtered by:
- post_id
- page_id
- author
- approved status
create:
Create a new comment. Requires authentication.
retrieve:
Return the details of a specific comment by ID.
update:
Update all fields of a specific comment. Requires authentication and appropriate permissions.
partial_update:
Update one or more fields of a specific comment. Requires authentication and appropriate permissions.
destroy:
Delete a specific comment. Requires authentication and appropriate permissions.
"""
class CommentViewSet(viewsets.ModelViewSet):
queryset = Comment.objects.all()
serializer_class = CommentSerializer
permission_classes = [IsAuthorOrReadOnly]
filter_backends = [DjangoFilterBackend, filters.OrderingFilter]
filterset_fields = ['post', 'page', 'author', 'approved']
ordering_fields = ['created_at']
ordering = ['-created_at']
def perform_create(self, serializer):
serializer.save(author=self.request.user)
def get_queryset(self):
queryset = Comment.objects.all()
if not self.request.user.is_authenticated:
queryset = queryset.filter(approved=True)
elif not self.request.user.role in ['admin', 'moderator']:
queryset = queryset.filter(
approved=True
) | Comment.objects.filter(
author=self.request.user
)
return queryset
#obsługa widoków dla stron
class PageViewSet(viewsets.ModelViewSet):
queryset = Page.objects.all()
serializer_class = PageSerializer
permission_classes = [IsEditorOrAdmin]
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
filterset_fields = ['status', 'author', 'category']
search_fields = ['title', 'content']
ordering_fields = ['created_at', 'updated_at', 'title']
ordering = ['-created_at']
def perform_create(self, serializer):
serializer.save(author=self.request.user)
def get_queryset(self):
queryset = Page.objects.all()
if not self.request.user.is_authenticated:
queryset = queryset.filter(status='published')
elif not self.request.user.role in ['admin', 'moderator']:
queryset = queryset.filter(
status='published'
) | Page.objects.filter(
author=self.request.user
)
return queryset
# Obsługa kategorii
class CategoryViewSet(viewsets.ModelViewSet):
queryset = Category.objects.all()
serializer_class = CategorySerializer
permission_classes = [IsAuthenticatedOrReadOnly]
filter_backends = [filters.SearchFilter]
search_fields = ['name', 'slug']
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]
def get_queryset(self):
return UploadedImage.objects.filter(user=self.request.user)
def get_serializer_context(self):
context = super().get_serializer_context()
context['request'] = self.request
return context
def perform_create(self, serializer):
user = self.request.user
# 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)
file_size = self.request.FILES['image'].size
if current_size + file_size > DAILY_UPLOAD_LIMIT:
raise serializers.ValidationError({
'image': 'Daily upload limit (5MB) exceeded. 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.'
})
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)
@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
# Register your models here.
from django.contrib import admin
# Register your models here.

View File

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

View File

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

View File

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

View File

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

View File

@ -1,3 +1,3 @@
from django.shortcuts import render
# Create your views here.
from django.shortcuts import render
# 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