Repository: apluslms/a-plus

Issue:
Error: view object has no attribute 'redirect'
We have seen this error in the server logs a lot recently. Any internal server error is bad and implies that users end up suddenly on a crash page (HTTP 500 Internal server error). This must be looked into.

```
Internal Server Error: /COURSEKEY/COURSEINSTANCEKEY/toc/

AttributeError at /COURSEKEY/COURSEINSTANCEKEY/toc/
'TableOfContentsView' object has no attribute 'redirect'
```

This occurs on multiple courses (at least, I saw many errors on the courses Y1 and TRAK Y) and on multiple views. `TableOfContentsView` has been common there, but several other view classes have been named in the error logs, at least the following:

* SubmissionView
* ResultsView
* LanguageView
* ModuleView


Discussion:
Example traceback:
```
[2022-09-07 09:57:25,856: ERROR/log] Internal Server Error: /a1141/2022/user/setlang
Traceback (most recent call last):
  File "/usr/local/lib/python3.8/dist-packages/django/core/handlers/exception.py", line 47, in inner
    response = get_response(request)
  File "/usr/local/lib/python3.8/dist-packages/django/core/handlers/base.py", line 181, in _get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
  File "/usr/local/lib/python3.8/dist-packages/django/views/generic/base.py", line 70, in view
    return self.dispatch(request, *args, **kwargs)
  File "/a-plus/authorization/views.py", line 39, in dispatch
    response = self.handle_exception(exc)
  File "/a-plus/course/viewbase.py", line 141, in handle_exception
    return super().handle_exception(exc)
  File "/a-plus/authorization/views.py", line 22, in handle_exception
    raise exc
  File "/a-plus/authorization/views.py", line 34, in dispatch
    self.validate_request(request, *args, **kwargs)
  File "/a-plus/authorization/views.py", line 80, in validate_request
    super().validate_request(request, *args, **kwargs)
  File "/a-plus/authorization/views.py", line 177, in validate_request
    super().validate_request(request, *args, **kwargs)
  File "/a-plus/authorization/views.py", line 128, in validate_request
    self.check_permissions(request)
  File "/a-plus/authorization/views.py", line 115, in check_permissions
    self.permission_denied(message)
  File "/a-plus/authorization/views.py", line 105, in permission_denied
    raise ValidationFailed(self.handle_no_permission())
  File "/a-plus/course/viewbase.py", line 170, in handle_no_permission
    return self.redirect(exercise.get_absolute_url())
AttributeError: 'LanguageView' object has no attribute 'redirect'
```

Questions: 
* Is a permission denied correctly or should the request actually pass the permissions?
* These views haven't raised this AttributeError before 2022. Why would they do it now? Should the view actually call `.redirect()` at all?
* If the view class isn't supposed to redirect like this, then the bug is somewhere else that now causes the permission error and the call to `handle_no_permission()` in the view base class.
Progress: able to reproduce the error locally.

A redirect is called when trying to find the enrolment exercise
https://github.com/apluslms/a-plus/blob/a236226539c7a37f601e78a3bb39ec9f76c843fa/course/viewbase.py#L170
![Screenshot from 2022-09-20 13-31-59](https://user-images.githubusercontent.com/46625399/191235706-5ca05904-5286-49b6-bb71-9fd601d14b2b.png)

This can be achieved by signing is an unenrolled and opening /def/current/toc/


Possible solution: all the views causing issues (TableOfContentsView, SubmissionView, ResultsView, LanguageView, ModuleView) all derive from CourseInstanceMixin. If a redirect method is added to CourseInstanceMixin then all will have the attribute redirect.

Resolving patch:
diff --git a/course/viewbase.py b/course/viewbase.py
--- a/course/viewbase.py
+++ b/course/viewbase.py
@@ -167,7 +167,7 @@ def handle_no_permission(self):
                 exercise = LearningObject.objects.find_enrollment_exercise(
                     self.instance, self.profile.is_external)
                 if exercise:
-                    return self.redirect(exercise.get_absolute_url())
+                    return redirect(exercise.get_absolute_url())
             return redirect(self.instance.get_url('enroll'))
         return super().handle_no_permission()
 


Test patch: