From 1b7490084b127484fe00cc293dd57d1618649d10 Mon Sep 17 00:00:00 2001
From: Ichlasul Affan <ichlaffterlalu@gmail.com>
Date: Sun, 6 Oct 2019 09:48:08 +0700
Subject: [PATCH 01/14] Add test to show list of milestones

---
 core/models/vacancies.py     |  3 +++
 core/tests/test_vacancies.py | 40 +++++++++++++++++++++++++++++++++++-
 core/views/vacancies.py      |  6 +++++-
 kape/urls.py                 |  4 +++-
 4 files changed, 50 insertions(+), 3 deletions(-)

diff --git a/core/models/vacancies.py b/core/models/vacancies.py
index f2b5dc49..9fd1bb0d 100644
--- a/core/models/vacancies.py
+++ b/core/models/vacancies.py
@@ -32,3 +32,6 @@ class Application(models.Model):
     class Meta:
         unique_together = (("student", "vacancy"),)
 
+
+class VacancyMilestone(models.Model):
+    pass
diff --git a/core/tests/test_vacancies.py b/core/tests/test_vacancies.py
index 1667ac14..15fe1cb4 100644
--- a/core/tests/test_vacancies.py
+++ b/core/tests/test_vacancies.py
@@ -6,7 +6,7 @@ from rest_framework import status
 from rest_framework.test import APITestCase
 
 from core.models.accounts import Company, Student, Supervisor
-from core.models.vacancies import Vacancy, Application
+from core.models.vacancies import Vacancy, Application, VacancyMilestone
 
 
 class ApplicationTests(APITestCase):
@@ -433,3 +433,41 @@ class SupervisorApprovalTests(APITestCase):
         response = self.client.patch(url, format='json')
         self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
         self.assertEqual(new_vacancy.verified, False)
+
+
+class VacancyMilestoneTests(APITestCase):
+    def test_vacancy_milestones_list(self):
+        company_user = User.objects.create_user('dummy.company2', 'dummy.compan2y@company.com', 'lalala123')
+        new_company = Company.objects.create(user=company_user, description="lalala", status=Company.VERIFIED, logo=None,
+                                              address=None)
+        new_vacancy = Vacancy.objects.create(company=new_company, verified=True, open_time=datetime.fromtimestamp(0),
+                                              description="lalala", close_time=datetime.today())
+        milestone1 = VacancyMilestone.objects.create(vacancy=new_vacancy, name="initiate", detail="install things",
+                                                     expected_start=datetime.fromtimestamp(0),
+                                                     expected_finish=datetime.fromtimestamp(86400))
+
+        url = '/api/vacancies/' + str(new_vacancy.pk) + '/milestones/'
+        response = self.client.get(url, format='json')
+        self.assertEqual(response.status_code, status.HTTP_200_OK)
+        self.assertEqual(len(response.data), 1)
+        self.assertEqual(response.data[0]["name"], milestone1.name)
+        self.assertEqual(response.data[0]["detail"], milestone1.detail)
+
+
+    def test_create_new_milestone_on_a_vacancy_success(self):
+        pass
+
+    def test_create_new_milestone_on_a_vacancy_unauthorized_user(self):
+        pass
+
+    def test_modify_milestone_on_a_vacancy_success(self):
+        pass
+
+    def test_modify_milestone_on_a_vacancy_unauthorized_user(self):
+        pass
+
+    def test_delete_milestone_on_a_vacancy_success(self):
+        pass
+
+    def test_delete_milestone_on_a_vacancy_unauthorized_user(self):
+        pass
diff --git a/core/views/vacancies.py b/core/views/vacancies.py
index e4b8b989..19b13fc8 100644
--- a/core/views/vacancies.py
+++ b/core/views/vacancies.py
@@ -12,7 +12,7 @@ from core.lib.mixins import MultiSerializerViewSetMixin
 from core.lib.permissions import IsAdminOrStudent, IsAdminOrCompany, IsAdminOrVacancyOwner, AsAdminOrSupervisor, \
     VacancyApprovalPermission
 from core.models import Student, Company
-from core.models.vacancies import Vacancy, Application
+from core.models.vacancies import Vacancy, Application, VacancyMilestone
 from core.serializers.vacancies import VacancySerializer, ApplicationSerializer, ApplicationStatusSerializer, \
     PostVacancySerializer, VacancyVerifiedSerializer, SupervisorStudentApplicationSerializer
 from core.views.accounts import StudentViewSet
@@ -275,3 +275,7 @@ class BookmarkedVacancyByStudentViewSet(viewsets.GenericViewSet):
         student.bookmarked_vacancies.remove(vacancy)
         return Response(
             self.serializer_class(student.bookmarked_vacancies, many=True, context={'request': request}).data)
+
+
+class VacancyMilestoneViewSet(viewsets.GenericViewSet):
+    pass
diff --git a/kape/urls.py b/kape/urls.py
index 21a1d46b..966abf3d 100755
--- a/kape/urls.py
+++ b/kape/urls.py
@@ -25,7 +25,7 @@ from core import apps
 from core.views.accounts import StudentViewSet, CompanyViewSet, SupervisorViewSet, UserViewSet, LoginViewSet, \
     CompanyRegisterViewSet
 from core.views.vacancies import VacancyViewSet, BookmarkedVacancyByStudentViewSet, StudentApplicationViewSet, \
-    CompanyApplicationViewSet, CompanyVacanciesViewSet, ApplicationViewSet
+    CompanyApplicationViewSet, CompanyVacanciesViewSet, ApplicationViewSet, VacancyMilestoneViewSet
 
 schema_view = get_swagger_view()
 router = routers.DefaultRouter()
@@ -38,6 +38,8 @@ router.register(r'register', CompanyRegisterViewSet)
 router.register(r'vacancies', VacancyViewSet)
 router.register(r'applications', ApplicationViewSet)
 # router.register(r'students/(?P<student_id>\d+)/profile', StudentProfileViewSet)
