Skip to content
14 changes: 14 additions & 0 deletions employees/common/strings.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,3 +74,17 @@ class AdminReportDetailStrings(NotCallableMixin, Enum):
PAGE_TITLE = ugettext_lazy("Report - ")
UPDATE_REPORT_BUTTON = ugettext_lazy("Update")
DISCARD_CHANGES_BUTTON = ugettext_lazy("Discard")


class ProjectReportListStrings(NotCallableMixin, Enum):
PAGE_TITLE = ugettext_lazy(": Reports")
DATE_COLUMN_HEADER = ugettext_lazy("Date")
PROJECT_COLUMN_HEADER = ugettext_lazy("Project")
AUTHOR_COLUMN_HEADER = ugettext_lazy("Author")
WORK_HOURS_COLUMN_HEADER = ugettext_lazy("Work hours")
DESCRIPTION_COLUMN_HEADER = ugettext_lazy("Description")
CREATION_DATE_COLUMN_HEADER = ugettext_lazy("Created")
LAST_UPDATE_COLUMN_HEADER = ugettext_lazy("Last update")
EDITED_COLUMN_HEADER = ugettext_lazy("Edited")
TASK_ACTIVITY_HEADER = ugettext_lazy("Task Activity")
NO_REPORTS_MESSAGE = ugettext_lazy("There are no reports for this project to display.")
84 changes: 84 additions & 0 deletions employees/static/employees/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -115,3 +115,87 @@ textarea.form-control, #id_description {
height: 200px;
resize: none;
}

.project-reports-date-header{
width: 11.23%;
}

.project-reports-project-header {
width:1%;
white-space:nowrap;
}


.project-reports-task-activity-header {
width:1%;
white-space:nowrap;
}

.project-reports-author-header {
width:1%;
white-space:nowrap;
}

.project-reports-work-hours-header {
width: 8.16%;
}

.project-reports-creation-date-header {
width: 10.09%;
}

.project-reports-last-update-header {
width: 10.09%;
}

.project-reports-editable-header {
width: 5.09%;
}

.project-reports-edit-button-header {
width: 4.91%;
}

.project-reports-date-column{
text-align: left;
}

.project-reports-project-column {
width:1%;
white-space:nowrap;
}

.project-reports-task-activity-column {
width:1%;
white-space:nowrap;
}

.project-reports-author-column {
width:1%;
white-space:nowrap;
text-align: left;
}

.project-reports-work-hours-column {
text-align: right;
}

.project-reports-description-column {
text-align: left;
}

.project-reports-creation-date-column {
text-align: center;
}

.project-reports-last-update-column {
text-align: center;
}

.project-reports-editable-column {
text-align: center;
}

.project-reports-edit-button-column {
text-align: right;
}
59 changes: 59 additions & 0 deletions employees/templates/employees/project_report_list.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
{% extends 'base.html' %}
{% load i18n %}
{% block content %}
{% load rest_framework %}
{% load static %}

<link rel="stylesheet" type="text/css" href="{% static 'employees/style.css' %}">

