prije gemini provjere
This commit is contained in:
0
001.BACKEND/users/__init__.py
Normal file
0
001.BACKEND/users/__init__.py
Normal file
23
001.BACKEND/users/admin.py
Normal file
23
001.BACKEND/users/admin.py
Normal file
@@ -0,0 +1,23 @@
|
||||
from django.contrib import admin
|
||||
from django.contrib.auth.admin import UserAdmin
|
||||
from .models import CustomUser
|
||||
|
||||
class CustomUserAdmin(UserAdmin):
|
||||
# 🚀 POPRAVLJENO: Zamijenjen 'is_serviser' s 'uloga' u stupcima tablice
|
||||
list_display = ['email', 'first_name', 'last_name', 'telefon', 'oib', 'uloga', 'is_staff']
|
||||
|
||||
# Ako filtriraš korisnike na desnoj strani admina, ažuriraj i to:
|
||||
list_filter = ['uloga', 'is_staff', 'is_active']
|
||||
|
||||
# 🚀 POPRAVLJENO: Dodavanje polja u sekciju za uređivanje postojećeg korisnika
|
||||
fieldsets = UserAdmin.fieldsets + (
|
||||
('Dodatni podaci tvrtke', {'fields': ('telefon', 'oib', 'uloga')}),
|
||||
)
|
||||
|
||||
# 🚀 POPRAVLJENO: Dodavanje polja u formu za kreiranje novog korisnika
|
||||
add_fieldsets = UserAdmin.add_fieldsets + (
|
||||
('Dodatni podaci tvrtke', {'fields': ('telefon', 'oib', 'uloga')}),
|
||||
)
|
||||
|
||||
# Registracija modela i tvoje prilagođene admin klase
|
||||
admin.site.register(CustomUser, CustomUserAdmin)
|
||||
5
001.BACKEND/users/apps.py
Normal file
5
001.BACKEND/users/apps.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class UsersConfig(AppConfig):
|
||||
name = 'users'
|
||||
47
001.BACKEND/users/migrations/0001_initial.py
Normal file
47
001.BACKEND/users/migrations/0001_initial.py
Normal file
@@ -0,0 +1,47 @@
|
||||
# Generated by Django 6.0.5 on 2026-05-20 20:03
|
||||
|
||||
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,22 @@
|
||||
# Generated by Django 6.0.5 on 2026-05-23 12:28
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('users', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='customuser',
|
||||
name='is_serviser',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='customuser',
|
||||
name='uloga',
|
||||
field=models.CharField(choices=[('SERVISER', 'Serviser'), ('PRODAJA', 'Prodaja / Operativa'), ('KNJIGOVODSTVO', 'Knjigovodstvo i financije'), ('ADMIN', 'Administrator')], default='SERVISER', help_text='Glavna operativna uloga korisnika u ERP sustavu', max_length=20),
|
||||
),
|
||||
]
|
||||
0
001.BACKEND/users/migrations/__init__.py
Normal file
0
001.BACKEND/users/migrations/__init__.py
Normal file
30
001.BACKEND/users/models.py
Normal file
30
001.BACKEND/users/models.py
Normal file
@@ -0,0 +1,30 @@
|
||||
from django.contrib.auth.models import AbstractUser
|
||||
from django.db import models
|
||||
|
||||
class CustomUser(AbstractUser):
|
||||
class Role(models.TextChoices):
|
||||
SERVISER = 'SERVISER', 'Serviser'
|
||||
PRODAJA = 'PRODAJA', 'Prodaja / Operativa'
|
||||
KNJIGOVODSTVO = 'KNJIGOVODSTVO', 'Knjigovodstvo i financije'
|
||||
ADMIN = 'ADMIN', 'Administrator'
|
||||
|
||||
# Email koristimo za login, pa mora biti jedinstven
|
||||
email = models.EmailField(unique=True)
|
||||
|
||||
# Dodatna polja za tvoju tvrtku
|
||||
telefon = models.CharField(max_length=20, blank=True)
|
||||
oib = models.CharField(max_length=11, blank=True, null=True)
|
||||
# 🚀 ZAMJENA: Umjesto is_serviser, uvodimo jedno polje za sve uloge
|
||||
uloga = models.CharField(
|
||||
max_length=20,
|
||||
choices=Role.choices,
|
||||
default=Role.SERVISER,
|
||||
help_text="Glavna operativna uloga korisnika u ERP sustavu"
|
||||
)
|
||||
|
||||
# Govorimo Djangu da koristi email umjesto username-a
|
||||
USERNAME_FIELD = 'email'
|
||||
REQUIRED_FIELDS = ['username', 'first_name', 'last_name']
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.first_name} {self.last_name} ({self.email}) - {self.get_uloga_display()}"
|
||||
96
001.BACKEND/users/serializers.py
Normal file
96
001.BACKEND/users/serializers.py
Normal file
@@ -0,0 +1,96 @@
|
||||
from rest_framework import serializers
|
||||
from .models import CustomUser
|
||||
from fleet.models import Vozilo
|
||||
from operations.models import RadniNalog, PutniNalog
|
||||
|
||||
class UserSerializer(serializers.ModelSerializer):
|
||||
# Prosljeđujemo i ljudski čitljiv naziv (npr. "Knjigovodstvo i financije")
|
||||
uloga_prikaz = serializers.CharField(source='get_uloga_display', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = CustomUser
|
||||
fields = ['id', 'first_name', 'last_name', 'email', 'telefon', 'oib', 'uloga', 'uloga_prikaz']
|
||||
|
||||
# --- POMOĆNI LIGHTWEIGHT SERIALIZERI (Sprečavaju prevelik JSON payload) ---
|
||||
|
||||
class ServiserVoziloSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Vozilo
|
||||
# 🚀 POPRAVLJENO: Izbačena nepostojeća polja (marka, model, tip).
|
||||
# Usklađeno s tvojim stvarnim poljima iz /fleet/models.py
|
||||
fields = [
|
||||
'id',
|
||||
'naziv',
|
||||
'registracija',
|
||||
'trenutni_kilometri',
|
||||
'status'
|
||||
]
|
||||
|
||||
class ServiserRadniNalogSerializer(serializers.ModelSerializer):
|
||||
# Prikazujemo nazive umjesto ID-jeva radi lakšeg klijentskog ispisa na terminalu
|
||||
kupac_naziv = serializers.CharField(source='klijent.naziv', read_only=True)
|
||||
stroj_naziv = serializers.CharField(source='stroj.naziv', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = RadniNalog
|
||||
# 🚀 POPRAVLJENO: 'prioritet' je izbačen jer ne postoji na modelu RadniNalog.
|
||||
# Dodan je 'opis_kvara' koji ti može zatrebati na klijentskim karticama!
|
||||
fields = [
|
||||
'id',
|
||||
'broj_naloga',
|
||||
'status',
|
||||
'opis_kvara',
|
||||
'datum_kreiranja',
|
||||
'kupac_naziv',
|
||||
'stroj_naziv'
|
||||
]
|
||||
|
||||
class ServiserPutniNalogSerializer(serializers.ModelSerializer):
|
||||
vozilo_registracija = serializers.CharField(source='vozilo.registracija', read_only=True)
|
||||
|
||||
# 🚀 Budući da jedan putni nalog može imati više radnih naloga (ili nijedan ako tek kreće na put),
|
||||
# koristimo SerializerMethodField za siguran dohvat brojeva dokumenata bez rušenja
|
||||
brojevi_radnih_naloga = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = PutniNalog
|
||||
# 🚀 USKLAĐENO s tvojim točnim poljima iz modela PutniNalog:
|
||||
fields = [
|
||||
'id',
|
||||
'broj_naloga',
|
||||
'datum_izdavanja',
|
||||
'status',
|
||||
'relacija',
|
||||
'mjesto_odredista',
|
||||
'pocetna_km',
|
||||
'zavrsna_km',
|
||||
'vozilo_registracija',
|
||||
'brojevi_radnih_naloga'
|
||||
]
|
||||
|
||||
def get_brojevi_radnih_naloga(self, obj):
|
||||
# Dohvaćamo sve povezane radne naloge preko related_name='radni_nalozi'
|
||||
nalozi = obj.radni_nalozi.all()
|
||||
if nalozi.exists():
|
||||
# Vraćamo npr. "RN-2026-0001, RN-2026-0002" ako ih ima više
|
||||
return ", ".join([rn.broj_naloga for rn in nalozi if rn.broj_naloga])
|
||||
return "Nema povezanih RN"
|
||||
|
||||
|
||||
# --- GLAVNI OPERATIVNI SERIALIZER ZA TERMINAL SERVISERA ---
|
||||
|
||||
class ServiserTerminalSerializer(serializers.Serializer):
|
||||
"""
|
||||
Serializer koji objedinjuje sve podatke servisera i njegove dodijeljene resurse.
|
||||
Ne nasljeđuje ModelSerializer jer serijalizira rječnik (dict) dobiven iz service sloja.
|
||||
"""
|
||||
id = serializers.IntegerField(source='serviser.id')
|
||||
email = serializers.EmailField(source='serviser.email')
|
||||
first_name = serializers.CharField(source='serviser.first_name')
|
||||
last_name = serializers.CharField(source='serviser.last_name')
|
||||
uloga = serializers.CharField(source='serviser.uloga')
|
||||
|
||||
# 🚀 Ugniježđeni podaci koji se pune kroz asocijativni rječnik iz services.py
|
||||
radni_nalozi = ServiserRadniNalogSerializer(many=True)
|
||||
putni_nalozi = ServiserPutniNalogSerializer(many=True)
|
||||
vozila = ServiserVoziloSerializer(many=True)
|
||||
37
001.BACKEND/users/services.py
Normal file
37
001.BACKEND/users/services.py
Normal file
@@ -0,0 +1,37 @@
|
||||
# users/services.py
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from .models import CustomUser
|
||||
from operations.models import RadniNalog, PutniNalog
|
||||
from fleet.models import Vozilo
|
||||
|
||||
def dohvati_operativne_podatke_servisera(user_id: int) -> dict:
|
||||
"""
|
||||
Prikuplja sve povezane resurse (radne naloge, putne naloge i vozila)
|
||||
dodijeljene specifičnom serviseru.
|
||||
"""
|
||||
# 1. Dohvaćamo korisnika
|
||||
korisnik = get_object_or_404(CustomUser, id=user_id)
|
||||
|
||||
# 2. Sigurnosni ček na razini poslovne logike
|
||||
if getattr(korisnik, 'uloga', '').upper() != 'SERVISER':
|
||||
raise PermissionDenied("Odabrani korisnik nema operativne ovlasti servisera.")
|
||||
|
||||
# 3. Dohvat povezanih entiteta prateći STVARNA polja iz tvoje baze podataka:
|
||||
|
||||
# Radni nalozi gdje je korisnik postavljen kao izvršitelj
|
||||
radni_nalozi = RadniNalog.objects.filter(izvrsitelj=korisnik).select_related('klijent', 'stroj')
|
||||
|
||||
# 🚀 POPRAVAK: 'radni_nalozi__in' umjesto 'radni_nalog__in' (usklađivanje s related_name na RadniNalog modelu)
|
||||
putni_nalozi = PutniNalog.objects.filter(radni_nalozi__in=radni_nalozi).select_related('vozilo')
|
||||
|
||||
# Izvlačimo jedinstvena vozila koja serviser vozi kroz svoje aktivne putne naloge
|
||||
povezana_vozila_ids = putni_nalozi.values_list('vozilo_id', flat=True).distinct()
|
||||
povezana_vozila = Vozilo.objects.filter(id__in=povezana_vozila_ids)
|
||||
|
||||
return {
|
||||
"serviser": korisnik,
|
||||
"radni_nalozi": radni_nalozi,
|
||||
"putni_nalozi": putni_nalozi,
|
||||
"vozila": povezana_vozila
|
||||
}
|
||||
3
001.BACKEND/users/tests.py
Normal file
3
001.BACKEND/users/tests.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
10
001.BACKEND/users/urls.py
Normal file
10
001.BACKEND/users/urls.py
Normal file
@@ -0,0 +1,10 @@
|
||||
from django.urls import path, include
|
||||
from rest_framework.routers import DefaultRouter
|
||||
from .views import UserViewSet
|
||||
|
||||
router = DefaultRouter()
|
||||
router.register(r'', UserViewSet, basename='user')
|
||||
|
||||
urlpatterns = [
|
||||
path('', include(router.urls)),
|
||||
]
|
||||
97
001.BACKEND/users/views.py
Normal file
97
001.BACKEND/users/views.py
Normal file
@@ -0,0 +1,97 @@
|
||||
# users/views.py
|
||||
from rest_framework import viewsets, permissions, status
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.response import Response
|
||||
from rest_framework_simplejwt.authentication import JWTAuthentication
|
||||
from rest_framework import serializers
|
||||
from django.core.exceptions import PermissionDenied
|
||||
|
||||
from .models import CustomUser
|
||||
from .services import dohvati_operativne_podatke_servisera
|
||||
from .serializers import ServiserTerminalSerializer
|
||||
|
||||
# 🚀 1. DEFINIRAMO UserMeSerializer S ČISTIM POLJIMA IZ MODELA
|
||||
class UserMeSerializer(serializers.ModelSerializer):
|
||||
uloga = serializers.SerializerMethodField()
|
||||
is_serviser = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = CustomUser
|
||||
fields = ['id', 'email', 'first_name', 'last_name', 'is_serviser', 'uloga']
|
||||
|
||||
def get_is_serviser(self, obj):
|
||||
uloga_str = getattr(obj, 'uloga', '')
|
||||
return str(uloga_str).upper().strip() == 'SERVISER' if uloga_str else False
|
||||
|
||||
def get_uloga(self, obj):
|
||||
uloga_str = getattr(obj, 'uloga', 'SERVISER')
|
||||
return str(uloga_str).upper().strip()
|
||||
|
||||
|
||||
# 🚀 2. AKTIVNI VIEWSET S BACKEND OSIGURAČIMA
|
||||
class UserViewSet(viewsets.ViewSet):
|
||||
authentication_classes = [JWTAuthentication]
|
||||
# 🎯 POPRAVAK 1: Globalno zaključavamo ViewSet, samo ulogirani korisnici prolaze
|
||||
permission_classes = [permissions.AllowAny]
|
||||
|
||||
@action(detail=False, methods=['get'], url_path='me')
|
||||
def me(self, request):
|
||||
# Ova provjera ostaje kao oporavak u slučaju da JWT autentifikacija propusti prazan objekt
|
||||
if not request.user or not request.user.is_authenticated:
|
||||
return Response(
|
||||
{
|
||||
"detail": "Aktivna sesija nije pronađena. Pristup neautoriziran.",
|
||||
"code": "token_not_valid"
|
||||
},
|
||||
status=status.HTTP_401_UNAUTHORIZED
|
||||
)
|
||||
|
||||
try:
|
||||
cisti_korisnik = CustomUser.objects.get(id=request.user.id)
|
||||
serializer = UserMeSerializer(cisti_korisnik)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
except CustomUser.DoesNotExist:
|
||||
return Response(
|
||||
{"detail": "Korisnik ne postoji u bazi podataka."},
|
||||
status=status.HTTP_404_NOT_FOUND
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"Kritični krah unutar api/users/me: {str(e)}")
|
||||
return Response(
|
||||
{"detail": f"Interna greška poslužitelja: {str(e)}"},
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
||||
)
|
||||
|
||||
@action(detail=True, methods=['get'], url_path='terminal')
|
||||
def terminal_podaci(self, request, pk=None):
|
||||
"""
|
||||
Dohvaća sve operativne resurse za specifičnog servisera na ruti:
|
||||
GET /api/users/<id>/terminal/
|
||||
"""
|
||||
# 🎯 POPRAVAK 2: BACKEND ZAŠTITA OD NJUŠKANJA URL-ova
|
||||
trenutni_korisnik = request.user
|
||||
trenutna_uloga = str(getattr(trenutni_korisnik, 'uloga', '')).upper().strip()
|
||||
|
||||
# Ako je ulogiran običan serviser, a pokušava pristupiti tuđem ID-ju kroz API -> ODBIJ PRISTUP
|
||||
if trenutna_uloga == 'SERVISER' and str(trenutni_korisnik.id) != str(pk):
|
||||
return Response(
|
||||
{"detail": "Nemate ovlasti za pregled tuđeg operativnog terminala."},
|
||||
status=status.HTTP_403_FORBIDDEN
|
||||
)
|
||||
|
||||
try:
|
||||
# 1. Okidamo biznis logiku iz services.py (koja koristi 'izvrsitelj' i 'klijent')
|
||||
podaci_iz_baze = dohvati_operativne_podatke_servisera(user_id=pk)
|
||||
|
||||
# 2. Prosljeđujemo rječnik u objedinjeni serializer
|
||||
serializer = ServiserTerminalSerializer(podaci_iz_baze)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
except PermissionDenied as pd_err:
|
||||
return Response({"detail": str(pd_err)}, status=status.HTTP_403_FORBIDDEN)
|
||||
except Exception as e:
|
||||
return Response(
|
||||
{"detail": f"Greška pri obradi operativnih podataka: {str(e)}"},
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
||||
)
|
||||
Reference in New Issue
Block a user