+router.register(r'vacancies/(?P<vacancy_id>\d+)/milestones', VacancyMilestoneViewSet,
+                base_name='vacancy-milestones')
 router.register(r'students/(?P<student_id>\d+)/bookmarked-vacancies', BookmarkedVacancyByStudentViewSet,
                 base_name='bookmarked-vacancy-list')
 router.register(r'students/(?P<student_id>\d+)/applied-vacancies', StudentApplicationViewSet,
-- 
GitLab


From 08120446259f153711f5a8d62e211ce561aee52b Mon Sep 17 00:00:00 2001
From: Ichlasul Affan <ichlaffterlalu@gmail.com>
Date: Sun, 6 Oct 2019 10:03:36 +0700
Subject: [PATCH 02/14] Create test for model

---
 core/tests/test_vacancies.py | 21 +++++++++++++++++++++
 1 file changed, 21 insertions(+)

diff --git a/core/tests/test_vacancies.py b/core/tests/test_vacancies.py
index 15fe1cb4..f164a18a 100644
--- a/core/tests/test_vacancies.py
+++ b/core/tests/test_vacancies.py
@@ -1,6 +1,7 @@
 from datetime import datetime
 
 import requests_mock
+from django.core.exceptions import ValidationError
 from django.contrib.auth.models import User
 from rest_framework import status
 from rest_framework.test import APITestCase
@@ -436,6 +437,26 @@ class SupervisorApprovalTests(APITestCase):
 
 
 class VacancyMilestoneTests(APITestCase):
+    def test_vacancy_milestone_model(self):
+        new_vacancy = Vacancy.objects.create(company=new_company, verified=True, open_time=datetime.fromtimestamp(0),
+                                              description="lalala", close_time=datetime.today())
+        milestone1 = VacancyMilestone(vacancy=new_vacancy, name="initiate", detail="install things",
+                                                     expected_start=datetime.fromtimestamp(0),
+                                                     expected_finish=datetime.fromtimestamp(86400))
+        milestone1.full_clean()
+
+        milestone2 = VacancyMilestone(vacancy=new_vacancy, name="a"*101, detail="install things",
+                                                     expected_start=datetime.fromtimestamp(0),
+                                                     expected_finish=datetime.fromtimestamp(86400))
+        with self.assertRaises(ValidationError, msg="Name with more than 100 character should raise ValidationError"):
+            milestone2.full_clean()
+
+        milestone3 = VacancyMilestone(vacancy=new_vacancy, name="initiate", detail="install things",
+                                                     expected_start=datetime.fromtimestamp(86400),
+                                                     expected_finish=datetime.fromtimestamp(0))
+        with self.assertRaises(ValidationError, msg="Expected finish earlier than tart should raise ValidationError"):
+            milestone3.full_clean()
+
     def test_vacancy_milestones_list(self):
         company_user = User.objects.create_user('dummy.company2', 'dummy.compan2y@company.com', 'lalala123')
         new_company = Company.objects.create(user=company_user, description="lalala", status=Company.VERIFIED, logo=None,
-- 
GitLab


From a180f6efbd2fa2b0547d4036aab218faa923e483 Mon Sep 17 00:00:00 2001
From: Ichlasul Affan <ichlaffterlalu@gmail.com>
Date: Sun, 6 Oct 2019 10:09:53 +0700
Subject: [PATCH 03/14] Fix test mistakes and implement vacancy milestone model

---
 core/migrations/0014_vacancymilestone.py | 27 ++++++++++++++++++++++++
 core/models/vacancies.py                 | 13 ++++++++++--
 core/tests/test_vacancies.py             |  3 +++
 3 files changed, 41 insertions(+), 2 deletions(-)
 create mode 100644 core/migrations/0014_vacancymilestone.py

diff --git a/core/migrations/0014_vacancymilestone.py b/core/migrations/0014_vacancymilestone.py
new file mode 100644
index 00000000..7faa5bd0
--- /dev/null
+++ b/core/migrations/0014_vacancymilestone.py
@@ -0,0 +1,27 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.17 on 2019-10-06 03:07
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('core', '0013_auto_20170602_1130'),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='VacancyMilestone',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('name', models.CharField(max_length=100)),
+                ('detail', models.TextField()),
+                ('expected_start', models.DateField()),
+                ('expected_finish', models.DateField()),
+                ('vacancy', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.Vacancy')),
+            ],
+        ),
+    ]
diff --git a/core/models/vacancies.py b/core/models/vacancies.py
index 9fd1bb0d..ebec5763 100644
--- a/core/models/vacancies.py
+++ b/core/models/vacancies.py
@@ -1,5 +1,5 @@
 from django.db import models
-
+from django.core.exceptions import ValidationError
 from core.models.accounts import Company, Student
 
 
@@ -34,4 +34,13 @@ class Application(models.Model):
 
 
 class VacancyMilestone(models.Model):
-    pass
+    vacancy = models.ForeignKey(Vacancy, on_delete=models.CASCADE)
+    name = models.CharField(max_length=100, null=False)
+    detail = models.TextField()
+    expected_start = models.DateField()
+    expected_finish = models.DateField()
+
+    def clean(self):
+        super(VacancyMilestone, self).clean()
+        if self.expected_start >= self.expected_finish:
+            raise ValidationError("Expected start must be earlier than expected finish/")
diff --git a/core/tests/test_vacancies.py b/core/tests/test_vacancies.py
index f164a18a..4e4262d5 100644
--- a/core/tests/test_vacancies.py
+++ b/core/tests/test_vacancies.py
@@ -438,6 +438,9 @@ class SupervisorApprovalTests(APITestCase):
 
 class VacancyMilestoneTests(APITestCase):
     def test_vacancy_milestone_model(self):
+        company_user = User.objects.create_user('dummy.company2', 'dummy.compan2y@company.com', 'lalala123')
+        new_company = Company.objects.create(user=company_user, description="lalala", status=Company.VERIFIED, logo=None,
+                                              address=None)
         new_vacancy = Vacancy.objects.create(company=new_company, verified=True, open_time=datetime.fromtimestamp(0),
                                               description="lalala", close_time=datetime.today())
         milestone1 = VacancyMilestone(vacancy=new_vacancy, name="initiate", detail="install things",
-- 
GitLab


From 17f7f153172c603462bb8be59eb856639fd2a3a7 Mon Sep 17 00:00:00 2001
From: Ichlasul Affan <ichlaffterlalu@gmail.com>
Date: Sun, 6 Oct 2019 10:44:44 +0700
Subject: [PATCH 04/14] Fix test authentication and JSON convention mistake

---
 core/tests/test_vacancies.py | 8 +++++---
 1 file changed, 5 insertions(+), 3 deletions(-)

diff --git a/core/tests/test_vacancies.py b/core/tests/test_vacancies.py
index 4e4262d5..50f7efdf 100644
--- a/core/tests/test_vacancies.py
+++ b/core/tests/test_vacancies.py
@@ -461,6 +461,7 @@ class VacancyMilestoneTests(APITestCase):
             milestone3.full_clean()
 
     def test_vacancy_milestones_list(self):
+        user = User.objects.create_user('dummy.student', 'dummy.student@home.com', 'lalala123')
         company_user = User.objects.create_user('dummy.company2', 'dummy.compan2y@company.com', 'lalala123')
         new_company = Company.objects.create(user=company_user, description="lalala", status=Company.VERIFIED, logo=None,
                                               address=None)
@@ -469,13 +470,14 @@ class VacancyMilestoneTests(APITestCase):
         milestone1 = VacancyMilestone.objects.create(vacancy=new_vacancy, name="initiate", detail="install things",
                                                      expected_start=datetime.fromtimestamp(0),
                                                      expected_finish=datetime.fromtimestamp(86400))
+        self.client.force_authenticate(user=user)
 
         url = '/api/vacancies/' + str(new_vacancy.pk) + '/milestones/'
         response = self.client.get(url, format='json')
         self.assertEqual(response.status_code, status.HTTP_200_OK)
-        self.assertEqual(len(response.data), 1)
-        self.assertEqual(response.data[0]["name"], milestone1.name)
-        self.assertEqual(response.data[0]["detail"], milestone1.detail)
+        self.assertEqual(len(response.data["results"]), 1)
+        self.assertEqual(response.data["results"][0]["name"], milestone1.name)
+        self.assertEqual(response.data["results"][0]["detail"], milestone1.detail)
 
 
     def test_create_new_milestone_on_a_vacancy_success(self):
-- 
GitLab


From 18434f53c225602ee645666bdcd985d94e4f2f44 Mon Sep 17 00:00:00 2001
From: Ichlasul Affan <ichlaffterlalu@gmail.com>
Date: Sun, 6 Oct 2019 10:45:48 +0700
Subject: [PATCH 05/14] Update model, and create new serializer and viewset

---
 core/migrations/0014_vacancymilestone.py |  4 ++--
 core/models/vacancies.py                 |  2 +-
 core/serializers/vacancies.py            |  8 +++++++-
 core/views/vacancies.py                  | 21 ++++++++++++++++++---
 4 files changed, 28 insertions(+), 7 deletions(-)

diff --git a/core/migrations/0014_vacancymilestone.py b/core/migrations/0014_vacancymilestone.py
index 7faa5bd0..4ac5eba7 100644
--- a/core/migrations/0014_vacancymilestone.py
+++ b/core/migrations/0014_vacancymilestone.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Generated by Django 1.11.17 on 2019-10-06 03:07
+# Generated by Django 1.11.17 on 2019-10-06 03:23
 from __future__ import unicode_literals
 
 from django.db import migrations, models
@@ -21,7 +21,7 @@ class Migration(migrations.Migration):
                 ('detail', models.TextField()),
                 ('expected_start', models.DateField()),
                 ('expected_finish', models.DateField()),
-                ('vacancy', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.Vacancy')),
+                ('vacancy', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='milestones', to='core.Vacancy')),
             ],
         ),
     ]
