prvi kod za live uporabu
Some checks failed
ERP CI Pipeline / test (push) Has been cancelled

This commit is contained in:
mariomitte
2026-07-09 21:21:17 +02:00
parent 857bb65d52
commit 5d39e048ba
239 changed files with 25151 additions and 0 deletions

View File

View File

@@ -0,0 +1,27 @@
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from .models import CustomUser
@admin.register(CustomUser)
class CustomUserAdmin(UserAdmin):
model = CustomUser
# Definiramo koja polja se vide u admin listi i formama
list_display = ('email', 'first_name', 'last_name', 'occupation', 'work_position', 'residence', 'is_serviser', 'is_kupac', 'is_team_member', 'is_active', 'client_profile')
fieldsets = (
(None, {'fields': ('email', 'password')}),
('Osobni podaci', {'fields': ('first_name', 'last_name', 'telefon', 'oib', 'occupation', 'work_position', 'residence')}),
('ERP Status', {'fields': ('is_active', 'is_staff', 'is_superuser', 'is_serviser', 'is_kupac', 'is_team_member', 'is_verified', 'client_profile')}),
('Važni datumi', {'fields': ('last_login', 'date_joined')}),
)
# 3. Add fieldsets (za kreiranje novog korisnika u adminu)
add_fieldsets = (
(None, {
'classes': ('wide',),
'fields': ('email', 'password', 'is_team_member'),
}),
)
search_fields = ('email', 'first_name', 'last_name', 'occupation', 'work_position', 'residence')
ordering = ('email',)
raw_id_fields = ('client_profile',)

View File

@@ -0,0 +1,7 @@
# backend/core/users/apps.py
from django.apps import AppConfig
class UsersConfig(AppConfig):
name = 'core.users' # Ovo je putanja do modula
label = 'users' # Ovo je kratko ime koje Django koristi (app_label)
verbose_name = 'User Management'

View File

@@ -0,0 +1,2 @@
from core.management.commands.seed_all_models import Command # noqa: F401

View File

@@ -0,0 +1,2 @@
from core.management.commands.seed_fleet_assets import Command # noqa: F401

View File

@@ -0,0 +1,107 @@
"""
seed_knezljubo.py
Kreira demo klijenta "Knezljubo d.o.o." s jednim fleet vozilom (is_company_vehicle=True)
i jednim vozilom u vlasništvu klijenta (is_company_vehicle=False).
Idempotentna: ponavljanjem ne kreira duplikate (koristi get_or_create).
Uporaba:
python manage.py seed_knezljubo
docker compose exec -it backend python manage.py seed_knezljubo
"""
from django.core.management.base import BaseCommand
from django.db import transaction
from modules.crm.models import Client
from modules.fleet.models import Vehicle
KNEZLJUBO_TAX_ID = "12345678901"
FLEET_VIN = "VIN_FLEET_KNEZLJUBO001"
CLIENT_VIN = "VIN_CLIENT_KNEZLJUBO002"
class Command(BaseCommand):
help = "Kreira klijenta Knezljubo s fleet vozilom i vozilom u vlasništvu klijenta."
def handle(self, *args, **options):
with transaction.atomic():
client = self._seed_client()
fleet_vehicle = self._seed_fleet_vehicle(client)
client_vehicle = self._seed_client_vehicle(client)
self.stdout.write(self.style.SUCCESS(
f"\nZavršeno!\n"
f" Klijent : {client} (id={client.id})\n"
f" Fleet vozilo : {fleet_vehicle} [is_company_vehicle=True]\n"
f" Klijentovo : {client_vehicle} [is_company_vehicle=False]\n"
))
# ── Klijent ───────────────────────────────────────────────────────────────
def _seed_client(self) -> Client:
client, created = Client.objects.get_or_create(
tax_id=KNEZLJUBO_TAX_ID,
defaults={
"name": "Knezljubo d.o.o.",
"client_type": "legal",
"email": "info@knezljubo.hr",
"phone": "+385912345678",
"address": "Ilica 100",
"city": "Zagreb",
"postal_code": "10000",
},
)
action = "Kreiran" if created else "Već postoji"
self.stdout.write(self.style.SUCCESS(f"Client: {action}{client}"))
return client
# ── Fleet vozilo (in vlasništvu naše tvrtke, ali vezano za klijenta) ──────
def _seed_fleet_vehicle(self, client: Client) -> Vehicle:
"""
Vozilo je dio naše fleet-a (is_company_vehicle=True) ALI je operativno
vezano za klijenta Knezljubo — npr. vozilo koje servisiramo po ugovoru.
"""
vehicle, created = Vehicle.objects.get_or_create(
vin=FLEET_VIN,
defaults={
"registration_number": "ZG-0001-KZ",
"make": "Mercedes",
"model": "Sprinter",
"year": 2021,
"current_mileage": 48500,
"service_interval_km": 15000,
"is_company_vehicle": True, # dio naše flote
"client": client, # operativno vezan za klijenta
},
)
action = "Kreiran" if created else "Već postoji"
self.stdout.write(self.style.SUCCESS(f"Fleet vozilo: {action}{vehicle}"))
return vehicle
# ── Vozilo u vlasništvu klijenta ──────────────────────────────────────────
def _seed_client_vehicle(self, client: Client) -> Vehicle:
"""
Vozilo koje KLIJENT posjeduje — naša tvrtka ga servisira.
is_company_vehicle=False → filter u admin i frontend pokazuje ovo kao klijentovo.
"""
vehicle, created = Vehicle.objects.get_or_create(
vin=CLIENT_VIN,
defaults={
"registration_number": "ZG-0002-KZ",
"make": "VW",
"model": "Crafter",
"year": 2019,
"current_mileage": 112000,
"service_interval_km": 10000,
"is_company_vehicle": False, # klijentovo vozilo
"client": client,
},
)
action = "Kreiran" if created else "Već postoji"
self.stdout.write(self.style.SUCCESS(f"Klijentovo vozilo: {action}{vehicle}"))
return vehicle

