Repository: kartoza/prj.app

Issue:
Certificate - two similar courses raise 500 error
We have two similar courses - the same name, period, center. But two different conveners. I defined without a problem second course. Entry 2 and 3 on [our courses page](http://changelog.kartoza.com/en/qgis/certifyingorganisation/qgis-cracow-university-of-technology/).

But those two almost identical entries are impossible to edit/view/delete. 

Reference errors raised by Django are `7d50c658e4a644d9a4ac580f97f70ea0` and `a8c4d3d667cc424f905b73cc31376cfa`.

For testing purposes I have created one more course (4 entry), but changed period. This course is editable and works fine. 


Discussion:
Full traceback for these errors is here:

For viewing:

http://sentry.kartoza.com/sentry/projecta/issues/290/
updating:


http://sentry.kartoza.com/sentry/projecta/issues/36/

deleting:
http://sentry.kartoza.com/sentry/projecta/issues/291/

@Cracert we will try to fix ASAP

Resolving patch:
diff --git a/django_project/certification/models/course.py b/django_project/certification/models/course.py
--- a/django_project/certification/models/course.py
+++ b/django_project/certification/models/course.py
@@ -16,7 +16,8 @@
 from core.settings.contrib import STOP_WORDS
 from course_convener import CourseConvener
 from certifying_organisation import CertifyingOrganisation
-from course_type import CourseType, increment_name
+from course_type import CourseType
+from certification.utilities import check_slug
 from training_center import TrainingCenter
 
 logger = logging.getLogger(__name__)
@@ -90,19 +91,22 @@ class Meta:
             'end_date', 'certifying_organisation']
 
     def save(self, *args, **kwargs):
-        project_name = self.certifying_organisation.project.name
-        course_type_name = self.course_type.name
-        self.name = \
-            project_name + '_' + course_type_name + '_' + \
-            str(self.start_date) + '-' + str(self.end_date)
-        registered_course = Course.objects.all()
-        name = increment_name(self.name, registered_course)
-        words = name.split()
-        filtered_words = [word for word in words if
-                          word.lower() not in STOP_WORDS]
-        # unidecode() represents special characters (unicode data) in ASCII
-        new_list = unidecode(' '.join(filtered_words))
-        self.slug = slugify(new_list)[:100]
+        if not self.pk:
+            project_name = self.certifying_organisation.project.name
+            course_type_name = self.course_type.name
+            self.name = \
+                project_name + '_' + course_type_name + '_' + \
+                str(self.start_date) + '-' + str(self.end_date)
+            registered_course = Course.objects.all()
+            words = self.name.split()
+            filtered_words = [word for word in words if
+                              word.lower() not in STOP_WORDS]
+            # unidecode() represents special characters (unicode data) in ASCII
+            new_list = unidecode(' '.join(filtered_words))
+            new_slug = slugify(new_list)[:100]
+            # increment slug when there is duplicate.
+            new_slug = check_slug(registered_course, new_slug)
+            self.slug = new_slug
         super(Course, self).save(*args, **kwargs)
 
     def __unicode__(self):
diff --git a/django_project/certification/views/course.py b/django_project/certification/views/course.py
--- a/django_project/certification/views/course.py
+++ b/django_project/certification/views/course.py
@@ -1,6 +1,7 @@
 # coding=utf-8
 from django.core.urlresolvers import reverse
-from django.http import Http404
+from django.core.exceptions import PermissionDenied
+from django.http import Http404, HttpResponseRedirect
 from django.views.generic import (
     CreateView,
     UpdateView,
@@ -16,6 +17,7 @@
     CourseAttendee,
     Certificate)
 from ..forms import CourseForm
+from certification.utilities import check_slug
 
 
 class CourseMixin(object):
@@ -88,6 +90,44 @@ class CourseUpdateView(LoginRequiredMixin, CourseMixin, UpdateView):
     context_object_name = 'course'
     template_name = 'course/update.html'
 