diff --git a/core/models/vacancies.py b/core/models/vacancies.py
index ebec5763..07591b58 100644
--- a/core/models/vacancies.py
+++ b/core/models/vacancies.py
@@ -34,7 +34,7 @@ class Application(models.Model):
 
 
 class VacancyMilestone(models.Model):
-    vacancy = models.ForeignKey(Vacancy, on_delete=models.CASCADE)
+    vacancy = models.ForeignKey(Vacancy, on_delete=models.CASCADE, related_name="milestones", null=False)
     name = models.CharField(max_length=100, null=False)
     detail = models.TextField()
     expected_start = models.DateField()
diff --git a/core/serializers/vacancies.py b/core/serializers/vacancies.py
index 313f2064..60300b50 100644
--- a/core/serializers/vacancies.py
+++ b/core/serializers/vacancies.py
@@ -1,7 +1,7 @@
 from rest_framework import serializers
 
 from core.models import Company
-from core.models.vacancies import Vacancy, Application
+from core.models.vacancies import Vacancy, Application, VacancyMilestone
 from core.serializers.accounts import StudentSerializer, CompanySerializer
 
 
@@ -88,3 +88,9 @@ class VacancyVerifiedSerializer(serializers.ModelSerializer):
     class Meta:
         model = Vacancy
         fields = ['verified']
+
+
+class VacancyMilestoneSerializer(serializers.ModelSerializer):
+    class Meta:
+        model = VacancyMilestone
+        fields = ['name', 'detail', 'expected_start', 'expected_finish']
diff --git a/core/views/vacancies.py b/core/views/vacancies.py
index 19b13fc8..e37d45d3 100644
--- a/core/views/vacancies.py
+++ b/core/views/vacancies.py
@@ -1,7 +1,7 @@
 import requests
 from django.conf import settings
 from rest_framework import viewsets, status
-from rest_framework.decorators import detail_route
+from rest_framework.decorators import detail_route, permission_classes
 from rest_framework.exceptions import ValidationError
 from rest_framework.generics import get_object_or_404
 from rest_framework.pagination import PageNumberPagination
@@ -14,7 +14,8 @@ from core.lib.permissions import IsAdminOrStudent, IsAdminOrCompany, IsAdminOrVa
 from core.models import Student, Company
 from core.models.vacancies import Vacancy, Application, VacancyMilestone
 from core.serializers.vacancies import VacancySerializer, ApplicationSerializer, ApplicationStatusSerializer, \
-    PostVacancySerializer, VacancyVerifiedSerializer, SupervisorStudentApplicationSerializer
+    PostVacancySerializer, VacancyVerifiedSerializer, SupervisorStudentApplicationSerializer, \
+    VacancyMilestoneSerializer
 from core.views.accounts import StudentViewSet
 
 
@@ -278,4 +279,18 @@ class BookmarkedVacancyByStudentViewSet(viewsets.GenericViewSet):
 
 
 class VacancyMilestoneViewSet(viewsets.GenericViewSet):
-    pass
+    serializer_class = VacancyMilestoneSerializer
+    permission_classes = [IsAuthenticated]
+
+    def list(self, request, vacancy_id):
+        """
+        Get list of a vacancy {vacancy_id}'s milestone plans
+        ---
+        """
+        vacancy = get_object_or_404(Vacancy.objects.all(), pk=vacancy_id)
+        milestones = vacancy.milestones.all()
+        page = self.paginate_queryset(milestones)
+        if page is not None:
+            return self.get_paginated_response(
+                VacancyMilestoneSerializer(page, many=True, context={'request': request}).data)
+        return Response(VacancyMilestoneSerializer(milestones, many=True, context={'request': request}).data)
-- 
GitLab


From 7feee1e8d5f34d322edcfb3be0d313953b22cc6f Mon Sep 17 00:00:00 2001
From: Ichlasul Affan <ichlaffterlalu@gmail.com>
Date: Sun, 6 Oct 2019 11:09:05 +0700
Subject: [PATCH 06/14] Refactor permission for vacancy milestone

---
 core/lib/permissions.py | 15 +++++++++++++++
 core/models/__init__.py |  5 +----
 core/views/vacancies.py |  4 ++--
 3 files changed, 18 insertions(+), 6 deletions(-)

diff --git a/core/lib/permissions.py b/core/lib/permissions.py
index f3ad630e..79122c0f 100644
--- a/core/lib/permissions.py
+++ b/core/lib/permissions.py
@@ -6,6 +6,7 @@ from core.models import Student
 from core.models import Supervisor
 from core.models import Application
 from core.models import Vacancy
+from core.models import VacancyMilestone
 
 
 def is_admin_or_student(user):
@@ -159,3 +160,17 @@ class VacancyApprovalPermission(permissions.BasePermission):
 
     def has_object_permission(self, request, view, obj):
         return isinstance(obj, Vacancy)