View File

@@ -0,0 +1,52 @@
# backend/core/users/management/commands/seed_users.py
from django.core.management.base import BaseCommand
from django.contrib.auth import get_user_model
from django.db import transaction
from modules.crm.models import Client
User = get_user_model()
class Command(BaseCommand):
help = 'Kreira testne korisnike za razvojno okruženje'
def handle(self, *args, **kwargs):
users_to_create = [
{
"email": "serviser@primjer.hr",
"first_name": "Ivan",
"last_name": "Serviser",
"is_serviser": True,
"is_team_member": True,
"password": "password123"
},
{
"email": "kupac@primjer.hr",
"first_name": "Marko",
"last_name": "Kupac",
"is_kupac": True,
"password": "password123"
}
]
with transaction.atomic():
for user_data in users_to_create:
email = user_data.pop('email')
password = user_data.pop('password')
if user_data.get('is_kupac'):
client, _ = Client.objects.get_or_create(name="Testni Klijent DOO")
user_data['client_profile'] = client
# Provjeri postoji li korisnik
if not User.objects.filter(email=email).exists():
# KORISTI MANAGER UMJESTO MODELA
user = User.objects.create_user(
email=email,
password=password,
**user_data
)
self.stdout.write(self.style.SUCCESS(f'Kreiran korisnik: {email}'))
else:
self.stdout.write(self.style.WARNING(f'Korisnik već postoji: {email}'))
self.stdout.write(self.style.SUCCESS('Seeding završen!'))

View File

@@ -0,0 +1,30 @@
from django.contrib.auth.models import BaseUserManager
class CustomUserManager(BaseUserManager):
"""
Custom manager za User model gdje koristimo email kao identifikator.
"""
def create_user(self, email, password=None, **extra_fields):
if not email:
raise ValueError('Email adresa mora biti zadana.')
extra_fields.pop('username', None)
email = self.normalize_email(email)
user = self.model(email=email, **extra_fields)
user.set_password(password)
user.save(using=self._db)
return user
def create_superuser(self, email, password=None, **extra_fields):
extra_fields.setdefault('is_staff', True)
extra_fields.setdefault('is_superuser', True)
extra_fields.setdefault('is_active', True)
extra_fields.setdefault('is_serviser', True)
if extra_fields.get('is_staff') is not True:
raise ValueError('Superuser mora imati is_staff=True.')
if extra_fields.get('is_superuser') is not True:
raise ValueError('Superuser mora imati is_superuser=True.')
return self.create_user(email, password, **extra_fields)

View File

