Repository: apluslms/a-plus

Issue:
Course page is rendered with invalid URLs even when the validation rejects the new URL value
Under A+ edit course, course tab, when you try to enter the instance key/url "instances", it does not pass form validation, but the links in the page are anyway rendered with the new value "instances". Those links do not work since there is no course instance with that url.

This issue originates from #772.

Edit:
The form has validation and the url value "instances" does not pass the validation. However, when you see the validation error in the form, you can see that the links to the course already use the instance url "instances" even though that value has not been written to the database. If you try to open the "content" tab under the A+ edit course section, you get HTTP 404 Not found. The URL clearly has the course instance url "instances", which is very wrong.


Discussion:
I think it is validating the Url field.
![image](https://user-images.githubusercontent.com/10437475/107486730-0d717700-6b8e-11eb-9708-3ce435afe5e8.png)

@jaguarfi Yes, that is what I tried to say. The form has validation and the url value "instances" does not pass the validation. However, when you see the validation error in the form, you can see that the links to the course already use the instance url "instances" even though that value has not been written to the database. If you try to open the "content" tab under the A+ edit course section, you get HTTP 404 Not found. The URL clearly has the course instance url "instances", which is very wrong. This bug probably existed before your new course instance list page, but it wasn't visible since all url values were valid. 

Actually, if the instance url contains spaces, what will happen? Whitespace could break the URL and the course would be basically broken and inaccessible.

Resolving patch:
diff --git a/course/models.py b/course/models.py
--- a/course/models.py
+++ b/course/models.py
@@ -592,6 +592,7 @@ class CourseInstance(CourseInstanceProto, models.Model):
         ('ROMAN', 2, _('NUMBERING_ROMAN')),
         ('HIDDEN', 3, _('NUMBERING_HIDDEN_ARABIC')),
     ])
+    RESERVED_URLS = ("instances",) # strings which cannot be used as course instance URL identifiers
 
     course = models.ForeignKey(Course,
         verbose_name=_('LABEL_COURSE'),
@@ -735,10 +736,9 @@ def __str__(self):
     def clean(self):
         super().clean()
         errors = {}
-        RESERVED = ("instances",)
-        if self.instance_name in RESERVED:
+        if self.instance_name in self.RESERVED_URLS:
             errors['instance_name'] = format_lazy(_('COURSE_INSTANCE_ERROR_INSTANCE_NAME -- {}'), self.instance_name)
-        if self.url in RESERVED:
+        if self.url in self.RESERVED_URLS:
             errors['url'] = format_lazy(_('COURSE_INSTANCE_ERROR_URL -- {}'), self.url)
         if self.ending_time and self.starting_time and self.ending_time <= self.starting_time:
             errors['ending_time'] = _('COURSE_INSTANCE_ERROR_ENDING_TIME_BEFORE_STARTING')
diff --git a/edit_course/course_forms.py b/edit_course/course_forms.py
--- a/edit_course/course_forms.py
+++ b/edit_course/course_forms.py
@@ -193,6 +193,8 @@ def clean_url(self):
         # having invalid values.
         generate_url_key_validator()(self.cleaned_data['url'])
         url = self.cleaned_data['url']
+        if url in self.instance.RESERVED_URLS:
+            raise ValidationError(_('ERROR_URL_ALREADY_TAKEN'))
         if url != self.instance.url and CourseInstance.objects.filter(course=self.instance.course, url=url).exists():
             raise ValidationError(_('ERROR_URL_ALREADY_TAKEN'))
         return url


Test patch: