This commit is contained in:
0
backend/modules/task_management/__init__.py
Normal file
0
backend/modules/task_management/__init__.py
Normal file
21
backend/modules/task_management/admin.py
Normal file
21
backend/modules/task_management/admin.py
Normal file
@@ -0,0 +1,21 @@
|
||||
# backend/modules/task_management/admin.py
|
||||
|
||||
from django.contrib import admin
|
||||
from .models import Task
|
||||
|
||||
@admin.register(Task)
|
||||
class TaskAdmin(admin.ModelAdmin):
|
||||
"""
|
||||
Konfiguracija prikaza modela u Admin panelu.
|
||||
"""
|
||||
# Koja polja želimo vidjeti u tabličnom prikazu
|
||||
list_display = ('title', 'status', 'assigned_to', 'work_order', 'created_at')
|
||||
|
||||
# Omogućuje filtriranje s desne strane
|
||||
list_filter = ('status', 'created_at', 'work_order')
|
||||
|
||||
# Omogućuje pretraživanje po tekstu
|
||||
search_fields = ('title', 'description', 'work_order__id', 'work_order__vehicle__registration_number')
|
||||
|
||||
# Polja koja su samo za čitanje (npr. UUID koji se ne smije mijenjati)
|
||||
readonly_fields = ('id', 'created_at', 'updated_at')
|
||||
5
backend/modules/task_management/apps.py
Normal file
5
backend/modules/task_management/apps.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class TaskManagementAppConfig(AppConfig):
|
||||
name = 'modules.task_management'
|
||||
34
backend/modules/task_management/migrations/0001_initial.py
Normal file
34
backend/modules/task_management/migrations/0001_initial.py
Normal file
@@ -0,0 +1,34 @@
|
||||
# Generated by Django 5.2.15 on 2026-06-29 13:34
|
||||
|
||||
import django.db.models.deletion
|
||||
import uuid
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Task',
|
||||
fields=[
|
||||
('id', models.UUIDField(default=uuid.uuid4, editable=False, help_text='Unikatni identifikator entiteta (UUID).', primary_key=True, serialize=False)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Vrijeme kreiranja')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Vrijeme zadnje izmjene')),
|
||||
('is_active', models.BooleanField(default=True, verbose_name='Aktivan zapis')),
|
||||
('title', models.CharField(max_length=255)),
|
||||
('description', models.TextField(blank=True)),
|
||||
('status', models.CharField(choices=[('aktivan', 'Aktivan'), ('servis', 'U tijeku'), ('neaktivan', 'Otkazano')], default='aktivan', max_length=20)),
|
||||
('assigned_to', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='tasks', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'ordering': ['-created_at'],
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,27 @@
|
||||
# Generated by Django 5.2.15 on 2026-07-07
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('fleet', '0012_workorder_purpose_choices'),
|
||||
('task_management', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='task',
|
||||
name='work_order',
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name='tasks',
|
||||
to='fleet.workorder',
|
||||
),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('fleet', '0013_vehicleservicerecord_task'),
|
||||
('task_management', '0002_task_work_order'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='task',
|
||||
name='vehicle',
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name='tasks',
|
||||
to='fleet.vehicle',
|
||||
verbose_name='Dizalica',
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='task',
|
||||
name='work_order',
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name='work_order_tasks',
|
||||
to='fleet.workorder',
|
||||
verbose_name='Putni nalog',
|
||||
),
|
||||
),
|
||||
]
|
||||
59
backend/modules/task_management/models.py
Normal file
59
backend/modules/task_management/models.py
Normal file
@@ -0,0 +1,59 @@
|
||||
# backend/modules/task_management/models.py
|
||||
|
||||
from django.db import models
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from core.base_models import BaseModel # Uvozimo naš Base
|
||||
|
||||
class Task(BaseModel):
|
||||
"""
|
||||
Modularni Task model nasljeđuje sva polja iz BaseModel.
|
||||
"""
|
||||
STATUS_CHOICES = [
|
||||
('aktivan', 'Aktivan'),
|
||||
('servis', 'U tijeku'),
|
||||
('neaktivan', 'Otkazano'),
|
||||
]
|
||||
title = models.CharField(max_length=255)
|
||||
description = models.TextField(blank=True)
|
||||
status = models.CharField(
|
||||
max_length=20,
|
||||
choices=STATUS_CHOICES,
|
||||
default='aktivan'
|
||||
)
|
||||
assigned_to = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
related_name='tasks'
|
||||
)
|
||||
vehicle = models.ForeignKey(
|
||||
'fleet.Vehicle',
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name='tasks',
|
||||
verbose_name=_("Dizalica"),
|
||||
)
|
||||
work_order = models.ForeignKey(
|
||||
'fleet.WorkOrder',
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name='work_order_tasks',
|
||||
verbose_name=_("Putni nalog"),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
ordering = ['-created_at']
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.title} ({self.status})"
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
if self.status == 'neaktivan' and not self.work_order_id:
|
||||
raise ValidationError({
|
||||
'work_order': "Putni nalog je obavezan prije zatvaranja zadatka."
|
||||
})
|
||||
75
backend/modules/task_management/serializers.py
Normal file
75
backend/modules/task_management/serializers.py
Normal file
@@ -0,0 +1,75 @@
|
||||
# backend/modules/task_management/serializers.py
|
||||
|
||||
from rest_framework import serializers
|
||||
from .models import Task
|
||||
|
||||
class TaskSerializer(serializers.ModelSerializer):
|
||||
assigned_to_name = serializers.SerializerMethodField()
|
||||
work_order_label = serializers.SerializerMethodField()
|
||||
vehicle_registration = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = Task
|
||||
fields = [
|
||||
'id', 'title', 'description', 'status',
|
||||
'assigned_to', 'assigned_to_name',
|
||||
'vehicle', 'vehicle_registration',
|
||||
'work_order', 'work_order_label',
|
||||
'created_at',
|
||||
]
|
||||
read_only_fields = ['id', 'created_at']
|
||||
|
||||
def validate_title(self, value):
|
||||
"""Primjer custom validacije: naziv ne smije biti prekratak."""
|
||||
if len(value) < 3:
|
||||
raise serializers.ValidationError("Naslov zadatka mora imati barem 3 znaka.")
|
||||
return value
|
||||
|
||||
def validate(self, attrs):
|
||||
current = getattr(self, 'instance', None)
|
||||
status_value = attrs.get('status', getattr(current, 'status', 'aktivan'))
|
||||
work_order = attrs.get('work_order', getattr(current, 'work_order', None))
|
||||
vehicle = attrs.get('vehicle', getattr(current, 'vehicle', None))
|
||||
|
||||
if current is None and not vehicle:
|
||||
raise serializers.ValidationError({
|
||||
'vehicle': "Dizalica je obavezna pri kreiranju taska."
|
||||
})
|
||||
|
||||
if work_order and not vehicle:
|
||||
vehicle = work_order.vehicle
|
||||
attrs['vehicle'] = vehicle
|
||||
if work_order and vehicle and work_order.vehicle_id != vehicle.id:
|
||||
raise serializers.ValidationError({
|
||||
'work_order': "Putni nalog mora pripadati istoj dizalici kao task."
|
||||
})
|
||||
|
||||
if status_value == 'neaktivan' and not work_order:
|
||||
raise serializers.ValidationError({
|
||||
'work_order': "Putni nalog je obavezan prije zatvaranja zadatka."
|
||||
})
|
||||
return attrs
|
||||
|
||||
def get_work_order_label(self, obj):
|
||||
if not obj.work_order_id:
|
||||
return None
|
||||
return f"PN-{str(obj.work_order_id).split('-')[0].upper()}"
|
||||
|
||||
def get_assigned_to_name(self, obj):
|
||||
if not obj.assigned_to_id:
|
||||
return None
|
||||
full_name = obj.assigned_to.get_full_name()
|
||||
if full_name:
|
||||
return full_name
|
||||
email = getattr(obj.assigned_to, 'email', None)
|
||||
if email:
|
||||
return email
|
||||
username = getattr(obj.assigned_to, 'username', None)
|
||||
if username:
|
||||
return username
|
||||
return str(obj.assigned_to_id)
|
||||
|
||||
def get_vehicle_registration(self, obj):
|
||||
if not obj.vehicle_id:
|
||||
return None
|
||||
return obj.vehicle.registration_number
|
||||
48
backend/modules/task_management/services.py
Normal file
48
backend/modules/task_management/services.py
Normal file
@@ -0,0 +1,48 @@
|
||||
# backend/modules/task_management/services.py
|
||||
|
||||
from .models import Task
|
||||
from rest_framework.exceptions import ValidationError
|
||||
from django.core.exceptions import ValidationError as DjangoValidationError
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class TaskService:
|
||||
@staticmethod
|
||||
def create_task_entry(data: dict, user=None) -> Task:
|
||||
"""
|
||||
Univerzalna metoda: prihvaća podatke i opcionalnog korisnika.
|
||||
"""
|
||||
# Poslovna pravila
|
||||
if data.get('status') == 'DONE' and (user and not user.is_staff):
|
||||
raise ValidationError({"status": "Samo admini mogu odmah označiti zadatak kao gotov."})
|
||||
|
||||
payload = dict(data)
|
||||
if user and 'assigned_to' not in payload:
|
||||
payload['assigned_to'] = user
|
||||
|
||||
entry = Task(**payload)
|
||||
try:
|
||||
entry.full_clean()
|
||||
except DjangoValidationError as exc:
|
||||
logger.error(f"Neuspješna validacija zadatka: {exc}")
|
||||
raise ValidationError(exc.message_dict if hasattr(exc, 'message_dict') else exc.messages)
|
||||
entry.save()
|
||||
logger.info(f"Zadatak uspješno kreiran: {entry.id}")
|
||||
return entry
|
||||
|
||||
@staticmethod
|
||||
def update_task_entry(instance, data, user):
|
||||
# Validacija za update (npr. samo vlasnik ili admin smije ažurirati)
|
||||
if not user.is_staff and instance.assigned_to != user:
|
||||
raise ValidationError("Nemate dozvolu za izmjenu ovog zadatka.")
|
||||
|
||||
# Spremanje
|
||||
for attr, value in data.items():
|
||||
setattr(instance, attr, value)
|
||||
try:
|
||||
instance.full_clean()
|
||||
except DjangoValidationError as exc:
|
||||
raise ValidationError(exc.message_dict if hasattr(exc, 'message_dict') else exc.messages)
|
||||
instance.save()
|
||||
return instance
|
||||
37
backend/modules/task_management/tasks.py
Normal file
37
backend/modules/task_management/tasks.py
Normal file
@@ -0,0 +1,37 @@
|
||||
# backend/modules/task_management/tasks.py
|
||||
|
||||
from celery import shared_task
|
||||
from .models import Task
|
||||
from django.contrib.auth import get_user_model
|
||||
from modules.fleet.models import Vehicle, WorkOrder
|
||||
|
||||
@shared_task
|
||||
def create_task_from_invoice(invoice_data):
|
||||
"""
|
||||
Ovaj zadatak se poziva asinkrono.
|
||||
"""
|
||||
User = get_user_model()
|
||||
# Logika: dodijeli zadatak adminu ako je račun veći od 1000
|
||||
amount = float(invoice_data.get('amount', 0))
|
||||
admin_user = User.objects.filter(is_staff=True).first()
|
||||
work_order = None
|
||||
vehicle = None
|
||||
work_order_id = invoice_data.get('work_order_id')
|
||||
vehicle_id = invoice_data.get('vehicle_id')
|
||||
|
||||
if work_order_id:
|
||||
work_order = WorkOrder.objects.filter(pk=work_order_id).first()
|
||||
if work_order:
|
||||
vehicle = work_order.vehicle
|
||||
if vehicle is None and vehicle_id:
|
||||
vehicle = Vehicle.objects.filter(pk=vehicle_id).first()
|
||||
|
||||
if amount > 1000:
|
||||
Task.objects.create(
|
||||
title=f"Provjera računa: {invoice_data['invoice_number']}",
|
||||
description=f"Račun iznosi {amount}, potrebna revizija.",
|
||||
assigned_to=admin_user,
|
||||
status='aktivan',
|
||||
vehicle=vehicle,
|
||||
work_order=work_order,
|
||||
)
|
||||
12
backend/modules/task_management/tests/test_models.py
Normal file
12
backend/modules/task_management/tests/test_models.py
Normal file
@@ -0,0 +1,12 @@
|
||||
from django.test import TestCase
|
||||
from django.contrib.auth import get_user_model
|
||||
|
||||
from modules.task_management.models import Task
|
||||
|
||||
|
||||
class TaskModelTests(TestCase):
|
||||
def test_str_returns_title_and_status(self):
|
||||
user = get_user_model().objects.create_user(username="u1", email="u1@example.test", password="pass")
|
||||
task = Task.objects.create(title="My Task", description="Desc", assigned_to=user)
|
||||
assert str(task).startswith("My Task")
|
||||
assert "aktivan" in str(task) or "(" in str(task) # basic sanity
|
||||
32
backend/modules/task_management/tests/test_serializers.py
Normal file
32
backend/modules/task_management/tests/test_serializers.py
Normal file
@@ -0,0 +1,32 @@
|
||||
from django.test import TestCase
|
||||
from modules.task_management.serializers import TaskSerializer
|
||||
from rest_framework.exceptions import ValidationError
|
||||
from django.contrib.auth import get_user_model
|
||||
from modules.task_management.models import Task
|
||||
from modules.fleet.models import Vehicle, WorkOrder
|
||||
|
||||
|
||||
class TaskSerializerTests(TestCase):
|
||||
def test_validate_title_too_short_raises(self):
|
||||
serializer = TaskSerializer(data={"title": "ab", "description": "", "status": "aktivan"})
|
||||
with self.assertRaises(ValidationError):
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
def test_validate_title_ok(self):
|
||||
serializer = TaskSerializer(data={"title": "Valid title", "description": "", "status": "aktivan"})
|
||||
assert serializer.is_valid(), serializer.errors
|
||||
|
||||
def test_cannot_close_without_work_order(self):
|
||||
serializer = TaskSerializer(data={"title": "Task 1", "description": "", "status": "neaktivan"})
|
||||
with self.assertRaises(ValidationError):
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
def test_can_close_with_work_order(self):
|
||||
User = get_user_model()
|
||||
user = User.objects.create_user(username="task-user", email="task-user@example.test", password="pass")
|
||||
vehicle = Vehicle.objects.create(registration_number="ZG-TSK-02", asset_type='crane')
|
||||
work_order = WorkOrder.objects.create(vehicle=vehicle, creator=user, purpose='defektaza')
|
||||
task = Task.objects.create(title="Task 2", description="", status="servis", assigned_to=user)
|
||||
|
||||
serializer = TaskSerializer(instance=task, data={"status": "neaktivan", "work_order": str(work_order.id)}, partial=True)
|
||||
assert serializer.is_valid(), serializer.errors
|
||||
53
backend/modules/task_management/tests/test_services.py
Normal file
53
backend/modules/task_management/tests/test_services.py
Normal file
@@ -0,0 +1,53 @@
|
||||
from django.test import TestCase
|
||||
from django.contrib.auth import get_user_model
|
||||
from rest_framework.exceptions import ValidationError
|
||||
from modules.task_management.services import TaskService
|
||||
from modules.task_management.models import Task
|
||||
from modules.fleet.models import Vehicle, WorkOrder
|
||||
|
||||
|
||||
class TaskServiceTests(TestCase):
|
||||
def setUp(self):
|
||||
User = get_user_model()
|
||||
self.user = User.objects.create_user(username="user1", email="u1@example.test", password="pass")
|
||||
self.other_user = User.objects.create_user(username="user2", email="u2@example.test", password="pass")
|
||||
self.staff = User.objects.create_user(username="admin", email="admin@example.test", password="pass", is_staff=True)
|
||||
|
||||
def test_create_task_entry_creates_task_and_assigns_user(self):
|
||||
data = {"title": "Do work", "description": "Something", "status": "aktivan"}
|
||||
entry = TaskService.create_task_entry(data=data, user=self.user)
|
||||
assert isinstance(entry, Task)
|
||||
assert entry.assigned_to == self.user
|
||||
assert entry.title == "Do work"
|
||||
|
||||
def test_create_task_entry_short_title_raises(self):
|
||||
data = {"title": "x", "description": "", "status": "aktivan"}
|
||||
with self.assertRaises(ValidationError):
|
||||
TaskService.create_task_entry(data=data, user=self.user)
|
||||
|
||||
def test_update_task_entry_permission_denied_for_non_owner(self):
|
||||
task = Task.objects.create(title="T", description="", assigned_to=self.other_user)
|
||||
with self.assertRaises(ValidationError):
|
||||
TaskService.update_task_entry(instance=task, data={"title": "New"}, user=self.user)
|
||||
|
||||
def test_update_task_entry_allowed_for_owner(self):
|
||||
task = Task.objects.create(title="Old", description="", assigned_to=self.user)
|
||||
updated = TaskService.update_task_entry(instance=task, data={"title": "New"}, user=self.user)
|
||||
assert updated.title == "New"
|
||||
|
||||
def test_update_task_entry_rejects_closing_without_work_order(self):
|
||||
task = Task.objects.create(title="Za zatvaranje", description="", assigned_to=self.user, status='servis')
|
||||
with self.assertRaises(ValidationError):
|
||||
TaskService.update_task_entry(instance=task, data={"status": "neaktivan"}, user=self.user)
|
||||
|
||||
def test_update_task_entry_allows_closing_with_work_order(self):
|
||||
vehicle = Vehicle.objects.create(registration_number="ZG-TASK-01", asset_type='crane')
|
||||
work_order = WorkOrder.objects.create(vehicle=vehicle, creator=self.user, purpose='defektaza')
|
||||
task = Task.objects.create(title="Za zatvaranje", description="", assigned_to=self.user, status='servis')
|
||||
updated = TaskService.update_task_entry(
|
||||
instance=task,
|
||||
data={"status": "neaktivan", "work_order": work_order},
|
||||
user=self.user,
|
||||
)
|
||||
assert updated.status == "neaktivan"
|
||||
assert updated.work_order == work_order
|
||||
25
backend/modules/task_management/tests/test_tasks.py
Normal file
25
backend/modules/task_management/tests/test_tasks.py
Normal file
@@ -0,0 +1,25 @@
|
||||
from django.test import TestCase
|
||||
from django.contrib.auth import get_user_model
|
||||
from modules.task_management.tasks import create_task_from_invoice
|
||||
from modules.task_management.models import Task
|
||||
|
||||
|
||||
class TaskCeleryTests(TestCase):
|
||||
def setUp(self):
|
||||
User = get_user_model()
|
||||
# Create an admin user to be assigned the task
|
||||
self.admin = User.objects.create_user(username="admin", email="admin@example.test", password="pass", is_staff=True)
|
||||
|
||||
def test_create_task_from_invoice_creates_task_for_large_amount(self):
|
||||
invoice_data = {"amount": 1500, "invoice_number": "INV-100"}
|
||||
# run the task function synchronously via .run (no bind)
|
||||
create_task_from_invoice.run(invoice_data)
|
||||
task = Task.objects.filter(title__icontains="INV-100").first()
|
||||
assert task is not None
|
||||
assert task.assigned_to == self.admin
|
||||
|
||||
def test_create_task_from_invoice_does_not_create_for_small_amount(self):
|
||||
invoice_data = {"amount": 100, "invoice_number": "INV-101"}
|
||||
create_task_from_invoice.run(invoice_data)
|
||||
task = Task.objects.filter(title__icontains="INV-101").first()
|
||||
assert task is None
|
||||
54
backend/modules/task_management/tests/test_views.py
Normal file
54
backend/modules/task_management/tests/test_views.py
Normal file
@@ -0,0 +1,54 @@
|
||||
from django.test import TestCase, RequestFactory
|
||||
from django.contrib.auth import get_user_model
|
||||
from unittest.mock import patch
|
||||
|
||||
from modules.task_management.views import TaskViewSet
|
||||
from modules.task_management.serializers import TaskSerializer
|
||||
from modules.task_management.models import Task
|
||||
|
||||
|
||||
class TaskViewSetTests(TestCase):
|
||||
def setUp(self):
|
||||
User = get_user_model()
|
||||
self.user = User.objects.create_user(username="u1", email="u1@example.test", password="pass")
|
||||
self.factory = RequestFactory()
|
||||
self.view = TaskViewSet()
|
||||
self.view.request = self.factory.post("/")
|
||||
self.view.request.user = self.user
|
||||
|
||||
@patch("modules.task_management.views.TaskService.create_task_entry")
|
||||
def test_perform_create_calls_service(self, mock_create):
|
||||
data = {"title": "Task from view", "description": "", "status": "aktivan"}
|
||||
serializer = TaskSerializer(data=data, context={"request": self.view.request})
|
||||
assert serializer.is_valid(), serializer.errors
|
||||
|
||||
# Call perform_create, which should delegate to TaskService.create_task_entry
|
||||
self.view.perform_create(serializer)
|
||||
mock_create.assert_called_once()
|
||||
args, kwargs = mock_create.call_args
|
||||
assert "data" in kwargs and kwargs["user"] == self.user
|
||||
|
||||
@patch("modules.task_management.views.TaskService.update_task_entry")
|
||||
def test_perform_update_calls_service_and_saves(self, mock_update):
|
||||
# Create an instance and use serializer to update
|
||||
task = Task.objects.create(title="Old", description="", assigned_to=self.user)
|
||||
data = {"title": "Updated", "description": "new", "status": "aktivan"}
|
||||
serializer = TaskSerializer(instance=task, data=data, partial=True, context={"request": self.view.request})
|
||||
assert serializer.is_valid(), serializer.errors
|
||||
|
||||
# attach view.request and instance then call perform_update
|
||||
self.view.request = self.factory.put("/")
|
||||
self.view.request.user = self.user
|
||||
serializer.instance = task
|
||||
|
||||
self.view.perform_update(serializer)
|
||||
mock_update.assert_called_once_with(instance=task, data=serializer.validated_data, user=self.view.request.user)
|
||||
# confirm instance saved with new title
|
||||
task.refresh_from_db()
|
||||
assert task.title == "Updated"
|
||||
|
||||
def test_perform_destroy_soft_deletes(self):
|
||||
task = Task.objects.create(title="ToDelete", description="", assigned_to=self.user)
|
||||
self.view.perform_destroy(task)
|
||||
task.refresh_from_db()
|
||||
assert task.is_active is False
|
||||
9
backend/modules/task_management/urls.py
Normal file
9
backend/modules/task_management/urls.py
Normal file
@@ -0,0 +1,9 @@
|
||||
from rest_framework.routers import DefaultRouter
|
||||
from .views import TaskViewSet
|
||||
|
||||
router = DefaultRouter()
|
||||
# Registriramo ViewSet pod putanjom 'tasks'
|
||||
# Rezultat: /api/task_management/tasks/
|
||||
router.register(r'tasks', TaskViewSet, basename='task')
|
||||
|
||||
urlpatterns = router.urls
|
||||
64
backend/modules/task_management/views.py
Normal file
64
backend/modules/task_management/views.py
Normal file
@@ -0,0 +1,64 @@
|
||||
# backend/modules/task_management/views.py
|
||||
|
||||
import logging
|
||||
from rest_framework import viewsets, permissions
|
||||
from rest_framework.exceptions import ValidationError
|
||||
from .models import Task
|
||||
from .serializers import TaskSerializer
|
||||
from .services import TaskService
|
||||
|
||||
# Postavljanje loggera za modul
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class TaskViewSet(viewsets.ModelViewSet):
|
||||
"""
|
||||
ViewSet koji koristi TaskService za rukovanje operacijama.
|
||||
"""
|
||||
serializer_class = TaskSerializer
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
|
||||
def get_queryset(self):
|
||||
qs = Task.objects.filter(is_active=True).select_related(
|
||||
'assigned_to', 'vehicle', 'work_order', 'work_order__vehicle'
|
||||
)
|
||||
vehicle_id = self.request.query_params.get('vehicle')
|
||||
if vehicle_id:
|
||||
qs = qs.filter(vehicle_id=vehicle_id)
|
||||
return qs
|
||||
|
||||
def perform_create(self, serializer):
|
||||
"""
|
||||
Umjesto direktnog serializer.save(), pozivamo servis.
|
||||
Proslijeđujemo validirane podatke (dict) i korisnika.
|
||||
"""
|
||||
try:
|
||||
entry = TaskService.create_task_entry(
|
||||
data=serializer.validated_data,
|
||||
user=self.request.user)
|
||||
serializer.instance = entry
|
||||
except ValidationError as e:
|
||||
# Ponovno podižemo iznimku kako bi DRF ispravno vratio 400 Bad Request
|
||||
logger.warning(f"Validacijska greška pri kreiranju zadatka od strane {self.request.user}: {e}")
|
||||
raise e
|
||||
|
||||
def perform_update(self, serializer):
|
||||
"""
|
||||
Servis obavlja biznis logiku (validaciju/modifikaciju),
|
||||
a serializer.save() finalizira zapis.
|
||||
"""
|
||||
# Servis sada samo modificira instancu u memoriji
|
||||
TaskService.update_task_entry(
|
||||
instance=serializer.instance,
|
||||
data=serializer.validated_data,
|
||||
user=self.request.user
|
||||
)
|
||||
# Sada DRF sigurno sprema izmijenjenu instancu
|
||||
serializer.save()
|
||||
|
||||
def perform_destroy(self, instance):
|
||||
"""
|
||||
Umjesto fizičkog brisanja, radimo soft-delete ako je potrebno.
|
||||
"""
|
||||
instance.is_active = False
|
||||
instance.save()
|
||||
logger.info(f"Zapis zadatka {instance.id} je deaktiviran od strane {self.request.user}")
|
||||
Reference in New Issue
Block a user