@@ -0,0 +1,47 @@
# Generated by Django 5.2.15 on 2026-06-29 13:34
import django.contrib.auth.models
import django.contrib.auth.validators
import django.utils.timezone
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('auth', '0012_alter_user_first_name_max_length'),
]
operations = [
migrations.CreateModel(
name='CustomUser',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('password', models.CharField(max_length=128, verbose_name='password')),
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')),
('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')),
('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
('email', models.EmailField(max_length=254, unique=True)),
('telefon', models.CharField(blank=True, max_length=20)),
('oib', models.CharField(blank=True, max_length=11, null=True)),
('is_serviser', models.BooleanField(default=False)),
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')),
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')),
],
options={
'verbose_name': 'user',
'verbose_name_plural': 'users',
'abstract': False,
},
managers=[
('objects', django.contrib.auth.models.UserManager()),
],
),
]

View File

@@ -0,0 +1,40 @@
# Generated by Django 5.2.15 on 2026-06-29 20:12
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('crm', '__first__'),
('users', '0001_initial'),
]
operations = [
migrations.AlterModelManagers(
name='customuser',
managers=[
],
),
migrations.AddField(
model_name='customuser',
name='client_profile',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='users', to='crm.client', verbose_name='Povezani klijent'),
),
migrations.AddField(
model_name='customuser',
name='is_kupac',
field=models.BooleanField(default=False, verbose_name='Kupac'),
),
migrations.AddField(
model_name='customuser',
name='is_verified',
field=models.BooleanField(default=False, verbose_name='Verificiran profil'),
),
migrations.AddField(
model_name='customuser',
name='licence_number',
field=models.CharField(blank=True, max_length=50, null=True, verbose_name='Broj licence'),
),
]

View File

@@ -0,0 +1,17 @@
# Generated by Django 5.2.15 on 2026-06-30 06:10
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('users', '0002_alter_customuser_managers_customuser_client_profile_and_more'),
]
operations = [
migrations.RemoveField(
model_name='customuser',
name='username',
),
]

View File

@@ -0,0 +1,19 @@
# Generated by Django 5.2.15 on 2026-07-06 14:58
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('users', '0003_remove_customuser_username'),
]
operations = [
migrations.AddField(
model_name='customuser',
name='is_team_member',
field=models.BooleanField(default=False, verbose_name='Član tima'),
),
]

View File

@@ -0,0 +1,21 @@
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('users', '0004_customuser_is_team_member'),
]
operations = [
migrations.AddField(
model_name='customuser',
name='occupation',
field=models.CharField(blank=True, default='', max_length=120, verbose_name='Zanimanje'),
),
migrations.AddField(
model_name='customuser',
name='residence',
field=models.CharField(blank=True, default='', max_length=255, verbose_name='Prebivalište'),
),
]

View File

@@ -0,0 +1,16 @@
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('users', '0005_customuser_occupation_residence'),
]
operations = [
migrations.AddField(
model_name='customuser',
name='work_position',
field=models.CharField(blank=True, default='', max_length=120, verbose_name='Na radnom mjestu'),
),
]

View File

@@ -0,0 +1,52 @@
from django.contrib.auth.models import AbstractUser
from django.core.exceptions import ValidationError
from django.db import models
from .managers import CustomUserManager
class CustomUser(AbstractUser):
# Email koristimo za login, pa mora biti jedinstven
username=None
email = models.EmailField(unique=True)
# ERP Specifična polja
is_serviser = models.BooleanField(default=False) # Razlikovanje uloga
is_kupac = models.BooleanField(default=False, verbose_name="Kupac")
is_team_member = models.BooleanField(default=False, verbose_name="Član tima")
# Dodatne informacije o servisera
telefon = models.CharField(max_length=20, blank=True)
oib = models.CharField(max_length=11, blank=True, null=True)
occupation = models.CharField(max_length=120, blank=True, default='', verbose_name="Zanimanje")
residence = models.CharField(max_length=255, blank=True, default='', verbose_name="Prebivalište")
work_position = models.CharField(max_length=120, blank=True, default='', verbose_name="Na radnom mjestu")
licence_number = models.CharField(max_length=50, blank=True, null=True, verbose_name="Broj licence")
is_verified = models.BooleanField(default=False, verbose_name="Verificiran profil")
# Poveznica na klijenta (ako je korisnik kupac)
client_profile = models.ForeignKey(
'crm.Client',
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='users', # Omogućuje pristup: klijent.users.all()
verbose_name="Povezani klijent"
)
objects = CustomUserManager()
# Govorimo Djangu da koristi email umjesto username-a
USERNAME_FIELD = 'email'
REQUIRED_FIELDS = ['first_name', 'last_name']
def __str__(self):
return f"{self.first_name} {self.last_name} ({self.email})"
def clean(self):
# if self.email == "bot@erp.hr":
# return # Preskoči validaciju za bota
super().clean()
if self.is_kupac and not self.client_profile:
raise ValidationError({'client_profile': "Korisnik koji je označen kao kupac mora imati povezan klijentski profil."})
def save(self, *args, **kwargs):
self.full_clean() # Prisiljava pokretanje clean() prije svakog save()
super().save(*args, **kwargs)

View File

@@ -0,0 +1,73 @@
from rest_framework import serializers
from django.core.exceptions import ObjectDoesNotExist
from .models import CustomUser
class UserSerializer(serializers.ModelSerializer):
assigned_vehicle = serializers.SerializerMethodField()
assigned_crane = serializers.SerializerMethodField()
class Meta:
model = CustomUser
fields = ['id', 'email', 'first_name', 'last_name', 'is_serviser', 'is_kupac', 'is_team_member', 'telefon', 'oib', 'occupation', 'residence', 'work_position', 'client_profile', 'assigned_vehicle', 'assigned_crane']
read_only_fields = ['id', 'email']
extra_kwargs = {
'first_name': {'required': True, 'allow_blank': False}
}
def _get_assigned_asset(self, obj):
try:
return obj.assigned_vehicles
except ObjectDoesNotExist:
return None
def _serialize_asset(self, vehicle):
if not vehicle:
return None
return {
'id': vehicle.id,
'asset_type': vehicle.asset_type,
'registration_number': vehicle.registration_number,
'make': vehicle.make,
'model': vehicle.model,
'current_mileage': vehicle.current_mileage,
'service_interval_km': vehicle.service_interval_km,
}
def get_assigned_vehicle(self, obj):
return self._serialize_asset(self._get_assigned_asset(obj))
def get_assigned_crane(self, obj):
return self._serialize_asset(self._get_assigned_asset(obj))
def validate(self, data):
"""
Provodi poslovnu logiku na razini serializer-a.
"""
is_kupac = data.get('is_kupac')
client_profile = data.get('client_profile')
# Logika: Ako je kupac, mora imati profil
if is_kupac and not client_profile:
raise serializers.ValidationError({
"client_profile": "Korisnik označen kao kupac mora imati dodijeljen klijentski profil."
})
# Logika: Ako ima profil, trebao bi biti kupac (opcionalno, ali preporučeno)
if client_profile and not is_kupac:
raise serializers.ValidationError({
"is_kupac": "Korisnik s dodijeljenim klijentskim profilom mora biti označen kao kupac."
})
return data
class TeamMemberSerializer(serializers.ModelSerializer):
full_name = serializers.SerializerMethodField()
class Meta:
model = CustomUser
fields = ['id', 'email', 'full_name', 'is_team_member']
read_only_fields = fields
def get_full_name(self, obj):
return obj.get_full_name() or obj.email

View File

@@ -0,0 +1,35 @@
# backend/core/users/services.py
import logging
from django.contrib.auth import get_user_model
from rest_framework.exceptions import ValidationError
from .serializers import UserSerializer
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from .models import CustomUser as User
else:
User = get_user_model()
logger = logging.getLogger(__name__)
class UserService:
@staticmethod
def update_user_profile(user, data: dict) -> User:
"""
Ažurira profil koristeći serializer za validaciju.
Ovo osigurava da poslovna pravila definirana u serializeru
budu poštivana i kod programskog ažuriranja.
"""
# Koristimo serializer za validaciju dolaznih podataka
serializer = UserSerializer(user, data=data, partial=True)
if not serializer.is_valid():
logger.error(f"Neuspješna validacija profila korisnika {user.id}: {serializer.errors}")
raise ValidationError(serializer.errors)
# Spremanje validiranih podataka
updated_user = serializer.save()
logger.info(f"Korisnik {user.id} je uspješno ažurirao profil.")
return updated_user

View File

