diff --git a/managers/forms.py b/managers/forms.py new file mode 100644 index 000000000..e3c5f9d60 --- /dev/null +++ b/managers/forms.py @@ -0,0 +1,11 @@ +from bootstrap_datepicker_plus import DatePickerInput +from django import forms + +from managers.models import Project + + +class ProjectForm(forms.ModelForm): + class Meta: + model = Project + fields = "__all__" + widgets = {"start_date": DatePickerInput(format="%Y-%m-%d"), "stop_date": DatePickerInput(format="%Y-%m-%d")} diff --git a/managers/migrations/0003_auto_20190405_0058.py b/managers/migrations/0003_auto_20190405_0058.py new file mode 100644 index 000000000..c8b233e18 --- /dev/null +++ b/managers/migrations/0003_auto_20190405_0058.py @@ -0,0 +1,19 @@ +# Generated by Django 2.1.1 on 2019-04-05 00:58 + +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('managers', '0002_auto_20190403_1125'), + ] + + operations = [ + migrations.AlterField( + model_name='project', + name='members', + field=models.ManyToManyField(help_text='How to add more employees? Select by CTRL + click', related_name='projects', to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/managers/models.py b/managers/models.py index 5f47279de..7ced1e13a 100644 --- a/managers/models.py +++ b/managers/models.py @@ -6,6 +6,7 @@ from django.db.models.query import QuerySet from django.db.models.signals import m2m_changed from django.dispatch import receiver +from django.utils.translation import gettext_lazy as _ from managers.commons.constants import MAX_NAME_LENGTH from users.models import CustomUser @@ -28,7 +29,9 @@ class Project(models.Model): stop_date = models.DateField(null=True, blank=True) terminated = models.BooleanField(default=False) managers = models.ManyToManyField(CustomUser, related_name="manager_projects") - members = models.ManyToManyField(CustomUser, related_name="projects") + members = models.ManyToManyField( + CustomUser, related_name="projects", help_text=_("How to add more employees? Select by CTRL + click") + ) objects = ProjectQuerySet.as_manager() diff --git a/managers/templates/managers/project_detail.html b/managers/templates/managers/project_detail.html index 11eedd904..3432f7453 100644 --- a/managers/templates/managers/project_detail.html +++ b/managers/templates/managers/project_detail.html @@ -11,7 +11,7 @@

 {{ project.name }}  - + diff --git a/managers/templates/managers/project_update.html b/managers/templates/managers/project_update.html new file mode 100644 index 000000000..89a15de9a --- /dev/null +++ b/managers/templates/managers/project_update.html @@ -0,0 +1,30 @@ +{% extends 'base.html' %} + +{% load i18n %} +{% load static %} +{% load rest_framework %} +{% load crispy_forms_tags %} + +{% block content %} + +

+ + + + + +  {{project.name}}  +

