Repository: apluslms/a-plus

Issue:
Course-page containing all the instances of a course
Currently there is no list of the instances of the same course. A page collecting these different instances would needed, as discussed in https://github.com/apluslms/a-plus/issues/425

The page should contain the past, current and future instances of the same course. 

Details about the style, navigation and possible limitations by user-type (teacher, student, assistant) can be discussed here. 

This old issue is related https://github.com/apluslms/a-plus/issues/11


Discussion:
> O1 needs

The specific Programming 1 need is the ability to link to the newest version of the course. See the part about query string `?code=cs-a1110&when=current&next=/foo/bar&forward=true` in #522 . However, that could be worked on as a followup issue.

Resolving patch:
diff --git a/course/models.py b/course/models.py
--- a/course/models.py
+++ b/course/models.py
@@ -16,6 +16,7 @@
 from django.db.models.signals import post_save
 from django.utils import timezone
 from django.utils.functional import cached_property
+from django.utils.text import format_lazy
 from django.utils.translation import ugettext_lazy as _
 from django_colortag.models import ColorTag
 
@@ -450,6 +451,11 @@ def __str__(self):
     def clean(self):
         super().clean()
         errors = {}
+        RESERVED = ("instances",)
+        if self.instance_name in RESERVED:
+            errors['instance_name'] = format_lazy(_("You cannot use word '{}' as an instance name."), self.instance_name)
+        if self.url in RESERVED:
+            errors['url'] = format_lazy(_("You cannot use word '{}' in the url."), self.url)
         if self.ending_time <= self.starting_time:
             errors['ending_time'] = _("Ending time must be later than starting time.")
         if self.lifesupport_time and self.lifesupport_time < self.ending_time:
diff --git a/course/urls.py b/course/urls.py
--- a/course/urls.py
+++ b/course/urls.py
@@ -16,6 +16,12 @@
     url(r'^archive/$',
         views.ArchiveView.as_view(),
         name="archive"),
+    url(COURSE_URL_PREFIX + r'instances/$',
+        views.CourseInstancesView.as_view(),
+        name="course_instances"),
+    url(COURSE_URL_PREFIX + r'$',
+        views.LastInstanceView.as_view(),
+        name="course_last_instance"),
     url(INSTANCE_URL_PREFIX + r'$',
         views.InstanceView.as_view(),
         name="course"),
diff --git a/course/views.py b/course/views.py
--- a/course/views.py
+++ b/course/views.py
@@ -21,13 +21,13 @@
 from exercise.cache.hierarchy import NoSuchContent
 from exercise.models import LearningObject
 from lib.helpers import settings_text, remove_query_param_from_url
-from lib.viewbase import BaseTemplateView, BaseRedirectMixin, BaseFormView, BaseView
+from lib.viewbase import BaseTemplateView, BaseRedirectMixin, BaseFormView, BaseView, BaseRedirectView
 from userprofile.viewbase import UserProfileView
 from .forms import GroupsForm, GroupSelectForm
-from .models import CourseInstance, Enrollment
+from .models import CourseInstance, Course, Enrollment
 from .permissions import EnrollInfoVisiblePermission
 from .renders import group_info_context
-from .viewbase import CourseModuleBaseView, CourseInstanceMixin, EnrollableViewMixin
+from .viewbase import CourseModuleBaseView, CourseInstanceMixin, EnrollableViewMixin, CourseMixin
 
 
 class HomeView(UserProfileView):
@@ -89,6 +89,38 @@ def get_common_objects(self):
         self.instances = CourseInstance.objects.get_visible(self.request.user)
         self.note("instances")
 
+class CourseInstancesView(UserProfileView):
+    access_mode = ACCESS.ANONYMOUS
+    template_name = "course/course_instances.html"
+
+    def get_common_objects(self, **kwargs):
+        course = get_object_or_404(Course, url=self.kwargs['course_slug'])
+        self.instances = []
+        self.msg = ""
+        if CourseInstance.objects.filter(course=course).count() > 0: 
+            self.instances = CourseInstance.objects.get_visible(self.request.user).filter(course=course).order_by('-starting_time')
+            self.msg = _("The course instances of this course are not visible to students.")   
+        else:
+            self.msg = _("There are no course instances for this course.")
+
+        self.note("instances", "msg")
+
+class LastInstanceView(CourseMixin, BaseRedirectView):
+    access_mode = ACCESS.STUDENT
+
+    def get_resource_objects(self):
+        super().get_resource_objects()
+        course = get_object_or_404(Course, url=self._get_kwarg(self.course_kw))
+        course_instances = CourseInstance.objects.filter(course=course).order_by('-starting_time')
+
+        if course_instances:
+            self.course_instance = course_instances[0]
+        else:
+            raise Http404(_("There are no course instances for this course."))
+    
+    def get(self, request, *args, **kwargs):
+        return self.redirect(self.course_instance.url)
+
 class InstanceView(EnrollableViewMixin, BaseTemplateView):
     access_mode = ACCESS.STUDENT
     # ACCESS.STUDENT requires users to log in, but the access mode is dropped


Test patch: