This commit is contained in:
0
backend/core/users/__init__.py
Normal file
0
backend/core/users/__init__.py
Normal file
27
backend/core/users/admin.py
Normal file
27
backend/core/users/admin.py
Normal 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',)
|
||||
7
backend/core/users/apps.py
Normal file
7
backend/core/users/apps.py
Normal 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'
|
||||
0
backend/core/users/management/__init__.py
Normal file
0
backend/core/users/management/__init__.py
Normal file
0
backend/core/users/management/commands/__init__.py
Normal file
0
backend/core/users/management/commands/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
from core.management.commands.seed_all_models import Command # noqa: F401
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
from core.management.commands.seed_fleet_assets import Command # noqa: F401
|
||||
|
||||
107
backend/core/users/management/commands/seed_knezljubo.py
Normal file
107
backend/core/users/management/commands/seed_knezljubo.py
Normal 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
|
||||
52
backend/core/users/management/commands/seed_users.py
Normal file
52
backend/core/users/management/commands/seed_users.py
Normal 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!'))
|
||||
30
backend/core/users/managers.py
Normal file
30
backend/core/users/managers.py
Normal 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)
|
||||
47
backend/core/users/migrations/0001_initial.py
Normal file
47
backend/core/users/migrations/0001_initial.py
Normal 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()),
|
||||
],
|
||||
),
|
||||
]
|
||||
@@ -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'),
|
||||
),
|
||||
]
|
||||
@@ -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',
|
||||
),
|
||||
]
|
||||
@@ -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'),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -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'),
|
||||
),
|
||||
]
|
||||
@@ -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'),
|
||||
),
|
||||
]
|
||||
0
backend/core/users/migrations/__init__.py
Normal file
0
backend/core/users/migrations/__init__.py
Normal file
52
backend/core/users/models.py
Normal file
52
backend/core/users/models.py
Normal 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)
|
||||
73
backend/core/users/serializers.py
Normal file
73
backend/core/users/serializers.py
Normal 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
|
||||
35
backend/core/users/services.py
Normal file
35
backend/core/users/services.py
Normal 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
|
||||
25
backend/core/users/tests/test_models.py
Normal file
25
backend/core/users/tests/test_models.py
Normal 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
|
||||
37
backend/core/users/tests/test_permissions.py
Normal file
37
backend/core/users/tests/test_permissions.py
Normal 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"
|
||||
56
backend/core/users/tests/test_seed_all_models_command.py
Normal file
56
backend/core/users/tests/test_seed_all_models_command.py
Normal 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
|
||||
35
backend/core/users/tests/test_services.py
Normal file
35
backend/core/users/tests/test_services.py
Normal 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)
|
||||
61
backend/core/users/tests/test_users.py
Normal file
61
backend/core/users/tests/test_users.py
Normal 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"
|
||||
13
backend/core/users/urls.py
Normal file
13
backend/core/users/urls.py
Normal 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)),
|
||||
]
|
||||
44
backend/core/users/views.py
Normal file
44
backend/core/users/views.py
Normal 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)
|
||||
Reference in New Issue
Block a user