diff --git a/employees/common/strings.py b/employees/common/strings.py index 7af4bc42a..127b8cd75 100644 --- a/employees/common/strings.py +++ b/employees/common/strings.py @@ -14,6 +14,10 @@ class ReportListStrings(Enum): PAGE_TITLE = ugettext_lazy("Reports") CREATE_REPORT_BUTTON = ugettext_lazy("Create") + JOIN_PROJECT_BUTTON = ugettext_lazy("Join project") + JOIN_POPUP_HEADER = ugettext_lazy("Join project") + JOIN_POPUP_YES = ugettext_lazy("Join") + JOIN_POPUP_NO = ugettext_lazy("Cancel") DATE_COLUMN_HEADER = ugettext_lazy("Date") PROJECT_COLUMN_HEADER = ugettext_lazy("Project") WORK_HOURS_COLUMN_HEADER = ugettext_lazy("Work hours") diff --git a/employees/forms.py b/employees/forms.py new file mode 100644 index 000000000..6c2aa5c48 --- /dev/null +++ b/employees/forms.py @@ -0,0 +1,12 @@ +from django import forms +from django.db.models import QuerySet + + +class ProjectJoinForm(forms.Form): + + projects = forms.ChoiceField(choices=[]) + + def __init__(self, queryset, *args, **kwargs): + super().__init__(*args, **kwargs) + assert isinstance(queryset, QuerySet) + self.fields['projects'].choices = [(project.id, project.name) for project in queryset] diff --git a/employees/static/employees/scripts/join_popup_window.js b/employees/static/employees/scripts/join_popup_window.js new file mode 100644 index 000000000..f66fa7842 --- /dev/null +++ b/employees/static/employees/scripts/join_popup_window.js @@ -0,0 +1,18 @@ +$(function () { + $("#dialog").dialog ({ + modal: true, + autoOpen: false, + buttons : [ + { + text: join_discard_text, + click: function () { + $(this).dialog('close'); + } + } + ] + }).prev().find(".ui-dialog-titlebar-close").hide (); + + $("#opener").click(function () { + $("#dialog").dialog('open'); + }); +}); \ No newline at end of file diff --git a/employees/templates/employees/report_list.html b/employees/templates/employees/report_list.html index b9077a1d8..1bce1da11 100644 --- a/employees/templates/employees/report_list.html +++ b/employees/templates/employees/report_list.html @@ -4,10 +4,21 @@ {% load rest_framework %} {% load static %} - + + + + + + + +

{{ UI_text.PAGE_TITLE.value }}