+
+class IsAdminOrVacancyOwnerOrAuthenticatedReadOnly(permissions.IsAuthenticated):
+    def has_object_permission(self, request, view, obj):
+        user = request.user
+        if request.user and request.method in permissions.SAFE_METHODS:
+            return True
+        if request.user.is_superuser or request.user.is_staff:
+            return True
+        if isinstance(obj, VacancyMilestone):
+            return user.company == obj.vacancy.company
+        else:
+            raise PermissionDenied(
+                "Checking owner permission on non-milestone object"
+            )
diff --git a/core/models/__init__.py b/core/models/__init__.py
index 8ca55685..c5561b1e 100755
--- a/core/models/__init__.py
+++ b/core/models/__init__.py
@@ -4,7 +4,4 @@ from core.models.accounts import Student
 from core.models.accounts import Supervisor
 from core.models.vacancies import Vacancy
 from core.models.vacancies import Application
-
-
-
-
+from core.models.vacancies import VacancyMilestone
diff --git a/core/views/vacancies.py b/core/views/vacancies.py
index e37d45d3..c5596da7 100644
--- a/core/views/vacancies.py
+++ b/core/views/vacancies.py
@@ -10,7 +10,7 @@ from rest_framework.response import Response
 
 from core.lib.mixins import MultiSerializerViewSetMixin
 from core.lib.permissions import IsAdminOrStudent, IsAdminOrCompany, IsAdminOrVacancyOwner, AsAdminOrSupervisor, \
-    VacancyApprovalPermission
+    VacancyApprovalPermission, IsAdminOrVacancyOwnerOrAuthenticatedReadOnly
 from core.models import Student, Company
 from core.models.vacancies import Vacancy, Application, VacancyMilestone
 from core.serializers.vacancies import VacancySerializer, ApplicationSerializer, ApplicationStatusSerializer, \
@@ -280,7 +280,7 @@ class BookmarkedVacancyByStudentViewSet(viewsets.GenericViewSet):
 
 class VacancyMilestoneViewSet(viewsets.GenericViewSet):
     serializer_class = VacancyMilestoneSerializer
-    permission_classes = [IsAuthenticated]
+    permission_classes = [IsAdminOrVacancyOwnerOrAuthenticatedReadOnly]
 
     def list(self, request, vacancy_id):
         """
-- 
GitLab


From 8a00e46c57aacf9fcc499b8cf698f21f65445d4e Mon Sep 17 00:00:00 2001
From: Ichlasul Affan <ichlaffterlalu@gmail.com>
Date: Sun, 6 Oct 2019 12:31:55 +0700
Subject: [PATCH 07/14] Add tests on create vacancy milestones

---
 core/tests/test_vacancies.py | 61 ++++++++++++++++++++++--------------
 1 file changed, 37 insertions(+), 24 deletions(-)

diff --git a/core/tests/test_vacancies.py b/core/tests/test_vacancies.py
index 50f7efdf..6c3d736b 100644
--- a/core/tests/test_vacancies.py
+++ b/core/tests/test_vacancies.py
@@ -437,42 +437,40 @@ class SupervisorApprovalTests(APITestCase):
 
 
 class VacancyMilestoneTests(APITestCase):
-    def test_vacancy_milestone_model(self):
-        company_user = User.objects.create_user('dummy.company2', 'dummy.compan2y@company.com', 'lalala123')
-        new_company = Company.objects.create(user=company_user, description="lalala", status=Company.VERIFIED, logo=None,
+    def setUp(self):
+        super(VacancyMilestoneTests, self).setUp()
+        self.user = User.objects.create_user('dummy.student', 'dummy.student@home.com', 'lalala123')
+        self.company_user =  User.objects.create_user('dummy.company2', 'dummy.compan2y@company.com', 'lalala123')
+        self.company = Company.objects.create(user=self.company_user, description="lalala", status=Company.VERIFIED, logo=None,
                                               address=None)
-        new_vacancy = Vacancy.objects.create(company=new_company, verified=True, open_time=datetime.fromtimestamp(0),
+        self.vacancy = Vacancy.objects.create(company=self.company, verified=True, open_time=datetime.fromtimestamp(0),
                                               description="lalala", close_time=datetime.today())
-        milestone1 = VacancyMilestone(vacancy=new_vacancy, name="initiate", detail="install things",
-                                                     expected_start=datetime.fromtimestamp(0),
-                                                     expected_finish=datetime.fromtimestamp(86400))
+
+    def test_vacancy_milestone_model(self):
+        milestone1 = VacancyMilestone(vacancy=self.vacancy, name="initiate", detail="install things",
+                                      expected_start=datetime.fromtimestamp(0),
+                                      expected_finish=datetime.fromtimestamp(86400))
         milestone1.full_clean()
 
-        milestone2 = VacancyMilestone(vacancy=new_vacancy, name="a"*101, detail="install things",
-                                                     expected_start=datetime.fromtimestamp(0),
-                                                     expected_finish=datetime.fromtimestamp(86400))
+        milestone2 = VacancyMilestone(vacancy=self.vacancy, name="a"*101, detail="install things",
+                                      expected_start=datetime.fromtimestamp(0),
+                                      expected_finish=datetime.fromtimestamp(86400))
         with self.assertRaises(ValidationError, msg="Name with more than 100 character should raise ValidationError"):
             milestone2.full_clean()
 
-        milestone3 = VacancyMilestone(vacancy=new_vacancy, name="initiate", detail="install things",
-                                                     expected_start=datetime.fromtimestamp(86400),
-                                                     expected_finish=datetime.fromtimestamp(0))
+        milestone3 = VacancyMilestone(vacancy=self.vacancy, name="initiate", detail="install things",
+                                      expected_start=datetime.fromtimestamp(86400),
+                                      expected_finish=datetime.fromtimestamp(0))
         with self.assertRaises(ValidationError, msg="Expected finish earlier than tart should raise ValidationError"):
             milestone3.full_clean()
 
     def test_vacancy_milestones_list(self):
-        user = User.objects.create_user('dummy.student', 'dummy.student@home.com', 'lalala123')
-        company_user = User.objects.create_user('dummy.company2', 'dummy.compan2y@company.com', 'lalala123')
-        new_company = Company.objects.create(user=company_user, description="lalala", status=Company.VERIFIED, logo=None,
-                                              address=None)
-        new_vacancy = Vacancy.objects.create(company=new_company, verified=True, open_time=datetime.fromtimestamp(0),
-                                              description="lalala", close_time=datetime.today())
-        milestone1 = VacancyMilestone.objects.create(vacancy=new_vacancy, name="initiate", detail="install things",
+        milestone1 = VacancyMilestone.objects.create(vacancy=self.vacancy, name="initiate", detail="install things",
                                                      expected_start=datetime.fromtimestamp(0),
                                                      expected_finish=datetime.fromtimestamp(86400))
-        self.client.force_authenticate(user=user)
+        self.client.force_authenticate(user=self.user)
 
-        url = '/api/vacancies/' + str(new_vacancy.pk) + '/milestones/'
+        url = '/api/vacancies/' + str(self.vacancy.pk) + '/milestones/'
         response = self.client.get(url, format='json')
         self.assertEqual(response.status_code, status.HTTP_200_OK)
         self.assertEqual(len(response.data["results"]), 1)
@@ -481,10 +479,25 @@ class VacancyMilestoneTests(APITestCase):
 
 
     def test_create_new_milestone_on_a_vacancy_success(self):
-        pass
+        self.client.force_authenticate(user=self.company_user)
+        url = '/api/vacancies/' + str(self.vacancy.pk) + '/milestones/'
+        data = {"name": "initiate", "detail": "install things", "expected_start": "2019-01-20",
+                "expected_finish": "2019-01-21"}
+        response = self.client.post(url, data, format='json')
+        self.assertEqual(response.status_code, status.HTTP_200_OK)
+        self.assertEqual(self.vacancy.milestones.count(), 1)
+        new_milestone = self.vacancy.milestones.first()
+        self.assertEqual(new_milestone.name, data["name"])
+        self.assertEqual(new_milestone.detail, data["detail"])
 
     def test_create_new_milestone_on_a_vacancy_unauthorized_user(self):
-        pass
+        self.client.force_authenticate(user=self.user)
+        url = '/api/vacancies/' + str(self.vacancy.pk) + '/milestones/'
+        data = {"name": "initiate", "detail": "install things", "expected_start": "2019-01-20",
+                "expected_finish": "2019-01-21"}
+        response = self.client.post(url, data, format='json')
+        self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
+        self.assertEqual(self.vacancy.milestones.count(), 0)
 
     def test_modify_milestone_on_a_vacancy_success(self):
         pass
-- 
GitLab


From 86f2cf33d5243f0e97c9eecb37f4fe3f34bdf9ed Mon Sep 17 00:00:00 2001
From: Ichlasul Affan <ichlaffterlalu@gmail.com>
Date: Sun, 6 Oct 2019 12:42:46 +0700
Subject: [PATCH 08/14] Add new test on invalid create input

---
 core/tests/test_vacancies.py | 13 ++++++++++++-
 1 file changed, 12 insertions(+), 1 deletion(-)

diff --git a/core/tests/test_vacancies.py b/core/tests/test_vacancies.py
index 6c3d736b..fbccc316 100644
--- a/core/tests/test_vacancies.py
+++ b/core/tests/test_vacancies.py
@@ -477,7 +477,6 @@ class VacancyMilestoneTests(APITestCase):
         self.assertEqual(response.data["results"][0]["name"], milestone1.name)
         self.assertEqual(response.data["results"][0]["detail"], milestone1.detail)
 
-
     def test_create_new_milestone_on_a_vacancy_success(self):
         self.client.force_authenticate(user=self.company_user)
         url = '/api/vacancies/' + str(self.vacancy.pk) + '/milestones/'
@@ -490,6 +489,15 @@ class VacancyMilestoneTests(APITestCase):
         self.assertEqual(new_milestone.name, data["name"])
         self.assertEqual(new_milestone.detail, data["detail"])
 
+    def test_create_new_milestone_on_a_vacancy_invalid_data(self):
+        self.client.force_authenticate(user=self.company_user)
+        url = '/api/vacancies/' + str(self.vacancy.pk) + '/milestones/'
+        data = {"name": "initiate", "detail": "install things", "expected_start": "2019-01-20",
+                "expected_finish": "2019-01-01"}
+        response = self.client.post(url, data, format='json')
+        self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+        self.assertEqual(self.vacancy.milestones.count(), 0)
+
     def test_create_new_milestone_on_a_vacancy_unauthorized_user(self):
         self.client.force_authenticate(user=self.user)
         url = '/api/vacancies/' + str(self.vacancy.pk) + '/milestones/'
@@ -502,6 +510,9 @@ class VacancyMilestoneTests(APITestCase):
     def test_modify_milestone_on_a_vacancy_success(self):
         pass
 
+    def test_modify_milestone_on_a_vacancy_invalid_data(self):
+        pass
+
     def test_modify_milestone_on_a_vacancy_unauthorized_user(self):
         pass
 
-- 
GitLab


From 0b7e0e07d998faf3afca8b5ee6086afa92c26bb6 Mon Sep 17 00:00:00 2001
From: Ichlasul Affan <ichlaffterlalu@gmail.com>
Date: Sun, 6 Oct 2019 13:02:19 +0700
Subject: [PATCH 09/14] Implement create new milestones

Also updates permission and serializer
---
 core/lib/permissions.py       | 17 +++++++++++------
 core/models/vacancies.py      |  2 +-
 core/serializers/vacancies.py |  5 +++++
 core/views/vacancies.py       | 21 +++++++++++++++++++++
 4 files changed, 38 insertions(+), 7 deletions(-)

diff --git a/core/lib/permissions.py b/core/lib/permissions.py
index 79122c0f..a4f7b829 100644
--- a/core/lib/permissions.py
+++ b/core/lib/permissions.py
@@ -162,15 +162,20 @@ class VacancyApprovalPermission(permissions.BasePermission):
         return isinstance(obj, Vacancy)
 
 class IsAdminOrVacancyOwnerOrAuthenticatedReadOnly(permissions.IsAuthenticated):
+    def has_permission(self, request, view):
+        is_authenticated = super(IsAdminOrVacancyOwnerOrAuthenticatedReadOnly, self).has_permission(request, view)
+        if is_authenticated and request.method in permissions.SAFE_METHODS:
+            return True
+        return is_admin_or_company(request.user)
+
     def has_object_permission(self, request, view, obj):
         user = request.user
-        if request.user and request.method in permissions.SAFE_METHODS:
+        if user and request.method in permissions.SAFE_METHODS:
             return True
-        if request.user.is_superuser or request.user.is_staff:
+        if user.is_superuser or user.is_staff:
             return True
         if isinstance(obj, VacancyMilestone):
             return user.company == obj.vacancy.company
-        else:
-            raise PermissionDenied(
-                "Checking owner permission on non-milestone object"
-            )
+        raise PermissionDenied(
+            "Checking owner permission on non-milestone object"
+        )
diff --git a/core/models/vacancies.py b/core/models/vacancies.py
index 07591b58..0aaa8ef3 100644
--- a/core/models/vacancies.py
+++ b/core/models/vacancies.py
@@ -43,4 +43,4 @@ class VacancyMilestone(models.Model):
     def clean(self):
         super(VacancyMilestone, self).clean()
         if self.expected_start >= self.expected_finish:
-            raise ValidationError("Expected start must be earlier than expected finish/")
+            raise ValidationError("Expected start must be earlier than expected finish.")
diff --git a/core/serializers/vacancies.py b/core/serializers/vacancies.py
index 60300b50..1093e28e 100644
--- a/core/serializers/vacancies.py
+++ b/core/serializers/vacancies.py
@@ -91,6 +91,11 @@ class VacancyVerifiedSerializer(serializers.ModelSerializer):
 
 
 class VacancyMilestoneSerializer(serializers.ModelSerializer):
+    def validate(self, data):
+        if data['expected_start'] > data['expected_finish']:
+            raise serializers.ValidationError("Expected start must be earlier than expected finish.")
+        return data
+
     class Meta:
         model = VacancyMilestone
         fields = ['name', 'detail', 'expected_start', 'expected_finish']
diff --git a/core/views/vacancies.py b/core/views/vacancies.py
index c5596da7..c44090fd 100644
--- a/core/views/vacancies.py
+++ b/core/views/vacancies.py
@@ -294,3 +294,24 @@ class VacancyMilestoneViewSet(viewsets.GenericViewSet):
             return self.get_paginated_response(
                 VacancyMilestoneSerializer(page, many=True, context={'request': request}).data)
         return Response(VacancyMilestoneSerializer(milestones, many=True, context={'request': request}).data)
+
+    def create(self, request, vacancy_id):
+        """
+        Create a new milestone for vacancy {vacancy_id}
+        ---
+        parameters:
+            - name: body
+              description: JSON object containing 'name' string, 'detail' string, 'expected_start' date string, and
+                           'expected_finish' date string
+              required: true
+              type: string
+              paramType: body
+        """
+        vacancy = get_object_or_404(Vacancy.objects.all(), pk=vacancy_id)
+        milestone_serializer = VacancyMilestoneSerializer(data=request.data)
+        if milestone_serializer.is_valid():
+            milestone = milestone_serializer.save(vacancy=vacancy)
+            return Response(VacancyMilestoneSerializer(milestone, context={'request': request}).data,
+                            status=status.HTTP_200_OK)
+        print(milestone_serializer.errors)
+        return Response(milestone_serializer.errors, status=status.HTTP_400_BAD_REQUEST)
-- 
GitLab


From 31482b05c87fd0404e2694d17f68c13a06819ef7 Mon Sep 17 00:00:00 2001
From: Ichlasul Affan <ichlaffterlalu@gmail.com>
Date: Sun, 6 Oct 2019 13:49:33 +0700
Subject: [PATCH 10/14] Add tests on milestone modify

---
 core/tests/test_vacancies.py | 42 +++++++++++++++++++++++++++++++++---
 1 file changed, 39 insertions(+), 3 deletions(-)

diff --git a/core/tests/test_vacancies.py b/core/tests/test_vacancies.py
index fbccc316..060d0b36 100644
--- a/core/tests/test_vacancies.py
+++ b/core/tests/test_vacancies.py
@@ -508,13 +508,49 @@ class VacancyMilestoneTests(APITestCase):
         self.assertEqual(self.vacancy.milestones.count(), 0)
 
     def test_modify_milestone_on_a_vacancy_success(self):
-        pass
+        milestone = VacancyMilestone.objects.create(vacancy=self.vacancy, name="initiate", detail="install things",
+                                                    expected_start=datetime.fromtimestamp(0),
+                                                    expected_finish=datetime.fromtimestamp(86400))
+        self.client.force_authenticate(user=self.company_user)
+        url = '/api/vacancies/' + str(self.vacancy.pk) + '/milestones/' + str(milestone.pk) + '/'
+        data = {"name": "initiate env", "detail": "install all things", "expected_start": "2019-01-20",
+                "expected_finish": "2019-01-21"}
+        response = self.client.put(url, data, format='json')
+        self.assertEqual(response.status_code, status.HTTP_200_OK)
+        self.assertEqual(self.vacancy.milestones.count(), 1)
+        new_milestone = self.vacancy.milestones.first()
+        self.assertEqual(new_milestone.name, data["name"])
+        self.assertEqual(new_milestone.detail, data["detail"])
 
     def test_modify_milestone_on_a_vacancy_invalid_data(self):
-        pass
+        milestone = VacancyMilestone.objects.create(vacancy=self.vacancy, name="initiate", detail="install things",
+                                                    expected_start=datetime.fromtimestamp(0),
+                                                    expected_finish=datetime.fromtimestamp(86400))
+        self.client.force_authenticate(user=self.company_user)
+        url = '/api/vacancies/' + str(self.vacancy.pk) + '/milestones/' + str(milestone.pk) + '/'
+        data = {"name": "a" * 101, "detail": "install all things", "expected_start": "2019-01-21",
+                "expected_finish": "2019-01-19"}
+        response = self.client.put(url, data, format='json')
+        self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+        self.assertEqual(self.vacancy.milestones.count(), 1)
+        new_milestone = self.vacancy.milestones.first()
+        self.assertEqual(new_milestone.name, milestone.name)
+        self.assertEqual(new_milestone.detail, milestone.detail)
 
     def test_modify_milestone_on_a_vacancy_unauthorized_user(self):
-        pass
+        milestone = VacancyMilestone.objects.create(vacancy=self.vacancy, name="initiate", detail="install things",
+                                                    expected_start=datetime.fromtimestamp(0),
+                                                    expected_finish=datetime.fromtimestamp(86400))
+        self.client.force_authenticate(user=self.user)
+        url = '/api/vacancies/' + str(self.vacancy.pk) + '/milestones/' + str(milestone.pk) + '/'
+        data = {"name": "initiate env", "detail": "install all things", "expected_start": "2019-01-20",
+                "expected_finish": "2019-01-21"}
+        response = self.client.put(url, data, format='json')
+        self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
+        self.assertEqual(self.vacancy.milestones.count(), 1)
+        new_milestone = self.vacancy.milestones.first()
+        self.assertEqual(new_milestone.name, milestone.name)
+        self.assertEqual(new_milestone.detail, milestone.detail)
 
     def test_delete_milestone_on_a_vacancy_success(self):
         pass
-- 
GitLab


From 4fd44541f035cfc5218c9bb29491b963dfa2b3b5 Mon Sep 17 00:00:00 2001
From: Ichlasul Affan <ichlaffterlalu@gmail.com>
Date: Sun, 6 Oct 2019 14:06:18 +0700
Subject: [PATCH 11/14] Implement update milestone viewset method

---
 core/views/vacancies.py | 22 +++++++++++++++++++++-
 1 file changed, 21 insertions(+), 1 deletion(-)

diff --git a/core/views/vacancies.py b/core/views/vacancies.py
index c44090fd..0c3a688c 100644
--- a/core/views/vacancies.py
+++ b/core/views/vacancies.py
@@ -313,5 +313,25 @@ class VacancyMilestoneViewSet(viewsets.GenericViewSet):
             milestone = milestone_serializer.save(vacancy=vacancy)
             return Response(VacancyMilestoneSerializer(milestone, context={'request': request}).data,
                             status=status.HTTP_200_OK)
-        print(milestone_serializer.errors)
+        return Response(milestone_serializer.errors, status=status.HTTP_400_BAD_REQUEST)
+
+    def update(self, request, vacancy_id, pk):
+        """
+        Create existing milestone {pk} for vacancy {vacancy_id}
+        ---
+        parameters:
+            - name: body
+              description: JSON object containing 'name' string, 'detail' string, 'expected_start' date string, and
+                           'expected_finish' date string
+              required: true
+              type: string
+              paramType: body
+        """
+        vacancy = get_object_or_404(Vacancy.objects.all(), pk=vacancy_id)
+        old_milestone = get_object_or_404(vacancy.milestones.all(), pk=pk)
+        milestone_serializer = VacancyMilestoneSerializer(old_milestone, data=request.data)
+        if milestone_serializer.is_valid():
+            milestone = milestone_serializer.save(vacancy=vacancy)
+            return Response(VacancyMilestoneSerializer(milestone, context={'request': request}).data,
+                            status=status.HTTP_200_OK)
         return Response(milestone_serializer.errors, status=status.HTTP_400_BAD_REQUEST)
-- 
GitLab


From dca24ed5d69c28919225bbd53d82a37ebd6acaa1 Mon Sep 17 00:00:00 2001
From: Ichlasul Affan <ichlaffterlalu@gmail.com>
Date: Sun, 6 Oct 2019 14:32:57 +0700
Subject: [PATCH 12/14] Add tests on delete milestones

---
 core/tests/test_vacancies.py | 18 ++++++++++++++++--
 1 file changed, 16 insertions(+), 2 deletions(-)

diff --git a/core/tests/test_vacancies.py b/core/tests/test_vacancies.py
index 060d0b36..105b9fe7 100644
--- a/core/tests/test_vacancies.py
+++ b/core/tests/test_vacancies.py
@@ -553,7 +553,21 @@ class VacancyMilestoneTests(APITestCase):
         self.assertEqual(new_milestone.detail, milestone.detail)
 
     def test_delete_milestone_on_a_vacancy_success(self):
-        pass
+        milestone = VacancyMilestone.objects.create(vacancy=self.vacancy, name="initiate", detail="install things",
+                                                    expected_start=datetime.fromtimestamp(0),
+                                                    expected_finish=datetime.fromtimestamp(86400))
+        self.client.force_authenticate(user=self.company_user)
+        url = '/api/vacancies/' + str(self.vacancy.pk) + '/milestones/' + str(milestone.pk) + '/'
+        response = self.client.delete(url, format='json')
+        self.assertEqual(response.status_code, status.HTTP_200_OK)
+        self.assertEqual(self.vacancy.milestones.count(), 0)
 
     def test_delete_milestone_on_a_vacancy_unauthorized_user(self):
-        pass
+        milestone = VacancyMilestone.objects.create(vacancy=self.vacancy, name="initiate", detail="install things",
+                                                    expected_start=datetime.fromtimestamp(0),
+                                                    expected_finish=datetime.fromtimestamp(86400))
+        self.client.force_authenticate(user=self.user)
+        url = '/api/vacancies/' + str(self.vacancy.pk) + '/milestones/' + str(milestone.pk) + '/'
+        response = self.client.delete(url, format='json')
+        self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
+        self.assertEqual(self.vacancy.milestones.count(), 1)
-- 
GitLab


From 5bff1f1708738ed6dbd9e9fa3ef4e36f4b57deba Mon Sep 17 00:00:00 2001
From: Ichlasul Affan <ichlaffterlalu@gmail.com>
Date: Sun, 6 Oct 2019 14:54:17 +0700
Subject: [PATCH 13/14] Refactor test and add tests for "object not found"

---
 core/tests/test_vacancies.py | 56 ++++++++++++++++++++++++------------
 1 file changed, 38 insertions(+), 18 deletions(-)

diff --git a/core/tests/test_vacancies.py b/core/tests/test_vacancies.py
index 105b9fe7..e5552b17 100644
--- a/core/tests/test_vacancies.py
+++ b/core/tests/test_vacancies.py
@@ -446,6 +446,11 @@ class VacancyMilestoneTests(APITestCase):
         self.vacancy = Vacancy.objects.create(company=self.company, verified=True, open_time=datetime.fromtimestamp(0),
                                               description="lalala", close_time=datetime.today())
 
+    def create_milestone_object(self):
+        return VacancyMilestone.objects.create(vacancy=self.vacancy, name="initiate", detail="install things",
+                                               expected_start=datetime.fromtimestamp(0),
+                                               expected_finish=datetime.fromtimestamp(86400))
+
     def test_vacancy_milestone_model(self):
         milestone1 = VacancyMilestone(vacancy=self.vacancy, name="initiate", detail="install things",
                                       expected_start=datetime.fromtimestamp(0),
@@ -465,9 +470,7 @@ class VacancyMilestoneTests(APITestCase):
             milestone3.full_clean()
 
     def test_vacancy_milestones_list(self):
-        milestone1 = VacancyMilestone.objects.create(vacancy=self.vacancy, name="initiate", detail="install things",
-                                                     expected_start=datetime.fromtimestamp(0),
-                                                     expected_finish=datetime.fromtimestamp(86400))
+        milestone1 = self.create_milestone_object()
         self.client.force_authenticate(user=self.user)
 
         url = '/api/vacancies/' + str(self.vacancy.pk) + '/milestones/'
@@ -508,9 +511,7 @@ class VacancyMilestoneTests(APITestCase):
         self.assertEqual(self.vacancy.milestones.count(), 0)
 
     def test_modify_milestone_on_a_vacancy_success(self):
-        milestone = VacancyMilestone.objects.create(vacancy=self.vacancy, name="initiate", detail="install things",
-                                                    expected_start=datetime.fromtimestamp(0),
-                                                    expected_finish=datetime.fromtimestamp(86400))
+        milestone = self.create_milestone_object()
         self.client.force_authenticate(user=self.company_user)
         url = '/api/vacancies/' + str(self.vacancy.pk) + '/milestones/' + str(milestone.pk) + '/'
         data = {"name": "initiate env", "detail": "install all things", "expected_start": "2019-01-20",
@@ -522,10 +523,24 @@ class VacancyMilestoneTests(APITestCase):
         self.assertEqual(new_milestone.name, data["name"])
         self.assertEqual(new_milestone.detail, data["detail"])
 
+    def test_modify_milestone_on_a_vacancy_not_found(self):
+        milestone = self.create_milestone_object()
+        self.client.force_authenticate(user=self.company_user)
+        url = '/api/vacancies/1000/milestones/' + str(milestone.pk) + '/'
+        data = {"name": "initiate env", "detail": "install all things", "expected_start": "2019-01-20",
+                "expected_finish": "2019-01-21"}
+        response = self.client.put(url, data, format='json')
+        self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
+        url2 = '/api/vacancies/' + str(self.vacancy.pk) + '/milestones/1000/'
+        response2 = self.client.put(url2, data, format='json')
+        self.assertEqual(response2.status_code, status.HTTP_404_NOT_FOUND)
+        self.assertEqual(self.vacancy.milestones.count(), 1)
+        new_milestone = self.vacancy.milestones.first()
+        self.assertEqual(new_milestone.name, milestone.name)
+        self.assertEqual(new_milestone.detail, milestone.detail)
+
     def test_modify_milestone_on_a_vacancy_invalid_data(self):
-        milestone = VacancyMilestone.objects.create(vacancy=self.vacancy, name="initiate", detail="install things",
-                                                    expected_start=datetime.fromtimestamp(0),
-                                                    expected_finish=datetime.fromtimestamp(86400))
+        milestone = self.create_milestone_object()
         self.client.force_authenticate(user=self.company_user)
         url = '/api/vacancies/' + str(self.vacancy.pk) + '/milestones/' + str(milestone.pk) + '/'
         data = {"name": "a" * 101, "detail": "install all things", "expected_start": "2019-01-21",
@@ -538,9 +553,7 @@ class VacancyMilestoneTests(APITestCase):
         self.assertEqual(new_milestone.detail, milestone.detail)
 
     def test_modify_milestone_on_a_vacancy_unauthorized_user(self):
-        milestone = VacancyMilestone.objects.create(vacancy=self.vacancy, name="initiate", detail="install things",
-                                                    expected_start=datetime.fromtimestamp(0),
-                                                    expected_finish=datetime.fromtimestamp(86400))
+        milestone = self.create_milestone_object()
         self.client.force_authenticate(user=self.user)
         url = '/api/vacancies/' + str(self.vacancy.pk) + '/milestones/' + str(milestone.pk) + '/'
         data = {"name": "initiate env", "detail": "install all things", "expected_start": "2019-01-20",
@@ -553,19 +566,26 @@ class VacancyMilestoneTests(APITestCase):
         self.assertEqual(new_milestone.detail, milestone.detail)
 
     def test_delete_milestone_on_a_vacancy_success(self):
-        milestone = VacancyMilestone.objects.create(vacancy=self.vacancy, name="initiate", detail="install things",
-                                                    expected_start=datetime.fromtimestamp(0),
-                                                    expected_finish=datetime.fromtimestamp(86400))
+        milestone = self.create_milestone_object()
         self.client.force_authenticate(user=self.company_user)
         url = '/api/vacancies/' + str(self.vacancy.pk) + '/milestones/' + str(milestone.pk) + '/'
         response = self.client.delete(url, format='json')
         self.assertEqual(response.status_code, status.HTTP_200_OK)
         self.assertEqual(self.vacancy.milestones.count(), 0)
 
+    def test_delete_milestone_on_a_vacancy_not_found(self):
+        milestone = self.create_milestone_object()
+        self.client.force_authenticate(user=self.company_user)
+        url = '/api/vacancies/1000/milestones/' + str(milestone.pk) + '/'
+        response = self.client.delete(url, format='json')
+        self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
+        url2 = '/api/vacancies/' + str(self.vacancy.pk) + '/milestones/1000/'
+        response2 = self.client.delete(url2, format='json')
+        self.assertEqual(response2.status_code, status.HTTP_404_NOT_FOUND)
+        self.assertEqual(self.vacancy.milestones.count(), 1)
+
     def test_delete_milestone_on_a_vacancy_unauthorized_user(self):
-        milestone = VacancyMilestone.objects.create(vacancy=self.vacancy, name="initiate", detail="install things",
-                                                    expected_start=datetime.fromtimestamp(0),
-                                                    expected_finish=datetime.fromtimestamp(86400))
+        milestone = self.create_milestone_object()
         self.client.force_authenticate(user=self.user)
         url = '/api/vacancies/' + str(self.vacancy.pk) + '/milestones/' + str(milestone.pk) + '/'
         response = self.client.delete(url, format='json')
-- 
GitLab


From 3c64981a9b7ce817a46f1a233d1ba69d59aa5d81 Mon Sep 17 00:00:00 2001
From: Ichlasul Affan <ichlaffterlalu@gmail.com>
Date: Sun, 6 Oct 2019 14:57:13 +0700
Subject: [PATCH 14/14] Implement delete milestones

---
 core/views/vacancies.py | 23 +++++++++++++++++------
 1 file changed, 17 insertions(+), 6 deletions(-)

diff --git a/core/views/vacancies.py b/core/views/vacancies.py
index 0c3a688c..2588f117 100644
--- a/core/views/vacancies.py
+++ b/core/views/vacancies.py
@@ -292,8 +292,8 @@ class VacancyMilestoneViewSet(viewsets.GenericViewSet):
         page = self.paginate_queryset(milestones)
         if page is not None:
             return self.get_paginated_response(
-                VacancyMilestoneSerializer(page, many=True, context={'request': request}).data)
-        return Response(VacancyMilestoneSerializer(milestones, many=True, context={'request': request}).data)
+               self.serializer_class(page, many=True, context={'request': request}).data)
+        return Response(self.serializer_class(milestones, many=True, context={'request': request}).data)
 
     def create(self, request, vacancy_id):
         """
@@ -308,10 +308,10 @@ class VacancyMilestoneViewSet(viewsets.GenericViewSet):
               paramType: body
         """
         vacancy = get_object_or_404(Vacancy.objects.all(), pk=vacancy_id)
-        milestone_serializer = VacancyMilestoneSerializer(data=request.data)
+        milestone_serializer = self.serializer_class(data=request.data)
         if milestone_serializer.is_valid():
             milestone = milestone_serializer.save(vacancy=vacancy)
-            return Response(VacancyMilestoneSerializer(milestone, context={'request': request}).data,
+            return Response(self.serializer_class(milestone, context={'request': request}).data,
                             status=status.HTTP_200_OK)
         return Response(milestone_serializer.errors, status=status.HTTP_400_BAD_REQUEST)
 
@@ -329,9 +329,20 @@ class VacancyMilestoneViewSet(viewsets.GenericViewSet):
         """
         vacancy = get_object_or_404(Vacancy.objects.all(), pk=vacancy_id)
         old_milestone = get_object_or_404(vacancy.milestones.all(), pk=pk)
-        milestone_serializer = VacancyMilestoneSerializer(old_milestone, data=request.data)
+        milestone_serializer = self.serializer_class(old_milestone, data=request.data)
         if milestone_serializer.is_valid():
             milestone = milestone_serializer.save(vacancy=vacancy)
-            return Response(VacancyMilestoneSerializer(milestone, context={'request': request}).data,
+            return Response(self.serializer_class(milestone, context={'request': request}).data,
                             status=status.HTTP_200_OK)
         return Response(milestone_serializer.errors, status=status.HTTP_400_BAD_REQUEST)
+
+    def destroy(self, request, vacancy_id, pk):
+        """
+        Remove existing milestone {pk} from vacancy {vacancy_id}
+        ---
+        """
+        vacancy = get_object_or_404(Vacancy.objects.all(), pk=vacancy_id)
+        milestone = get_object_or_404(vacancy.milestones.all(), pk=pk)
+        milestone.delete()
+        return Response(
+            self.serializer_class(vacancy.milestones.all(), many=True, context={'request': request}).data)
-- 
GitLab