<h1>{{ object.name }}{{ UI_text.PAGE_TITLE.value }}</h1>
<div class="container">
<div class="table-responsive">
<table class="table">
<tr>
<th class="project-reports-date-header">{{ UI_text.DATE_COLUMN_HEADER.value }}</th>
<th class="project-reports-project-header">{{ UI_text.PROJECT_COLUMN_HEADER.value }}</th>
<th class="project-reports-task-activity-header">{{ UI_text.TASK_ACTIVITY_HEADER.value }}</th>
<th class="project-reports-author-header">{{ UI_text.AUTHOR_COLUMN_HEADER.value }}</th>
<th class="project-reports-work-hours-header">{{ UI_text.WORK_HOURS_COLUMN_HEADER.value }}</th>
<th class="project-reports-description-header">{{ UI_text.DESCRIPTION_COLUMN_HEADER.value }}</th>
<th class="project-reports-creation-date-header">{{ UI_text.CREATION_DATE_COLUMN_HEADER.value }}</th>
<th class="project-reports-last-update-header">{{ UI_text.LAST_UPDATE_COLUMN_HEADER.value }}</th>
<th class="project-reports-editable-header">{{ UI_text.EDITED_COLUMN_HEADER.value }}</th>
<td class="project-reports-edit-button-header Invisible"></td>
</tr>
<tbody>
{% regroup object.get_report_ordered by date as reports_by_date %}
{% for date in reports_by_date %}
{% for report in date.list %}
{% if forloop.first %}
<td class="project-reports-date-column" rowspan={{ date.list|length }}>
<strong>{{ report.date }}</strong>
</td>
{% else %}
<tr>
{% endif %}
<td class="project-reports-project-column" >{{ report.project.name }}</td>
<td class="project-reports-task-activity-column">{{ report.task_activities }}</td>
<td class="project-reports-author-column">{{ report.author }}</td>
<td class="project-reports-work-hours-column">{{ report.work_hours_str }}</td>
<td class="project-reports-description-column">{{ report.markdown_description|safe }}</td>
<td class="project-reports-creation-date-column">{{ report.creation_date }}</td>
<td class="project-reports-last-update-column">{{ report.last_update }}</td>
<td class="project-reports-editable-column">{{ report.editable|yesno:"False,True" }}</td>
<td class="project-reports-edit-button-column Invisible">
<a class="btn btn-info">
<span class="glyphicon glyphicon-pencil"></span>
</a>
</td>
</tr>
{% endfor %}
{% endfor %}
</tbody>
</table>
{% if object.get_report_ordered.count == 0 %}
<strong>{{ UI_text.NO_REPORTS_MESSAGE.value }}</strong>
{% endif %}
</div>
</div>
{% endblock %}
142 changes: 142 additions & 0 deletions employees/tests/test_unit_report_viewset.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@
from rest_framework.reverse import reverse
from rest_framework.test import APIRequestFactory

from employees.common.strings import ProjectReportListStrings
from employees.common.strings import ReportListStrings
from employees.models import Report
from employees.models import TaskActivityType
from employees.views import ProjectReportList
from employees.views import ReportDetail
from employees.views import ReportList
from employees.views import ReportViewSet
Expand Down Expand Up @@ -521,3 +523,143 @@ def test_delete_report_view_should_delete_report_on_post(self):
response = delete_report(request, pk=self.report.pk)
self.assertEqual(response.status_code, 302)
self.assertEqual(Report.objects.all().count(), 0)


class ProjectReportListTests(TestCase):
def _assert_response_contain_report(self, response, reports):
for report in reports:
dates = ["creation_date", "last_update"]
other_fields = ["description", "author", "task_activities"]
work_hours = str(report.work_hours).replace(".", ":")
fields_to_check = [work_hours]
for date in dates:
fields_to_check.append(
datetime.datetime.strftime(
datetime.datetime.fromtimestamp(int(getattr(report, date).timestamp())), "%B %d, %Y, %-I:%M"
)
)
for field in other_fields:
if field == "author":
fields_to_check.append(getattr(report, field).email)
else:
fields_to_check.append(getattr(report, field))
for field in fields_to_check:
self.assertContains(response, field)

def setUp(self):
self.user = CustomUser(
email="testuser@codepoets.it", password="newuserpasswd", first_name="John", last_name="Doe", country="PL"
)
self.user.full_clean()
self.user.save()

self.project = Project(name="Test Project", start_date=datetime.datetime.now())
self.project.full_clean()
self.project.save()
self.project.members.add(self.user)

self.report = Report(
date=datetime.datetime.now().date(),
description="Some description",
author=self.user,
project=self.project,
work_hours=Decimal("8.00"),
)
self.report.full_clean()
self.report.save()
self.client.force_login(self.user)
self.url = reverse("project-report-list", kwargs={"pk": self.project.pk})

def test_project_report_list_view_should_display_projects_report_list_on_get(self):
response = self.client.get(self.url)
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, ProjectReportList.template_name)
self.assertContains(response, self.project.name)
self._assert_response_contain_report(response, [self.report])

def test_project_report_list_view_should_not_be_accessible_for_unauthenticated_user(self):
self.client.logout()
response = self.client.get(self.url)
self.assertEqual(response.status_code, 302)

def test_project_report_list_view_should_not_display_non_existing_projects_reports(self):
response = self.client.get(reverse("project-report-list", kwargs={"pk": 999}))
self.assertEqual(response.status_code, 404)

def test_project_report_list_view_should_not_display_other_projects_reports(self):
other_project = Project(name="Other Project", start_date=datetime.datetime.now())
other_project.full_clean()
other_project.save()

other_report = Report(
date=datetime.datetime.now().date(),
description="Some other description",
author=self.user,
project=other_project,
work_hours=Decimal("8.00"),
)
other_report.full_clean()
other_report.save()

