Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions employees/common/strings.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
12 changes: 12 additions & 0 deletions employees/forms.py
Original file line number Diff line number Diff line change
@@ -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]
18 changes: 18 additions & 0 deletions employees/static/employees/scripts/join_popup_window.js
Original file line number Diff line number Diff line change
@@ -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');
});
});
22 changes: 21 additions & 1 deletion employees/templates/employees/report_list.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,37 @@
{% load rest_framework %}
{% load static %}

<link rel="stylesheet" type="text/css" href="{% static 'employees/style.css' %}">
<head>
<link rel="stylesheet" type="text/css" href="{% static 'employees/style.css' %}">
<link rel="stylesheet" href="//code.jquery.com/ui/1.12.1/themes/smoothness/jquery-ui.css">
<script src="//code.jquery.com/jquery-1.12.4.js"></script>
<script src="//code.jquery.com/ui/1.12.1/jquery-ui.js"></script>
<script type="text/javascript">
var join_discard_text = "{{ UI_text.JOIN_POPUP_NO.value }}";
</script>
<script src="{% static 'employees/scripts/join_popup_window.js' %}"></script>
</head>

<h1>{{ UI_text.PAGE_TITLE.value }}</h1>

<input type="button" id="opener" value="{{ UI_text.JOIN_PROJECT_BUTTON.value }}"/>

<div class="modal-dialog">
<form action="{% url 'custom-report-list' %}" method="POST">
{% csrf_token %}
{% render_form serializer template_pack='rest_framework/vertical' %}
<input type="submit" value="{{ UI_text.CREATE_REPORT_BUTTON.value }}">
</form>
</div>

<div id="dialog" style="display: none" title="{{ UI_text.JOIN_POPUP_HEADER.value }}" align="center">
<form action="{% url 'custom-report-list' %}" method="POST">
{% csrf_token %}
{{ project_form }}
<input type="submit" name="join" value="{{ UI_text.JOIN_POPUP_YES.value }}">
</form>
</div>

</br>
<div class="container">
<div class="table-responsive">
Expand Down
25 changes: 25 additions & 0 deletions employees/tests/test_unit_project_join_form.py
Original file line number Diff line number Diff line change
@@ -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)
111 changes: 111 additions & 0 deletions employees/tests/test_unit_report_viewset.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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(),
Expand Down Expand Up @@ -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):
Expand Down
78 changes: 63 additions & 15 deletions employees/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -37,37 +39,76 @@ 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,
)

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:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could be changed to some kind of Enum and reused in place where it is assigned to request. Thanks to this will not stop working if it will be changed in another place

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You mean the 'join' string? It's an id from HTML template, so I'd say it's name is rather irrelevant, but I could supply some sort of constant for it. The question is whether it's worth the extra effort?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea, you're right. It won't be necessary :)

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)


Expand All @@ -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,
Expand All @@ -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,
Expand Down