03019898b0
Добавлена офлайн/EndUser-подпись «ознакомлен и принял в работу» для документов отчётов и файловые вложения к задачам через Nextcloud. Co-authored-by: Cursor <cursoragent@cursor.com>
266 lines
10 KiB
Python
266 lines
10 KiB
Python
import datetime
|
|
|
|
from django.test import TestCase
|
|
from django.utils import timezone
|
|
|
|
from apps.employees.models import Employee
|
|
from apps.tasks.models import Task
|
|
|
|
|
|
def make_employee(username):
|
|
# sync-флаги выключены: тестам не нужны сигналы AD/Mattermost
|
|
emp = Employee.objects.create_user(
|
|
username=username, first_name='T', last_name='User',
|
|
hire_date=datetime.date(2020, 1, 1),
|
|
mattermost_sync_enabled=False, ad_sync_enabled=False,
|
|
)
|
|
# Любая группа = сотрудник видит открытые разделы (например /tasks/).
|
|
from django.contrib.auth.models import Group
|
|
emp.groups.add(Group.objects.get_or_create(name='Сотрудники (тест)')[0])
|
|
return emp
|
|
|
|
|
|
class TaskVisibilityTests(TestCase):
|
|
"""TaskQuerySet.for_user — задачу видят автор, відповідальний,
|
|
співвиконавець и спостерігач; посторонний — нет."""
|
|
|
|
@classmethod
|
|
def setUpTestData(cls):
|
|
cls.creator = make_employee('creator')
|
|
cls.responsible = make_employee('responsible')
|
|
cls.co = make_employee('co')
|
|
cls.observer = make_employee('observer')
|
|
cls.outsider = make_employee('outsider')
|
|
cls.task = Task.objects.create(title='T', creator=cls.creator)
|
|
cls.task.responsible.add(cls.responsible)
|
|
cls.task.co_executors.add(cls.co)
|
|
cls.task.observers.add(cls.observer)
|
|
|
|
def test_for_user_includes_all_roles(self):
|
|
for user in (self.creator, self.responsible, self.co, self.observer):
|
|
self.assertIn(self.task, Task.objects.for_user(user), msg=user.username)
|
|
|
|
def test_for_user_excludes_outsider(self):
|
|
self.assertNotIn(self.task, Task.objects.for_user(self.outsider))
|
|
|
|
def test_for_user_distinct(self):
|
|
# один и тот же пользователь в двух ролях → одна строка, не дубль
|
|
self.task.co_executors.add(self.responsible)
|
|
self.assertEqual(Task.objects.for_user(self.responsible).count(), 1)
|
|
|
|
|
|
class TaskParticipantTests(TestCase):
|
|
"""is_participant — статус меняют автор/відповідальний/співвиконавець,
|
|
спостерігач и посторонний — нет."""
|
|
|
|
@classmethod
|
|
def setUpTestData(cls):
|
|
cls.creator = make_employee('creator')
|
|
cls.responsible = make_employee('responsible')
|
|
cls.co = make_employee('co')
|
|
cls.observer = make_employee('observer')
|
|
cls.outsider = make_employee('outsider')
|
|
cls.task = Task.objects.create(title='T', creator=cls.creator)
|
|
cls.task.responsible.add(cls.responsible)
|
|
cls.task.co_executors.add(cls.co)
|
|
cls.task.observers.add(cls.observer)
|
|
|
|
def test_participants_can_change_status(self):
|
|
self.assertTrue(self.task.is_participant(self.creator))
|
|
self.assertTrue(self.task.is_participant(self.responsible))
|
|
self.assertTrue(self.task.is_participant(self.co))
|
|
|
|
def test_observer_and_outsider_cannot(self):
|
|
self.assertFalse(self.task.is_participant(self.observer))
|
|
self.assertFalse(self.task.is_participant(self.outsider))
|
|
|
|
|
|
class TaskStatusTests(TestCase):
|
|
@classmethod
|
|
def setUpTestData(cls):
|
|
cls.user = make_employee('u')
|
|
|
|
def _task(self, **kw):
|
|
return Task.objects.create(title='t', creator=self.user, **kw)
|
|
|
|
def test_overdue_when_past_due_and_open(self):
|
|
t = self._task(due_date=timezone.localdate() - datetime.timedelta(days=1))
|
|
self.assertTrue(t.is_overdue)
|
|
|
|
def test_not_overdue_when_done(self):
|
|
t = self._task(status='done',
|
|
due_date=timezone.localdate() - datetime.timedelta(days=1))
|
|
self.assertFalse(t.is_overdue)
|
|
|
|
def test_not_overdue_without_due_date(self):
|
|
self.assertFalse(self._task().is_overdue)
|
|
|
|
def test_set_status_done_sets_completed_at(self):
|
|
t = self._task()
|
|
t.set_status('done')
|
|
self.assertEqual(t.status, 'done')
|
|
self.assertIsNotNone(t.completed_at)
|
|
|
|
def test_set_status_non_done_clears_completed_at(self):
|
|
t = self._task(status='done', completed_at=timezone.now())
|
|
t.set_status('in_progress')
|
|
self.assertIsNone(t.completed_at)
|
|
|
|
|
|
class TaskChecklistViewTests(TestCase):
|
|
"""Відмітка пунктів чек-листа в деталі задачі — тільки учасник."""
|
|
|
|
@classmethod
|
|
def setUpTestData(cls):
|
|
from apps.tasks.models import TaskChecklistItem
|
|
cls.creator = make_employee('cl_creator')
|
|
cls.executor = make_employee('cl_exec')
|
|
cls.outsider = make_employee('cl_out')
|
|
cls.task = Task.objects.create(title='T', creator=cls.creator)
|
|
cls.task.co_executors.add(cls.executor)
|
|
cls.item = TaskChecklistItem.objects.create(task=cls.task, label='Пункт', order=0)
|
|
|
|
def _toggle(self, user):
|
|
from django.test import Client
|
|
c = Client()
|
|
c.force_login(user)
|
|
return c.post(f'/tasks/{self.task.pk}/',
|
|
{'action': 'checklist_toggle', 'item_id': self.item.pk},
|
|
HTTP_HX_REQUEST='true')
|
|
|
|
def test_participant_can_toggle(self):
|
|
self._toggle(self.executor)
|
|
self.item.refresh_from_db()
|
|
self.assertTrue(self.item.is_done)
|
|
self.assertEqual(self.item.done_by, self.executor)
|
|
|
|
def test_outsider_cannot_toggle(self):
|
|
resp = self._toggle(self.outsider)
|
|
self.assertEqual(resp.status_code, 404)
|
|
self.item.refresh_from_db()
|
|
self.assertFalse(self.item.is_done)
|
|
|
|
|
|
class TaskAttachmentTests(TestCase):
|
|
"""Загрузка / скачивание / удаление вложений задачи."""
|
|
|
|
@classmethod
|
|
def setUpTestData(cls):
|
|
cls.creator = make_employee('att_creator')
|
|
cls.responsible = make_employee('att_resp')
|
|
cls.outsider = make_employee('att_out')
|
|
cls.task = Task.objects.create(title='T', creator=cls.creator)
|
|
cls.task.responsible.add(cls.responsible)
|
|
|
|
def test_unique_stored_name_adds_suffix(self):
|
|
from apps.tasks.attachments import unique_stored_name
|
|
from apps.tasks.models import TaskAttachment
|
|
|
|
TaskAttachment.objects.create(
|
|
task=self.task, original_name='a.pdf', stored_name='a.pdf',
|
|
uploaded_by=self.creator,
|
|
)
|
|
self.assertEqual(unique_stored_name(self.task.pk, 'a.pdf'), 'a-2.pdf')
|
|
|
|
def test_participant_can_upload(self):
|
|
from unittest.mock import patch
|
|
|
|
from django.core.files.uploadedfile import SimpleUploadedFile
|
|
from django.test import Client
|
|
|
|
from apps.tasks.models import TaskAttachment
|
|
|
|
c = Client()
|
|
c.force_login(self.responsible)
|
|
f = SimpleUploadedFile('plan.pdf', b'%PDF-1.4', content_type='application/pdf')
|
|
with patch('apps.nextcloud_connector.webdav_service.NextcloudWebDAVService') as MockDAV:
|
|
MockDAV.return_value.upload.return_value = None
|
|
resp = c.post(
|
|
f'/tasks/{self.task.pk}/attachments/upload/',
|
|
{'file': f},
|
|
HTTP_HX_REQUEST='true',
|
|
)
|
|
self.assertEqual(resp.status_code, 200)
|
|
att = TaskAttachment.objects.get(task=self.task)
|
|
self.assertEqual(att.original_name, 'plan.pdf')
|
|
self.assertEqual(att.uploaded_by, self.responsible)
|
|
MockDAV.return_value.upload.assert_called_once()
|
|
path = MockDAV.return_value.upload.call_args[0][0]
|
|
self.assertEqual(path, f'07_Задачи/{self.task.pk}/plan.pdf')
|
|
|
|
def test_outsider_cannot_upload(self):
|
|
from unittest.mock import patch
|
|
|
|
from django.core.files.uploadedfile import SimpleUploadedFile
|
|
from django.test import Client
|
|
|
|
c = Client()
|
|
c.force_login(self.outsider)
|
|
f = SimpleUploadedFile('x.pdf', b'data', content_type='application/pdf')
|
|
with patch('apps.nextcloud_connector.webdav_service.NextcloudWebDAVService') as MockDAV:
|
|
resp = c.post(
|
|
f'/tasks/{self.task.pk}/attachments/upload/',
|
|
{'file': f},
|
|
HTTP_HX_REQUEST='true',
|
|
)
|
|
self.assertEqual(resp.status_code, 404)
|
|
MockDAV.assert_not_called()
|
|
|
|
def test_uploader_can_delete(self):
|
|
from unittest.mock import patch
|
|
|
|
from django.test import Client
|
|
|
|
from apps.tasks.models import TaskAttachment
|
|
|
|
att = TaskAttachment.objects.create(
|
|
task=self.task, original_name='a.pdf', stored_name='a.pdf',
|
|
size=10, uploaded_by=self.responsible,
|
|
)
|
|
c = Client()
|
|
c.force_login(self.responsible)
|
|
with patch('apps.nextcloud_connector.webdav_service.NextcloudWebDAVService') as MockDAV:
|
|
MockDAV.return_value.delete.return_value = None
|
|
resp = c.post(
|
|
f'/tasks/{self.task.pk}/attachments/{att.pk}/delete/',
|
|
HTTP_HX_REQUEST='true',
|
|
)
|
|
self.assertEqual(resp.status_code, 200)
|
|
self.assertFalse(TaskAttachment.objects.filter(pk=att.pk).exists())
|
|
|
|
def test_outsider_cannot_delete(self):
|
|
from django.test import Client
|
|
|
|
from apps.tasks.models import TaskAttachment
|
|
|
|
att = TaskAttachment.objects.create(
|
|
task=self.task, original_name='a.pdf', stored_name='a.pdf',
|
|
size=10, uploaded_by=self.creator,
|
|
)
|
|
c = Client()
|
|
c.force_login(self.outsider)
|
|
resp = c.post(f'/tasks/{self.task.pk}/attachments/{att.pk}/delete/')
|
|
self.assertEqual(resp.status_code, 404)
|
|
self.assertTrue(TaskAttachment.objects.filter(pk=att.pk).exists())
|
|
|
|
def test_download_for_participant(self):
|
|
from unittest.mock import patch
|
|
|
|
from django.test import Client
|
|
|
|
from apps.tasks.models import TaskAttachment
|
|
|
|
att = TaskAttachment.objects.create(
|
|
task=self.task, original_name='a.pdf', stored_name='a.pdf',
|
|
content_type='application/pdf', size=4, uploaded_by=self.creator,
|
|
)
|
|
c = Client()
|
|
c.force_login(self.responsible)
|
|
with patch('apps.nextcloud_connector.webdav_service.NextcloudWebDAVService') as MockDAV:
|
|
MockDAV.return_value.download.return_value = (b'%PDF', 'application/pdf')
|
|
resp = c.get(f'/tasks/{self.task.pk}/attachments/{att.pk}/download/')
|
|
self.assertEqual(resp.status_code, 200)
|
|
self.assertEqual(resp.content, b'%PDF')
|
|
self.assertIn('attachment', resp['Content-Disposition'])
|
|
|