request = APIRequestFactory().get(path=reverse("project-report-list", args=(self.project.pk,)))
request.user = self.user
response = ProjectReportList.as_view()(request, pk=self.project.pk)
self.assertEqual(response.status_code, 200)
self.assertNotContains(response, other_report.description)

def test_project_report_list_view_should_display_message_if_project_has_no_reports(self):
other_project = Project(name="Other Project", start_date=datetime.datetime.now())
other_project.full_clean()
other_project.save()
response = self.client.get(reverse("project-report-list", kwargs={"pk": other_project.pk}))
self.assertEqual(response.status_code, 200)
self.assertContains(response, ProjectReportListStrings.NO_REPORTS_MESSAGE.value)

def test_that_project_report_list_should_return_list_of_all_reports_assigned_to_project(self):
other_user = CustomUser(
email="otheruser@codepoets.it", password="otheruserpasswd", first_name="Jane", last_name="Doe", country="PL"
)
other_user.full_clean()
other_user.save()
self.project.members.add(other_user)

other_project = Project(name="Project test", start_date=datetime.datetime.now())
other_project.full_clean()
other_project.save()
other_project.members.add(self.user)
other_project.members.add(other_user)

other_project_report = Report(
date=datetime.datetime.now().date(),
description="Some other description",
author=self.user,
project=other_project,
work_hours=Decimal("8.00"),
)
other_project_report.full_clean()
other_project_report.save()

other_report_1 = Report(
date=datetime.datetime.now().date(),
description="Some other description",
author=other_user,
project=self.project,
work_hours=Decimal("8.00"),
)
other_report_1.full_clean()
other_report_1.save()

other_report_2 = Report(
date=datetime.date(2001, 1, 1),
description="Some other description",
author=self.user,
project=self.project,
work_hours=Decimal("8.00"),
)
other_report_2.full_clean()
other_report_2.save()
response = self.client.get(self.url)
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, ProjectReportList.template_name)
self.assertContains(response, self.project.name)
self._assert_response_contain_report(response, [self.report, other_report_1, other_report_2])
1 change: 1 addition & 0 deletions employees/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,5 @@
url(r"^reports/(?P<pk>[0-9]+)/delete/$", views.delete_report, name="custom-report-delete"),
url(r"^reports/author/(?P<pk>[0-9]+)/$", views.AuthorReportView.as_view(), name="author-report-list"),
url(r"^reports/management/(?P<pk>[0-9]+)/$", views.AdminReportView.as_view(), name="admin-report-detail"),
url(r"^reports/project/(?P<pk>[0-9]+)/$", views.ProjectReportList.as_view(), name="project-report-list"),
]
13 changes: 13 additions & 0 deletions employees/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@

from employees.common.strings import AdminReportDetailStrings
from employees.common.strings import AuthorReportListStrings
from employees.common.strings import ProjectReportListStrings
from employees.common.strings import ReportDetailStrings
from employees.common.strings import ReportListStrings
from employees.forms import AdminReportForm
Expand Down Expand Up @@ -220,3 +221,15 @@ def form_valid(self, form: AdminReportForm) -> HttpResponseRedirectBase:
self.object.editable = True
self.object.save()
return super().form_valid(form)


@method_decorator(login_required, name="dispatch")
class ProjectReportList(DetailView):
template_name = "employees/project_report_list.html"
model = Project
queryset = Project.objects.prefetch_related("report_set")

def get_context_data(self, **kwargs: Any) -> dict:
context = super().get_context_data(**kwargs)
context["UI_text"] = ProjectReportListStrings
return context
3 changes: 3 additions & 0 deletions managers/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ class Project(models.Model):
def __str__(self) -> str:
return self.name

def get_report_ordered(self) -> QuerySet:
return self.report_set.select_related("task_activities").order_by("-date", "project__name")


@receiver(m2m_changed, sender=Project.managers.through)
def update_user_type(sender: Project, action: str, pk_set: Set, **kwargs: Any) -> None:
Expand Down
5 changes: 5 additions & 0 deletions managers/templates/managers/project_detail.html
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ <h2>
<span class="glyphicon glyphicon-pencil"></span>
</a>
</small>
<small class=Invisible align="center">
<a href="{% url 'project-report-list' pk=project.id %}">
<span class="glyphicon glyphicon-list-alt"></span>
</a>
</small>
</h2>

<br/>
Expand Down