+ + + + +
diff --git a/employees/tests/test_unit_project_join_form.py b/employees/tests/test_unit_project_join_form.py new file mode 100644 index 000000000..b170ec6d5 --- /dev/null +++ b/employees/tests/test_unit_project_join_form.py @@ -0,0 +1,25 @@ +import datetime +from django.test import TestCase + +from employees.forms import ProjectJoinForm +from managers.models import Project + + +class ProjectJoinFormTests(TestCase): + def test_project_join_form_should_create_choice_field_with_project_name_and_id_based_on_queryset_provided_in_constructor(self): + queryset_length = 10 + for i in range(queryset_length): + project = Project( + name=f"Test Project {i}", + start_date=datetime.datetime.now(), + ) + project.full_clean() + project.save() + queryset = Project.objects.all() + form = ProjectJoinForm(queryset) + choices = form.fields['projects'].choices + self.assertIsNotNone(choices) + self.assertEqual(len(choices), queryset_length) + for i in range(queryset_length): + self.assertEqual(choices[i][0], queryset[i].id) + self.assertEqual(choices[i][1], queryset[i].name) diff --git a/employees/tests/test_unit_report_viewset.py b/employees/tests/test_unit_report_viewset.py index c3e1ee43f..60e448b91 100644 --- a/employees/tests/test_unit_report_viewset.py +++ b/employees/tests/test_unit_report_viewset.py @@ -315,6 +315,78 @@ def test_get_queryset_method_should_return_queryset_containing_all_of_current_us self.assertEqual(queryset[1], self.report) self.assertEqual(queryset[2], other_report_2) + def test_custom_report_list_add_project_method_should_register_current_user_as_project_member(self): + new_project = Project( + name="New Project", + start_date=datetime.datetime.now(), + ) + new_project.full_clean() + new_project.save() + request = APIRequestFactory().get(path=self.url) + request.user = self.user + view = ReportList() + view.request = request + serializer = view._create_serializer() + view._add_project(serializer, new_project) + self.assertTrue(self.user in new_project.members.all()) + self.assertEqual(serializer.fields['project'].initial, new_project) + + def test_custom_report_list_create_serializer_method_should_return_serializer_with_project_field_options_containing_only_projects_to_which_current_user_belongs(self): + new_project = Project( + name="New Project", + start_date=datetime.datetime.now(), + ) + new_project.full_clean() + new_project.save() + new_project.members.add(self.user) + new_project.full_clean() + new_project.save() + request = APIRequestFactory().get(path=self.url) + request.user = self.user + view = ReportList() + view.request = request + serializer = view._create_serializer() + self.assertTrue(new_project in serializer.fields['project'].queryset) + self.assertTrue(self.project not in serializer.fields['project'].queryset) + + def test_custom_report_list_view_should_add_user_to_project_selected_in_project_join_form_on_join(self): + new_project = Project( + name="New Project", + start_date=datetime.datetime.now(), + ) + new_project.full_clean() + new_project.save() + request = APIRequestFactory().post( + path=self.url, + data={ + 'projects': new_project.id, + 'join': "join", + } + ) + request.user = self.user + response = ReportList.as_view()(request) + self.assertEqual(response.status_code, 200) + self.assertTrue(self.user in new_project.members.all()) + self.assertEqual(response.data['serializer'].fields['project'].initial, new_project) + + def test_custom_report_list_view_should_not_add_user_to_project_selected_in_project_join_form_on_post(self): + new_project = Project( + name="New Project", + start_date=datetime.datetime.now(), + ) + new_project.full_clean() + new_project.save() + request = APIRequestFactory().post( + path=self.url, + data={ + 'projects': new_project.id, + } + ) + request.user = self.user + response = ReportList.as_view()(request) + self.assertEqual(response.status_code, 200) + self.assertFalse(self.user in new_project.members.all()) + class ReportDetailTests(TestCase): def setUp(self): @@ -334,6 +406,7 @@ def setUp(self): ) self.project.full_clean() self.project.save() + self.project.members.add(self.user) self.report = Report( date=datetime.datetime.now().date(), @@ -417,6 +490,44 @@ def test_custom_report_detail_view_should_not_update_report_on_post_if_form_is_i self.assertIsNotNone(response.data['errors']) self.assertNotEqual(new_description, self.report.description) + def test_custom_report_detail_view_should_not_update_report_if_author_is_not_a_member_of_selected_project(self): + other_project = Project( + name="Other Project", + start_date=datetime.datetime.now(), + ) + other_project.full_clean() + other_project.save() + new_description = 'Some other description' + request = APIRequestFactory().post( + path=reverse('custom-report-detail', args=(self.report.pk,)), + data={ + 'date': datetime.datetime.now().date(), + 'description': new_description, + 'project': other_project, + 'work_hours': Decimal('8.00'), + }, + ) + request.user = self.user + response = ReportDetail.as_view()(request, pk=self.report.pk) + self.report.refresh_from_db() + self.assertEqual(response.status_code, 200) + self.assertEqual(response.data['errors']['project'][0].code, 'does_not_exist') + self.assertNotEqual(new_description, self.report.description) + self.assertNotEqual(other_project, self.report.project) + + def test_custom_report_detail_view_project_field_should_not_display_projects_the_author_is_not_a_member_of(self): + other_project = Project( + name="Other Project", + start_date=datetime.datetime.now(), + ) + other_project.full_clean() + other_project.save() + request = APIRequestFactory().get(path=reverse('custom-report-detail', args=(self.report.pk,))) + request.user = self.user + response = ReportDetail.as_view()(request, pk=self.report.pk) + self.assertEqual(response.status_code, 200) + self.assertTrue(other_project not in response.data['serializer']._fields['project'].queryset) + class DeleteReportTests(TestCase): def setUp(self): diff --git a/employees/views.py b/employees/views.py index 28e71640b..c8e87d09b 100644 --- a/employees/views.py +++ b/employees/views.py @@ -8,8 +8,10 @@ from employees.common.strings import ReportDetailStrings from employees.common.strings import ReportListStrings +from employees.forms import ProjectJoinForm from employees.models import Report from employees.serializers import ReportSerializer +from managers.models import Project class ReportViewSet(viewsets.ModelViewSet): @@ -37,6 +39,8 @@ def query_as_dict(query_set): class ReportList(APIView): renderer_classes = [renderers.TemplateHTMLRenderer] template_name = 'employees/report_list.html' + reports_dict = {} + project_form = '' permission_classes = ( permissions.IsAuthenticated, ) @@ -44,30 +48,67 @@ class ReportList(APIView): def get_queryset(self): return Report.objects.filter(author=self.request.user).order_by('-date', 'project__name') - def get(self, request): - reports_serializer = ReportSerializer(context={'request': request}) - reports_dict = query_as_dict(self.get_queryset()) + def _add_project(self, serializer, project): + project.members.add(self.request.user) + project.full_clean() + project.save() + serializer.fields['project'].initial = project + + def _create_serializer(self): + reports_serializer = ReportSerializer(context={'request': self.request}, ) + reports_serializer.fields['project'].queryset = \ + Project.objects.filter( + members__id=self.request.user.id + ).order_by('name') + return reports_serializer + + def initial(self, request, *args, **kwargs): + super().initial(request, *args, **kwargs) + self.reports_dict = query_as_dict(self.get_queryset()) + self.project_form = ProjectJoinForm( + queryset=Project.objects.exclude(members__id=self.request.user.id).order_by('name'), + ) + + def get(self, _request): return Response({ - 'serializer': reports_serializer, - 'reports_dict': reports_dict, + 'serializer': self._create_serializer(), + 'reports_dict': self.reports_dict, 'UI_text': ReportListStrings, + 'project_form': self.project_form, }) def post(self, request): reports_serializer = ReportSerializer(data=request.data, context={'request': request}) - if not reports_serializer.is_valid(): + if 'join' in request.POST: + project_id = request.POST['projects'] + project = Project.objects.get(id=int(project_id)) + self._add_project(serializer=reports_serializer, project=project) + self.project_form = ProjectJoinForm( + queryset=Project.objects.exclude(members__id=self.request.user.id).order_by('name'), + ) + reports_serializer = self._create_serializer() + reports_serializer.fields['project'].initial = project + return Response({ + 'serializer': reports_serializer, + 'reports_dict': self.reports_dict, + 'UI_text': ReportListStrings, + 'project_form': self.project_form, + }) + + elif not reports_serializer.is_valid(): return Response({ 'serializer': reports_serializer, - 'reports_dict': query_as_dict(self.get_queryset()), + 'reports_dict': self.reports_dict, 'errors': reports_serializer.errors, 'UI_text': ReportListStrings, + 'project_form': self.project_form, }) reports_serializer.save(author=self.request.user) - reports_serializer = ReportSerializer(context={'request': request}) return Response({ - 'serializer': reports_serializer, + 'serializer': self._create_serializer(), 'reports_dict': query_as_dict(self.get_queryset()), 'UI_text': ReportListStrings, + 'project_form': self.project_form, }, status=201) @@ -78,9 +119,20 @@ class ReportDetail(APIView): permissions.IsAuthenticated, ) + def _create_serializer(self, report, data=None): + if data is None: + reports_serializer = ReportSerializer(report, context={'request': self.request},) + else: + reports_serializer = ReportSerializer(report, data=data, context={'request': self.request}, ) + reports_serializer.fields['project'].queryset = \ + Project.objects.filter( + members__id=report.author.pk + ).order_by('name') + return reports_serializer + def get(self, request, pk): report = get_object_or_404(Report, pk=pk) - serializer = ReportSerializer(report, context={'request': request}) + serializer = self._create_serializer(report) return Response({ 'serializer': serializer, 'report': report, @@ -90,11 +142,7 @@ def get(self, request, pk): def post(self, request, pk): if "discard" not in request.POST: report = get_object_or_404(Report, pk=pk) - serializer = ReportSerializer( - report, - data=request.data, - context={'request': request} - ) + serializer = self._create_serializer(report, request.data) if not serializer.is_valid(): return Response({ 'serializer': serializer,