From 2fd0dd7d4ef12cc88d55d00541ae622fa0154a41 Mon Sep 17 00:00:00 2001 From: Maciej Date: Fri, 21 Dec 2018 16:52:44 +0100 Subject: [PATCH 1/7] Add form for project joining feature Adds form with singular `ChoiceField` which is supposed to contain a list of projects the user can join to. --- employees/forms.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 employees/forms.py 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] From 8a51fff9dafb77b645d53318c2b1b36c90519a3f Mon Sep 17 00:00:00 2001 From: Maciej Date: Wed, 9 Jan 2019 16:56:23 +0100 Subject: [PATCH 2/7] Add join feature related GUI text for report list view --- employees/common/strings.py | 4 ++++ 1 file changed, 4 insertions(+) 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") From a2f365f4d1b931a79fccecc909f82dbc5c578a3d Mon Sep 17 00:00:00 2001 From: Maciej Date: Wed, 9 Jan 2019 17:05:11 +0100 Subject: [PATCH 3/7] Update report list view for project join feature support Due to increased complexity of its' functionality, the class' structure has been heavily modified, introducing helper methods and additional fields. Most important of all, an `initial` method, which contains actions to be performed before each HTTP call, has been overridden to include initiation of aforementioned fields. --- employees/views.py | 59 +++++++++++++++++++++++++++++++++++++++------- 1 file changed, 50 insertions(+), 9 deletions(-) diff --git a/employees/views.py b/employees/views.py index 28e71640b..411661018 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) From 5a00f3dfa28114873f72cf42483872cf2659b796 Mon Sep 17 00:00:00 2001 From: Maciej Date: Tue, 12 Feb 2019 15:27:46 +0100 Subject: [PATCH 4/7] Add script for join form's popup in report list --- .../employees/scripts/join_popup_window.js | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 employees/static/employees/scripts/join_popup_window.js 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 From c699eb4e461c20cd624a79ba2dfc6d43969b529e Mon Sep 17 00:00:00 2001 From: Maciej Date: Wed, 9 Jan 2019 17:07:43 +0100 Subject: [PATCH 5/7] Add feature to template in form of a pop-up window Added javascript and HTML code for pop-up functionality --- .../templates/employees/report_list.html | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) 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 }}

+ + + + +
From 3ef31a9a7bb199f4abf5f952939317fb14ad9cb4 Mon Sep 17 00:00:00 2001 From: Maciej Date: Fri, 22 Mar 2019 14:11:16 +0100 Subject: [PATCH 6/7] Limit project choice in report detail view Limits projects displayed in project field in report detail view to only those which author is a member of. --- employees/views.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/employees/views.py b/employees/views.py index 411661018..c8e87d09b 100644 --- a/employees/views.py +++ b/employees/views.py @@ -119,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, @@ -131,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, From dbc9487ac8bd2c8cd4389ede12655b58a3764d85 Mon Sep 17 00:00:00 2001 From: Maciej Date: Mon, 21 Jan 2019 15:39:37 +0100 Subject: [PATCH 7/7] Add unit tests for join feature Adds tests for project join form and extends tests in custom views, regarding applied changes to code. --- .../tests/test_unit_project_join_form.py | 25 ++++ employees/tests/test_unit_report_viewset.py | 111 ++++++++++++++++++ 2 files changed, 136 insertions(+) create mode 100644 employees/tests/test_unit_project_join_form.py 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):