@@ -0,0 +1,25 @@
import pytest
from django.contrib.auth import get_user_model
User = get_user_model()
@pytest.mark.django_db
def test_create_user():
"""Testira kreiranje standardnog korisnika."""
user = User.objects.create_user(
email="test@example.com",
password="password123"
)
assert user.email == "test@example.com"
assert user.check_password("password123")
assert user.is_active is True
@pytest.mark.django_db
def test_create_superuser():
"""Testira kreiranje superuser-a."""
admin = User.objects.create_superuser(
email="admin@example.com",
password="password123"
)
assert admin.is_superuser is True
assert admin.is_staff is True

View File

@@ -0,0 +1,37 @@
# backend/core/users/tests/test_permissions.py
import pytest
from rest_framework import status
from rest_framework.test import APIClient
from django.contrib.auth import get_user_model
User = get_user_model()
@pytest.mark.django_db
class TestUserPermissions:
def setup_method(self):
self.client = APIClient()
# Kreiramo dva korisnika
self.user1 = User.objects.create_user(email="user1@erp.hr", password="password123")
self.user2 = User.objects.create_user(email="user2@erp.hr", password="password123")
def test_user_cannot_edit_other_user(self):
"""Osigurava da user1 ne može mijenjati podatke od user2."""
self.client.force_authenticate(user=self.user1)
# Pokušavamo patchati user2 (pretpostavljamo URL /api/users/{id}/)
response = self.client.patch(f'/api/users/{self.user2.id}/', {"first_name": "Haker"})
# Trebali bismo dobiti 403 Forbidden ili 404 (ovisno kako skrivaš postojanje drugih)
assert response.status_code in [status.HTTP_403_FORBIDDEN, status.HTTP_404_NOT_FOUND]
def test_authenticated_user_can_edit_self(self):
"""Osigurava da user1 može mijenjati svoje podatke."""
self.client.force_authenticate(user=self.user1)
response = self.client.patch(f'/api/users/{self.user1.id}/', {"first_name": "Ime"})
assert response.status_code == status.HTTP_200_OK
self.user1.refresh_from_db()
assert self.user1.first_name == "Ime"

View File

@@ -0,0 +1,56 @@
import uuid
import pytest
from django.contrib.auth import get_user_model
from core.management.commands.seed_all_models import Command
from modules.fleet.models import Vehicle, VehicleServiceRecord
from modules.task_management.models import Task
@pytest.mark.django_db
def test_seed_service_records_creates_record_with_task_and_task_vehicle():
user = get_user_model().objects.create_user(
email=f"seed-test-{uuid.uuid4().hex[:8]}@example.test",
password="pass12345",
)
vehicle = Vehicle.objects.create(
registration_number=f"ST-{uuid.uuid4().hex[:6].upper()}",
asset_type="crane",
)
task = Task.objects.create(
title=f"Task {uuid.uuid4().hex[:6]}",
status="aktivan",
vehicle=vehicle,
assigned_to=user,
)
cmd = Command()
before = VehicleServiceRecord.objects.count()
cmd._seed_service_records(1, [user], [task])
assert VehicleServiceRecord.objects.count() == before + 1
record = VehicleServiceRecord.objects.order_by("-created_at").first()
assert record is not None
assert record.task_id == task.id
assert record.vehicle_id == task.vehicle_id
@pytest.mark.django_db
def test_seed_service_records_skips_tasks_without_vehicle():
user = get_user_model().objects.create_user(
email=f"seed-test-{uuid.uuid4().hex[:8]}@example.test",
password="pass12345",
)
task = Task.objects.create(
title=f"Task {uuid.uuid4().hex[:6]}",
status="aktivan",
vehicle=None,
assigned_to=user,
)
cmd = Command()
before = VehicleServiceRecord.objects.count()
cmd._seed_service_records(1, [user], [task])
assert VehicleServiceRecord.objects.count() == before

View File

@@ -0,0 +1,35 @@
import pytest
from django.core.exceptions import ValidationError as DjangoValidationError
from rest_framework.exceptions import ValidationError as DRFValidationError
from core.users.services import UserService
from django.contrib.auth import get_user_model
User = get_user_model()
@pytest.mark.django_db
class TestUserService:
def setup_method(self):
self.user = User.objects.create_user(
email="servis@primjer.hr",
password="password123"
)
def test_update_profile_success(self):
"""Testira uspješno ažuriranje profila preko servisa."""
new_data = {"first_name": "Novi", "last_name": "Korisnik"}
updated_user = UserService.update_user_profile(self.user, new_data)
assert updated_user.first_name == "Novi"
assert updated_user.last_name == "Korisnik"
# Provjera da email nije promijenjen ako nismo poslali
assert updated_user.email == "servis@primjer.hr"
def test_update_profile_invalid_data(self):
"""Testira validaciju (npr. neispravan format podataka)."""
# Pretpostavimo da dodamo logiku u serializer koja brani prazno ime
invalid_data = {"first_name": ""}
with pytest.raises(DRFValidationError):
UserService.update_user_profile(self.user, invalid_data)

View File

@@ -0,0 +1,61 @@
import pytest
from django.urls import reverse
from rest_framework import status
from rest_framework.test import APIClient
from django.contrib.auth import get_user_model
User = get_user_model()
@pytest.mark.django_db
class TestUserAPI:
def setup_method(self):
self.client = APIClient()
self.user_data = {
"email": "test@primjer.hr",
"first_name": "Test",
"last_name": "Korisnik",
"password": "strongpassword123"
}
self.user = User.objects.create_user(**self.user_data)
def test_get_me_authenticated(self):
"""Testira dohvat vlastitih podataka (uspjeh)."""
self.client.force_authenticate(user=self.user)
url = reverse('user-me') # Provjeri naziv u routeru
response = self.client.get(url)
assert response.status_code == status.HTTP_200_OK
assert response.data['email'] == self.user.email
assert 'is_serviser' in response.data
def test_get_me_unauthenticated(self):
"""Testira pristup bez prijave (neuspjeh)."""
url = reverse('user-me')
response = self.client.get(url)
# Ovisno o tvojim permission_classes, ovdje bi trebalo biti 401 ili 403
assert response.status_code in [status.HTTP_401_UNAUTHORIZED, status.HTTP_403_FORBIDDEN]
def test_user_creation(self):
"""Testira osnovno stvaranje korisnika u bazi."""
assert User.objects.count() == 1
assert self.user.is_serviser is False
def test_update_user_via_api(self):
"""Testira cjelokupan tok: API -> ViewSet -> Service."""
self.client.force_authenticate(user=self.user)
url = reverse('user-detail', kwargs={'pk': self.user.id}) # Provjeri basename
new_data = {"first_name": "Inženjer"}
# Simuliramo PATCH zahtjev
response = self.client.patch(url, new_data)
# Osvježi instancu iz baze
self.user.refresh_from_db()
assert response.status_code == status.HTTP_200_OK
assert self.user.first_name == "Inženjer"

View File

@@ -0,0 +1,13 @@
# backend/core/users/urls.py
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from .views import UserViewSet
router = DefaultRouter()
# Registriramo UserViewSet pod imenom 'users'
# Ovo će generirati rute poput /api/users/me/
router.register(r'', UserViewSet, basename='user')
urlpatterns = [
path('', include(router.urls)),
]

View File

@@ -0,0 +1,44 @@
# backend/core/users/views.py
from rest_framework import viewsets, permissions
from rest_framework.decorators import action
from rest_framework.response import Response
from django.contrib.auth import get_user_model
from .serializers import TeamMemberSerializer, UserSerializer
from .services import UserService
User = get_user_model()
class IsOwner(permissions.BasePermission):
"""Prilagođeni permission: Korisnik može uređivati samo sebe."""
def has_object_permission(self, request, view, obj):
return obj.id == request.user.id
class UserViewSet(viewsets.ModelViewSet):
queryset = User.objects.all()
serializer_class = UserSerializer
permission_classes = [permissions.IsAuthenticated]
@action(detail=False, methods=['get'])
def me(self, request):
serializer = self.get_serializer(request.user)
return Response(serializer.data)
@action(detail=False, methods=['get'], url_path='team-members')
def team_members(self, request):
queryset = User.objects.filter(is_active=True, is_team_member=True).order_by('first_name', 'last_name', 'email')
serializer = TeamMemberSerializer(queryset, many=True)
return Response(serializer.data)
def perform_update(self, serializer):
# Delegiramo logiku servisu
UserService.update_user_profile(
user=self.request.user,
data=serializer.validated_data
)
def get_queryset(self):
# Korisnik vidi samo sebe (ili admini vide sve)
if self.request.user.is_staff:
return User.objects.all()
return User.objects.filter(id=self.request.user.id)