+    def get(self, request, *args, **kwargs):
+        """Get the organisation_slug from the URL
+        and define the Organisation.
+
+        :param request: HTTP request object
+        :type request: HttpRequest
+
+        :param args: Positional arguments
+        :type args: tuple
+
+        :param kwargs: Keyword arguments
+        :type kwargs: dict
+
+        :returns: Unaltered request object
+        :rtype: HttpResponse
+        """
+
+        self.organisation_slug = self.kwargs.get('organisation_slug', None)
+        try:
+            self.certifying_organisation = \
+                CertifyingOrganisation.objects.get(slug=self.organisation_slug)
+        except CertifyingOrganisation.DoesNotExist:
+            raise Http404(
+                'Sorry! We could not find your certifying organisation!')
+
+        try:
+            self.object = self.get_object()
+        except PermissionDenied:
+            # Return to organisation details page when permission to edit
+            # is denied.
+            return HttpResponseRedirect(
+                reverse('certifyingorganisation-detail', kwargs={
+                    'project_slug': self.certifying_organisation.project.slug,
+                    'slug': self.certifying_organisation.slug
+                }))
+
+        return super(CourseUpdateView, self).get(request, *args, **kwargs)
+
     def get_form_kwargs(self):
         """Get keyword arguments from form.
 
@@ -158,6 +198,53 @@ def form_valid(self, form):
             return ValidationError(
                 'ERROR: Course Convener is already exists!')
 
+    def get_object(self, queryset=None):
+        """Get the object for this view.
+
+        :param queryset: A query set
+        :type queryset: QuerySet
+
+        :returns: Queryset which is filtered to only show a course
+            within the organisation.
+        :rtype: QuerySet
+        :raises: Http404
+        """
+
+        if queryset is None:
+            queryset = self.get_queryset()
+            slug = self.kwargs.get('slug', None)
+            organisation_slug = self.kwargs.get('organisation_slug', None)
+            if slug and organisation_slug:
+                try:
+                    certifying_organisation = \
+                        CertifyingOrganisation.objects.get(
+                            slug=organisation_slug)
+                except CertifyingOrganisation.DoesNotExist:
+                    raise Http404(
+                        'Sorry! We could not find your '
+                        'certifying organisation!')
+                try:
+                    obj = queryset.get(
+                        certifying_organisation=certifying_organisation,
+                        slug=slug)
+                    return obj
+                except Course.DoesNotExist:
+                    raise Http404('Sorry! We could not find your course!')
+                except Course.MultipleObjectsReturned:
+                    # Update the slug for the latest object when multiple
+                    # objects with the same slug are found and raise
+                    # Permission Denied
+                    new_slug = check_slug(queryset, slug)
+                    objects = queryset.filter(
+                        certifying_organisation=certifying_organisation,
+                        slug=slug)
+                    latest_obj = len(objects) - 1
+                    objects[latest_obj].slug = new_slug
+                    objects[latest_obj].save()
+                    raise PermissionDenied
+            else:
+                raise Http404('Sorry! We could not find your course!')
+
 
 class CourseDeleteView(LoginRequiredMixin, CourseMixin, DeleteView):
     """Delete view for Course."""
@@ -183,10 +270,25 @@ def get(self, request, *args, **kwargs):
         """
 
         self.organisation_slug = self.kwargs.get('organisation_slug', None)
-        self.certifying_organisation = \
-            CertifyingOrganisation.objects.get(slug=self.organisation_slug)
-        return super(
-            CourseDeleteView, self).get(request, *args, **kwargs)
+        try:
+            self.certifying_organisation = \
+                CertifyingOrganisation.objects.get(slug=self.organisation_slug)
+        except CertifyingOrganisation.DoesNotExist:
+            raise Http404(
+                'Sorry! We could not find your certifying organisation!')
+
+        try:
+            self.object = self.get_object()
+        except PermissionDenied:
+            # Return to organisation details page when permission to delete
+            # is denied.
+            return HttpResponseRedirect(
+                reverse('certifyingorganisation-detail', kwargs={
+                    'project_slug': self.certifying_organisation.project.slug,
+                    'slug': self.certifying_organisation.slug
+                }))
+
+        return super(CourseDeleteView, self).get(request, *args, **kwargs)
 
     def post(self, request, *args, **kwargs):
         """Post the organisation_slug from the URL.
@@ -207,8 +309,7 @@ def post(self, request, *args, **kwargs):
         self.organisation_slug = self.kwargs.get('organisation_slug', None)
         self.certifying_organisation = \
             CertifyingOrganisation.objects.get(slug=self.organisation_slug)
-        return super(
-            CourseDeleteView, self).post(request, *args, **kwargs)
+        return super(CourseDeleteView, self).post(request, *args, **kwargs)
 
     def get_success_url(self):
         """Define the redirect URL.
@@ -239,6 +340,53 @@ def get_queryset(self):
             certifying_organisation=self.certifying_organisation)
         return qs
 
+    def get_object(self, queryset=None):
+        """Get the object for this view.
+
+        :param queryset: A query set
+        :type queryset: QuerySet
+
+        :returns: Queryset which is filtered to only show a course
+            within the organisation.
+        :rtype: QuerySet
+        :raises: Http404
+        """
+
+        if queryset is None:
+            queryset = self.get_queryset()
+            slug = self.kwargs.get('slug', None)
+            organisation_slug = self.kwargs.get('organisation_slug', None)
+            if slug and organisation_slug:
+                try:
+                    certifying_organisation = \
+                        CertifyingOrganisation.objects.get(
+                            slug=organisation_slug)
+                except CertifyingOrganisation.DoesNotExist:
+                    raise Http404(
+                        'Sorry! We could not find your '
+                        'certifying organisation!')
+                try:
+                    obj = queryset.get(
+                        certifying_organisation=certifying_organisation,
+                        slug=slug)
+                    return obj
+                except Course.DoesNotExist:
+                    raise Http404('Sorry! We could not find your course!')
+                except Course.MultipleObjectsReturned:
+                    # Update the slug for the latest object when multiple
+                    # objects with the same slug are found and raise
+                    # Permission Denied
+                    new_slug = check_slug(queryset, slug)
+                    objects = queryset.filter(
+                        certifying_organisation=certifying_organisation,
+                        slug=slug)
+                    latest_obj = len(objects) - 1
+                    objects[latest_obj].slug = new_slug
+                    objects[latest_obj].save()
+                    raise PermissionDenied
+            else:
+                raise Http404('Sorry! We could not find your course!')
+
 
 class CourseDetailView(CourseMixin, DetailView):
     """Detail view for Course."""
@@ -246,6 +394,44 @@ class CourseDetailView(CourseMixin, DetailView):
     context_object_name = 'course'
     template_name = 'course/detail.html'
 
+    def get(self, request, *args, **kwargs):
+        """Get the organisation_slug from the URL
+        and define the Organisation.
+
+        :param request: HTTP request object
+        :type request: HttpRequest
+
+        :param args: Positional arguments
+        :type args: tuple
+
+        :param kwargs: Keyword arguments
+        :type kwargs: dict
+
+        :returns: Unaltered request object
+        :rtype: HttpResponse
+        """
+
+        self.organisation_slug = self.kwargs.get('organisation_slug', None)
+        try:
+            self.certifying_organisation = \
+                CertifyingOrganisation.objects.get(slug=self.organisation_slug)
+        except CertifyingOrganisation.DoesNotExist:
+            raise Http404(
+                'Sorry! We could not find your certifying organisation!')
+
+        try:
+            self.object = self.get_object()
+        except PermissionDenied:
+            # Return to organisation details page when permission to view
+            # is denied.
+            return HttpResponseRedirect(
+                reverse('certifyingorganisation-detail', kwargs={
+                    'project_slug': self.certifying_organisation.project.slug,
+                    'slug': self.certifying_organisation.slug
+                }))
+
+        return super(CourseDetailView, self).get(request, *args, **kwargs)
+
     def get_context_data(self, **kwargs):
         """Get the context data which is passed to a template.
 
@@ -319,8 +505,20 @@ def get_object(self, queryset=None):
                     obj = queryset.get(
                         certifying_organisation=certifying_organisation,
                         slug=slug)
+                    return obj
                 except Course.DoesNotExist:
                     raise Http404('Sorry! We could not find your course!')
-                return obj
+                except Course.MultipleObjectsReturned:
+                    # Update the slug for the latest object when multiple
+                    # objects with the same slug are found and raise
+                    # Permission Denied
+                    new_slug = check_slug(queryset, slug)
+                    objects = queryset.filter(
+                        certifying_organisation=certifying_organisation,
+                        slug=slug)
+                    latest_obj = len(objects) - 1
+                    objects[latest_obj].slug = new_slug
+                    objects[latest_obj].save()
+                    raise PermissionDenied
             else:
                 raise Http404('Sorry! We could not find your course!')


Test patch:
diff --git a/django_project/certification/tests/views/test_course_view.py b/django_project/certification/tests/views/test_course_view.py
--- a/django_project/certification/tests/views/test_course_view.py
+++ b/django_project/certification/tests/views/test_course_view.py
@@ -65,6 +65,28 @@ def test_detail_view(self):
         }))
         self.assertEqual(response.status_code, 200)
 
+    @override_settings(VALID_DOMAIN=['testserver', ])
+    def test_detail_with_duplicates(self):
+        self.client.login(username='anita', password='password')
+        course2 = CourseF.create(
+            certifying_organisation=self.certifying_organisation
+        )
+        course2.slug = self.course.slug
+        course2.save()
+
+        response = self.client.get(reverse('course-detail', kwargs={
+            'project_slug': self.project.slug,
+            'organisation_slug': self.certifying_organisation.slug,
+            'slug': course2.slug
+        }))
+        self.assertEqual(response.status_code, 302)
+
+        expected_url = reverse('certifyingorganisation-detail', kwargs={
+            'project_slug': self.project.slug,
+            'slug': self.certifying_organisation.slug,
+        })
+        self.assertRedirects(response, expected_url)
+
     @override_settings(VALID_DOMAIN=['testserver', ])
     def test_detail_view_object_does_not_exist(self):
         client = Client()
@@ -80,3 +102,63 @@ def test_detail_view_object_does_not_exist(self):
             'slug': 'random'
         }))
         self.assertEqual(response.status_code, 404)
+
+    @override_settings(VALID_DOMAIN=['testserver', ])
+    def test_update_view(self):
+        self.client.login(username='anita', password='password')
+        post_data = {
+            'name': 'new name',
+            'language': 'Japanese',
+        }
+        response = self.client.post(
+            reverse('course-update', kwargs={
+                'project_slug': self.project.slug,
+                'organisation_slug': self.certifying_organisation.slug,
+                'slug': self.course.slug
+            }), post_data)
+        self.assertEqual(response.status_code, 200)
+
+    @override_settings(VALID_DOMAIN=['testserver', ])
+    def test_update_with_duplicates(self):
+        self.client.login(username='anita', password='password')
+        course2 = CourseF.create(
+            certifying_organisation=self.certifying_organisation
+        )
+        course2.slug = self.course.slug
+        course2.save()
+        post_data = {
+            'name': 'new course name',
+            'language': 'Indonesian',
+        }
+        response = self.client.get(reverse('course-update', kwargs={
+            'project_slug': self.project.slug,
+            'organisation_slug': self.certifying_organisation.slug,
+            'slug': course2.slug
+        }), post_data)
+        self.assertEqual(response.status_code, 302)
+
+        expected_url = reverse('certifyingorganisation-detail', kwargs={
+            'project_slug': self.project.slug,
+            'slug': self.certifying_organisation.slug,
+        })
+        self.assertRedirects(response, expected_url)
+
+    @override_settings(VALID_DOMAIN=['testserver', ])
+    def test_delete_with_duplicates(self):
+        self.client.login(username='anita', password='password')
+        course2 = CourseF.create(
+            certifying_organisation=self.certifying_organisation
+        )
+        course2.slug = self.course.slug
+        course2.save()
+        response = self.client.get(reverse('course-delete', kwargs={
+            'project_slug': self.project.slug,
+            'organisation_slug': self.certifying_organisation.slug,
+            'slug': course2.slug
+        }))
+        self.assertEqual(response.status_code, 302)
+        expected_url = reverse('certifyingorganisation-detail', kwargs={
+            'project_slug': self.project.slug,
+            'slug': self.certifying_organisation.slug,
+        })
+        self.assertRedirects(response, expected_url)