+ + +{% endblock %} + +{% block extra_script %} +{{ form.media }} +{% endblock %} diff --git a/managers/tests/test_api.py b/managers/tests/test_api.py index 8e7a53f94..edeb5e9d8 100644 --- a/managers/tests/test_api.py +++ b/managers/tests/test_api.py @@ -1,7 +1,12 @@ +import datetime + from django.test import TestCase from rest_framework.reverse import reverse +from rest_framework.test import APIRequestFactory +from managers import views from managers.factories import ProjectFactory +from managers.models import Project from users.models import CustomUser @@ -50,3 +55,141 @@ def test_project_list_view_should_display_project_list_ordered_by_name_regardles self.assertEqual(response.status_code, 200) self.assertEqual([project["name"] for project in response.data], expected_project_order) + + +class ProjectTest(TestCase): + def setUp(self): + super().setUp() + self.user = CustomUser( + email="testuser@codepoets.it", + first_name="John", + last_name="Doe", + country="PL", + user_type=CustomUser.UserType.ADMIN.name, + ) + self.user.set_password("newuserpasswd") + self.user.full_clean() + self.user.save() + self.client.force_login(self.user) + + self.project = Project( + name="Example Project", + start_date=datetime.datetime.now().date() - datetime.timedelta(days=30), + stop_date=datetime.datetime.now().date(), + terminated=False, + ) + self.project.full_clean() + self.project.save() + self.project.managers.add(self.user) + self.project.members.add(self.user) + + +class CustomProjectsListTests(ProjectTest): + def setUp(self): + super().setUp() + self.url = reverse("custom-projects-list") + + def test_project_list_view_should_display_projects_list_on_get(self): + request = APIRequestFactory().get(path=self.url) + request.user = self.user + response = views.ProjectsList.as_view()(request) + self.assertEqual(response.status_code, 200) + self.assertContains(response, self.project.name) + projects_list = response.context_data["object_list"] + self.assertTrue(self.project in projects_list) + + def test_projects_list_view_should_show_for_managers_only_own_projects(self): + self.user.user_type = CustomUser.UserType.MANAGER.name + manager_project_list = Project.objects.filter(managers__id=self.user.pk) + request = APIRequestFactory().get(path=self.url) + request.user = self.user + response = views.ProjectsList.as_view()(request) + self.assertEqual(response.status_code, 200) + self.assertContains(response, self.project.name) + projects_list = response.context_data["object_list"] + self.assertEqual(list(manager_project_list), list(projects_list)) + + def test_project_list_view_should_display_projects_sorted_by_name_ascending(self): + request = APIRequestFactory().get(path=self.url + "?sort=name") + request.user = self.user + response = views.ProjectsList.as_view()(request) + projects_list = response.context_data["object_list"] + self.assertTrue(projects_list.ordered) + self.assertTrue("name" in projects_list.query.order_by) + + def test_project_list_view_should_display_projects_sorted_by_name_descending(self): + request = APIRequestFactory().get(path=self.url + "?sort=-name") + request.user = self.user + response = views.ProjectsList.as_view()(request) + projects_list = response.context_data["object_list"] + self.assertTrue(projects_list.ordered) + self.assertTrue("-name" in projects_list.query.order_by) + + def test_project_list_view_should_display_projects_sorted_by_start_date_ascending(self): + request = APIRequestFactory().get(path=self.url + "?sort=start_date") + request.user = self.user + response = views.ProjectsList.as_view()(request) + projects_list = response.context_data["object_list"] + self.assertTrue(projects_list.ordered) + self.assertTrue("start_date" in projects_list.query.order_by) + + def test_project_list_view_should_display_projects_sorted_by_start_date_descending(self): + request = APIRequestFactory().get(path=self.url + "?sort=-start_date") + request.user = self.user + response = views.ProjectsList.as_view()(request) + projects_list = response.context_data["object_list"] + self.assertTrue(projects_list.ordered) + self.assertTrue("-start_date" in projects_list.query.order_by) + + def test_project_list_view_should_display_projects_sorted_by_stop_date_ascending(self): + request = APIRequestFactory().get(path=self.url + "?sort=stop_date") + request.user = self.user + response = views.ProjectsList.as_view()(request) + projects_list = response.context_data["object_list"] + self.assertTrue(projects_list.ordered) + self.assertTrue("stop_date" in projects_list.query.order_by) + + def test_project_list_view_should_display_projects_sorted_by_stop_date_descending(self): + request = APIRequestFactory().get(path=self.url + "?sort=-stop_date") + request.user = self.user + response = views.ProjectsList.as_view()(request) + projects_list = response.context_data["object_list"] + self.assertTrue(projects_list.ordered) + self.assertTrue("-stop_date" in projects_list.query.order_by) + + def test_project_list_view_should_display_projects_sorted_by_members_count_ascending(self): + request = APIRequestFactory().get(path=self.url + "?sort=members_count") + request.user = self.user + response = views.ProjectsList.as_view()(request) + projects_list = response.context_data["object_list"] + self.assertTrue(projects_list.ordered) + self.assertTrue("members_count" in projects_list.query.order_by) + + def test_project_list_view_should_display_projects_sorted_by_members_count_descending(self): + request = APIRequestFactory().get(path=self.url + "?sort=-members_count") + request.user = self.user + response = views.ProjectsList.as_view()(request) + projects_list = response.context_data["object_list"] + self.assertTrue(projects_list.ordered) + self.assertTrue("-members_count" in projects_list.query.order_by) + + +class ProjectDetailTests(ProjectTest): + def setUp(self): + super().setUp() + self.url = reverse("custom-project-detail", args=(self.project.pk,)) + + def test_project_detail_view_should_display_project_details_on_get(self): + request = APIRequestFactory().get(path=self.url) + request.user = self.user + response = views.ProjectDetail.as_view()(request, self.project.pk) + self.assertEqual(response.status_code, 200) + self.assertContains(response, self.project.name) + project = response.data["project"] + self.assertEqual(self.project, project) + + def test_project_detail_view_should_return_404_status_code_on_get_if_project_does_not_exist(self): + request = APIRequestFactory().get(path=self.url) + request.user = self.user + response = views.ProjectDetail.as_view()(request, self.project.pk + 1) + self.assertEqual(response.status_code, 404) diff --git a/managers/tests/test_unit_managers_apiview.py b/managers/tests/test_unit_managers_apiview.py deleted file mode 100644 index c85c22e0b..000000000 --- a/managers/tests/test_unit_managers_apiview.py +++ /dev/null @@ -1,142 +0,0 @@ -import datetime - -from django.test import TestCase -from rest_framework.reverse import reverse -from rest_framework.test import APIRequestFactory - -from managers import views -from managers.models import Project -from users.models import CustomUser - - -class ProjectTest(TestCase): - def setUp(self): - super().setUp() - self.user = CustomUser( - email="testuser@codepoets.it", - first_name="John", - last_name="Doe", - country="PL", - user_type=CustomUser.UserType.ADMIN.name, - ) - self.user.set_password("newuserpasswd") - self.user.full_clean() - self.user.save() - - self.project = Project( - name="Example Project", - start_date=datetime.datetime.now().date() - datetime.timedelta(days=30), - stop_date=datetime.datetime.now().date(), - terminated=False, - ) - self.project.full_clean() - self.project.save() - self.project.managers.add(self.user) - self.project.members.add(self.user) - self.custom_projects_list_url = [ - reverse("custom-projects-list"), - reverse("custom-project-detail", args=(self.project.pk,)), - ] - - -class ProjectsListTests(ProjectTest): - def test_project_list_view_should_display_projects_list_on_get(self): - request = APIRequestFactory().get(path=self.custom_projects_list_url[0]) - request.user = self.user - response = views.ProjectsList.as_view()(request) - self.assertEqual(response.status_code, 200) - self.assertContains(response, self.project.name) - projects_list = response.context_data["object_list"] - self.assertTrue(self.project in projects_list) - - def test_projects_list_view_should_show_for_managers_only_own_projects(self): - self.user.user_type = CustomUser.UserType.MANAGER.name - manager_project_list = Project.objects.filter(managers__id=self.user.pk) - request = APIRequestFactory().get(path=self.custom_projects_list_url[0]) - request.user = self.user - response = views.ProjectsList.as_view()(request) - self.assertEqual(response.status_code, 200) - self.assertContains(response, self.project.name) - projects_list = response.context_data["object_list"] - self.assertEqual(list(manager_project_list), list(projects_list)) - - def test_project_list_view_should_display_projects_sorted_by_name_ascending(self): - request = APIRequestFactory().get(path=self.custom_projects_list_url[0] + "?sort=name") - request.user = self.user - response = views.ProjectsList.as_view()(request) - projects_list = response.context_data["object_list"] - self.assertTrue(projects_list.ordered) - self.assertTrue("name" in projects_list.query.order_by) - - def test_project_list_view_should_display_projects_sorted_by_name_descending(self): - request = APIRequestFactory().get(path=self.custom_projects_list_url[0] + "?sort=-name") - request.user = self.user - response = views.ProjectsList.as_view()(request) - projects_list = response.context_data["object_list"] - self.assertTrue(projects_list.ordered) - self.assertTrue("-name" in projects_list.query.order_by) - - def test_project_list_view_should_display_projects_sorted_by_start_date_ascending(self): - request = APIRequestFactory().get(path=self.custom_projects_list_url[0] + "?sort=start_date") - request.user = self.user - response = views.ProjectsList.as_view()(request) - projects_list = response.context_data["object_list"] - self.assertTrue(projects_list.ordered) - self.assertTrue("start_date" in projects_list.query.order_by) - - def test_project_list_view_should_display_projects_sorted_by_start_date_descending(self): - request = APIRequestFactory().get(path=self.custom_projects_list_url[0] + "?sort=-start_date") - request.user = self.user - response = views.ProjectsList.as_view()(request) - projects_list = response.context_data["object_list"] - self.assertTrue(projects_list.ordered) - self.assertTrue("-start_date" in projects_list.query.order_by) - - def test_project_list_view_should_display_projects_sorted_by_stop_date_ascending(self): - request = APIRequestFactory().get(path=self.custom_projects_list_url[0] + "?sort=stop_date") - request.user = self.user - response = views.ProjectsList.as_view()(request) - projects_list = response.context_data["object_list"] - self.assertTrue(projects_list.ordered) - self.assertTrue("stop_date" in projects_list.query.order_by) - - def test_project_list_view_should_display_projects_sorted_by_stop_date_descending(self): - request = APIRequestFactory().get(path=self.custom_projects_list_url[0] + "?sort=-stop_date") - request.user = self.user - response = views.ProjectsList.as_view()(request) - projects_list = response.context_data["object_list"] - self.assertTrue(projects_list.ordered) - self.assertTrue("-stop_date" in projects_list.query.order_by) - - def test_project_list_view_should_display_projects_sorted_by_members_count_ascending(self): - request = APIRequestFactory().get(path=self.custom_projects_list_url[0] + "?sort=members_count") - request.user = self.user - response = views.ProjectsList.as_view()(request) - projects_list = response.context_data["object_list"] - self.assertTrue(projects_list.ordered) - self.assertTrue("members_count" in projects_list.query.order_by) - - def test_project_list_view_should_display_projects_sorted_by_members_count_descending(self): - request = APIRequestFactory().get(path=self.custom_projects_list_url[0] + "?sort=-members_count") - request.user = self.user - response = views.ProjectsList.as_view()(request) - projects_list = response.context_data["object_list"] - self.assertTrue(projects_list.ordered) - self.assertTrue("-members_count" in projects_list.query.order_by) - - -class ProjectDetailTests(ProjectTest): - def test_project_detail_view_should_display_project_details_on_get(self): - request = APIRequestFactory().get(path=self.custom_projects_list_url[1]) - request.user = self.user - response = views.ProjectDetail.as_view()(request, self.project.pk) - self.assertEqual(response.status_code, 200) - self.assertContains(response, self.project.name) - project = response.data["project"] - self.assertEqual(self.project, project) - - def test_project_detail_view_should_return_404_status_code_on_get_if_project_does_not_exist(self): - request = APIRequestFactory().get(path=self.custom_projects_list_url[1]) - request.user = self.user - response = views.ProjectDetail.as_view()(request, self.project.pk + 1) - self.assertEqual(response.status_code, 404) diff --git a/managers/tests/test_views.py b/managers/tests/test_views.py new file mode 100644 index 000000000..89a1ea9da --- /dev/null +++ b/managers/tests/test_views.py @@ -0,0 +1,39 @@ +from rest_framework.reverse import reverse + +from managers import views +from managers.tests.test_api import ProjectTest + + +class ProjectUpdateViewTestCase(ProjectTest): + def setUp(self): + super().setUp() + self.url = reverse("custom-project-update", kwargs={"pk": self.project.pk}) + self.data = { + "name": "New Example Project Name", + "start_date": self.project.start_date, + "managers": [self.user.pk], + "members": [self.user.pk], + } + + def test_project_update_view_should_display_update_template(self): + response = self.client.get(self.url) + self.assertEqual(response.status_code, 200) + self.assertContains(response, self.project.name) + self.assertTemplateUsed(response, views.ProjectUpdateView.template_name) + + def test_project_update_view_should_return_404_status_code_on_get_if_project_does_not_exist(self): + response = self.client.get(reverse("custom-project-update", kwargs={"pk": self.project.pk + 1})) + self.assertEqual(response.status_code, 404) + + def test_project_update_view_should_update_project_on_post(self): + response = self.client.post(self.url, self.data) + self.assertEqual(response.status_code, 302) + self.project.refresh_from_db() + self.assertEqual(self.project.name, self.data["name"]) + + def test_project_update_view_should_update_project_on_post_if_data_is_invalid(self): + del self.data["name"] + response = self.client.post(self.url, self.data) + self.assertEqual(response.status_code, 200) + self.project.refresh_from_db() + self.assertFormError(response, "form", "name", "This field is required.") diff --git a/managers/urls.py b/managers/urls.py index 8c90f97ed..ecab2276a 100644 --- a/managers/urls.py +++ b/managers/urls.py @@ -11,4 +11,5 @@ url("^api/", include(router.urls)), url("^projects/$", views.ProjectsList.as_view(), name="custom-projects-list"), url("^projects/(?P[0-9]+)/$", views.ProjectDetail.as_view(), name="custom-project-detail"), + url("^projects/(?P[0-9]+)/update/$", views.ProjectUpdateView.as_view(), name="custom-project-update"), ] diff --git a/managers/views.py b/managers/views.py index c0507d33a..1762b77cb 100644 --- a/managers/views.py +++ b/managers/views.py @@ -3,12 +3,15 @@ from django.db.models.query import QuerySet from django.http import HttpRequest from django.shortcuts import get_object_or_404 +from django.shortcuts import reverse from django.views.generic import ListView +from django.views.generic import UpdateView from rest_framework import renderers from rest_framework import viewsets from rest_framework.response import Response from rest_framework.views import APIView +from managers.forms import ProjectForm from managers.models import Project from managers.serializers import ProjectSerializer from users.models import CustomUser @@ -49,3 +52,12 @@ class ProjectDetail(APIView): def get(_request: HttpRequest, pk: int) -> Response: project = get_object_or_404(Project, pk=pk) return Response({"project": project}) + + +class ProjectUpdateView(UpdateView): + form_class = ProjectForm + model = Project + template_name = "managers/project_update.html" + + def get_success_url(self) -> None: + return reverse("custom-project-detail", kwargs={"pk": self.kwargs["pk"]}) diff --git a/requirements.lock b/requirements.lock index ad870a367..5ca61dafd 100644 --- a/requirements.lock +++ b/requirements.lock @@ -1,15 +1,19 @@ appdirs==1.4.3 atomicwrites==1.2.1 attrs==18.2.0 +backcall==0.1.0 black==19.3b0 certifi==2018.8.24 chardet==3.0.4 Click==7.0 coverage==4.5.2 +decorator==4.3.2 defusedxml==0.5.0 Django==2.1.1 django-allauth==0.37.1 +django-bootstrap-datepicker-plus==3.0.5 django-countries==5.3.2 +django-crispy-forms==1.7.2 django-markdown-deux==1.0.5 django-mock-queries==2.1.1 django-rest-auth==0.9.3 @@ -20,19 +24,28 @@ Faker==1.0.4 flake8==3.7.5 freezegun==0.3.11 idna==2.7 +ipython==7.3.0 +ipython-genutils==0.2.0 +jedi==0.13.3 markdown2==2.3.7 mccabe==0.6.1 mock==2.0.0 model-mommy==1.6.0 more-itertools==5.0.0 oauthlib==2.1.0 +parso==0.3.4 pbr==5.1.2 +pexpect==4.6.0 +pickleshare==0.7.5 pluggy==0.8.1 +prompt-toolkit==2.0.9 psycopg2==2.7.5 psycopg2-binary==2.7.7 +ptyprocess==0.6.0 py==1.7.0 pycodestyle==2.5.0 pyflakes==2.1.1 +Pygments==2.3.1 pytest==4.1.1 pytest-cov==2.6.1 pytest-django==3.4.5 @@ -45,4 +58,6 @@ requests-oauthlib==1.0.0 six==1.11.0 text-unidecode==1.2 toml==0.10.0 +traitlets==4.3.2 urllib3==1.23 +wcwidth==0.1.7 diff --git a/requirements.txt b/requirements.txt index 1e4492bf1..1ed3142a6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,8 +3,10 @@ certifi chardet defusedxml Django >= 2.1, < 2.2 -django-countries django-allauth +django-bootstrap-datepicker-plus +django-countries +django-crispy-forms django-markdown-deux django-mock-queries django-rest-auth diff --git a/sheetstorm/settings/base.py b/sheetstorm/settings/base.py index f4f1018cc..8140447d2 100644 --- a/sheetstorm/settings/base.py +++ b/sheetstorm/settings/base.py @@ -40,6 +40,8 @@ 'django.contrib.messages', 'django.contrib.staticfiles', 'django_countries', + 'bootstrap_datepicker_plus', + 'crispy_forms', 'users', 'markdown_deux', 'rest_framework', @@ -182,3 +184,5 @@ 'FR', 'US', ] + +CRISPY_TEMPLATE_PACK = 'bootstrap4' diff --git a/users/templates/base.html b/users/templates/base.html index bf1f1c6b2..32ceb2b51 100644 --- a/users/templates/base.html +++ b/users/templates/base.html @@ -1,21 +1,16 @@ +{% load static %} +{% load i18n %} +{% load rest_framework %} +{% load user_type_tags %} - {% load static %} - {% load i18n %} - {% load rest_framework %} - {% load user_type_tags %} - - - + {% block title %}Sheet storm{% endblock %} + - - - - {% block title %}Sheet storm{% endblock %} - + {% block extra_head %}{% endblock %}
@@ -57,5 +52,8 @@ {% endblock %} + + + {% block extra_script %}{% endblock %}