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 %}