Skip to content

Commit

Permalink
Add external students
Browse files Browse the repository at this point in the history
  • Loading branch information
teemulehtinen committed Jan 22, 2016
1 parent be24351 commit 4baca30
Show file tree
Hide file tree
Showing 13 changed files with 151 additions and 50 deletions.
16 changes: 8 additions & 8 deletions a-plus/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,14 +156,6 @@
"django.contrib.messages.context_processors.messages",
)

AUTHENTICATION_BACKENDS = [
'django.contrib.auth.backends.ModelBackend',
]
if 'shibboleth_login' in INSTALLED_APPS:
AUTHENTICATION_BACKENDS += 'shibboleth_login.auth_backend.ShibbolethAuthBackend'
if 'social.apps.django_app.default' in INSTALLED_APPS:
AUTHENTICATION_BACKENDS += 'social.backends.google.GoogleOAuth2'

FILE_UPLOAD_HANDLERS = (
#"django.core.files.uploadhandler.MemoryFileUploadHandler",
"django.core.files.uploadhandler.TemporaryFileUploadHandler",
Expand Down Expand Up @@ -256,3 +248,11 @@
pass

INSTALLED_APPS = INSTALLED_LOGIN_APPS + INSTALLED_APPS

AUTHENTICATION_BACKENDS = (
'django.contrib.auth.backends.ModelBackend',
)
if 'shibboleth_login' in INSTALLED_APPS:
AUTHENTICATION_BACKENDS += ('shibboleth_login.auth_backend.ShibbolethAuthBackend',)
if 'social.apps.django_app.default' in INSTALLED_APPS:
AUTHENTICATION_BACKENDS += ('social.backends.google.GoogleOAuth2',)
7 changes: 4 additions & 3 deletions apps/templatetags/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,19 +17,20 @@ def plugin_renderers(user, some_model, view_name=None):
"""
Builds the plugin renderers for a view.
"""
profile = user.userprofile if user.is_authenticated() else None
if isinstance(some_model, CourseInstance):
return build_plugin_renderers(
some_model.plugins.all(),
view_name or "course_instance",
user_profile=user.userprofile,
user_profile=profile,
course_instance=some_model,
)
if isinstance(some_model, BaseExercise):
course_instance = some_model.course_instance
return build_plugin_renderers(
course_instance.plugins.all(),
view_name or "exercise",
user_profile=user.userprofile,
user_profile=profile,
exercise=some_model,
course_instance=course_instance,
)
Expand All @@ -38,7 +39,7 @@ def plugin_renderers(user, some_model, view_name=None):
return build_plugin_renderers(
course_instance.plugins.all(),
view_name or "submission",
user_profile=user.userprofile,
user_profile=profile,
submission=some_model,
exercise=some_model.exercise,
course_instance=course_instance,
Expand Down
33 changes: 33 additions & 0 deletions course/migrations/0015_auto_20160121_1544.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals

from django.db import models, migrations


class Migration(migrations.Migration):

dependencies = [
('userprofile', '0002_auto_20150427_1717'),
('course', '0014_auto_20160106_1303'),
]

operations = [
migrations.AddField(
model_name='courseinstance',
name='students',
field=models.ManyToManyField(blank=True, related_name='enrolled', to='userprofile.UserProfile'),
preserve_default=True,
),
migrations.AddField(
model_name='courseinstance',
name='submission_access',
field=models.IntegerField(choices=[(1, 'Internal and external users'), (2, 'Only external users'), (3, 'Only internal users')], default=3),
preserve_default=True,
),
migrations.AddField(
model_name='courseinstance',
name='view_access',
field=models.IntegerField(choices=[(0, 'Public to internet'), (1, 'Internal and external users'), (2, 'Only external users'), (3, 'Only internal users')], default=3),
preserve_default=True,
),
]
22 changes: 22 additions & 0 deletions course/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,17 @@ class CourseInstance(models.Model):
validators=[RegexValidator(regex="^[\w\-\.]*$")],
help_text=_("Input an URL identifier for this course instance."))
visible_to_students = models.BooleanField(default=True)
view_access = models.IntegerField(choices=(
(0, _('Public to internet')),
(1, _('Internal and external users')),
(2, _('Only external users')),
(3, _('Only internal users')),
), default=3)
submission_access = models.IntegerField(choices=(
(1, _('Internal and external users')),
(2, _('Only external users')),
(3, _('Only internal users')),
), default=3)
starting_time = models.DateTimeField()
ending_time = models.DateTimeField()
image = models.ImageField(blank=True, null=True, upload_to=build_upload_dir)
Expand All @@ -125,6 +136,7 @@ class CourseInstance(models.Model):
), default=1)
configure_url = models.URLField(blank=True)
assistants = models.ManyToManyField(UserProfile, related_name="assisting_courses", blank=True)
students = models.ManyToManyField(UserProfile, related_name="enrolled", blank=True)
technical_error_emails = models.CharField(max_length=255, blank=True,
help_text=_("By default exercise errors are reported to teacher "
"email addresses. Set this field as comma separated emails to "
Expand Down Expand Up @@ -158,6 +170,16 @@ def save(self, *args, **kwargs):
if self.image:
resize_image(self.image.path, (800,600))

def has_submission_access(self, user):
if not user or not user.is_authenticated():
return False, _("You need to login to submit exercises to this course.")
if self.submission_access == 2:
return user.userprofile.is_external, \
_("You need to login as external student to submit exercises to this course.")
if self.submission_access > 2:
return not user.userprofile.is_external, \
_("You need to login as internal student to submit exercises to this course.")

def is_assistant(self, user):
return user and user.is_authenticated() \
and self.assistants.filter(id=user.userprofile.id).exists()
Expand Down
15 changes: 14 additions & 1 deletion course/viewbase.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,18 +48,31 @@ def get_resource_objects(self):
self.is_course_staff = self.is_teacher or self.is_assistant
self.note("instance", "is_assistant", "is_course_staff")

# Loosen the access mode if instance is public.
if self.instance.view_access == 0 and self.access_mode == ACCESS.STUDENT:
self.access_mode = ACCESS.ANONYMOUS

def access_control(self):
super().access_control()
if self.access_mode >= ACCESS.ASSISTANT:
if not self.is_course_staff:
messages.error(self.request,
_("Only course staff shall pass."))
raise PermissionDenied()
elif not self.instance.is_visible_to(self.request.user):
else:
if not self.instance.is_visible_to(self.request.user):
messages.error(self.request,
_("The resource is not currently visible."))
raise PermissionDenied()

# View access.
if self.instance.view_access == 2 and not self.profile.is_external:
messages.error(self.request, _("This course is only for external students."))
raise PermissionDenied()
if self.instance.view_access > 2 and self.profile.is_external:
messages.error(self.request, _("This course is only for internal students."))
raise PermissionDenied()


class CourseInstanceBaseView(CourseInstanceMixin, BaseTemplateView):
pass
Expand Down
2 changes: 2 additions & 0 deletions edit_course/course_forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ class Meta:
'language',
'starting_time',
'ending_time',
'view_access',
'submission_access',
'assistants',
'technical_error_emails',
]
Expand Down
72 changes: 44 additions & 28 deletions exercise/exercise_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,8 +160,8 @@ def load(self, request, students, url_name="exercise"):
"""
Loads the learning object page.
"""
if not self.exercise.service_url:
return ExercisePage(self.exercise)
if not self.service_url:
return ExercisePage(self)
if self.id and self.content \
and self.course_instance.ending_time < timezone.now():
page = ExercisePage(self)
Expand Down Expand Up @@ -217,6 +217,17 @@ def clean(self):
def is_submittable(self):
return True

def all_can_submit(self, students):
instance = self.course_instance
if not students:
return instance.has_submission_access(None)
for profile in students:
# TODO: check all students are enrolled
ok, warning = instance.has_submission_access(profile.user)
if not ok:
return ok, warning
return True, ""

def one_has_access(self, students, when=None):
"""
Checks if any of the users can submit taking the granted extra time
Expand Down Expand Up @@ -281,31 +292,36 @@ def is_submission_allowed(self, students):
warnings.append(_('The course is archived. Exercises are offline.'))
success = False
else:
if not self.one_has_access(students):
check, message = self.all_can_submit(students)
if not check:
warnings.append(message)
success = False
else:
if not self.one_has_access(students):
warnings.append(
_('This exercise is not open for submissions.'))
if not (self.min_group_size <= len(students) <= self.max_group_size):
warnings.append(
_('This exercise can be submitted in groups of %(min)d to %(max)d students.'
'The size of your current group is %(size)d.') % {
'min': self.min_group_size,
'max': self.max_group_size,
'size': len(students),
})
if not self.one_has_submissions(students):
warnings.append(
_('You have used the allowed amount of submissions for this exercise.'))
success = len(warnings) == 0 \
or all(self.course_instance.is_course_staff(p.user) for p in students)

# If late submission is open, notify the student about point reduction.
if self.course_module.is_late_submission_open():
warnings.append(
_('This exercise is not open for submissions.'))
if not (self.min_group_size <= len(students) <= self.max_group_size):
warnings.append(
_('This exercise can be submitted in groups of %(min)d to %(max)d students.'
'The size of your current group is %(size)d.') % {
'min': self.min_group_size,
'max': self.max_group_size,
'size': len(students),
})
if not self.one_has_submissions(students):
warnings.append(
_('You have used the allowed amount of submissions for this exercise.'))
success = len(warnings) == 0 \
or all(self.course_instance.is_course_staff(p.user) for p in students)

# If late submission is open, notify the student about point reduction.
if self.course_module.is_late_submission_open():
warnings.append(
_('Deadline for the exercise has passed. Late submissions are allowed until'
'{date} but points are only worth {percent:d}%.').format(
date=date_format(self.course_module.late_submission_deadline),
percent=self.course_module.get_late_submission_point_worth(),
))
_('Deadline for the exercise has passed. Late submissions are allowed until'
'{date} but points are only worth {percent:d}%.').format(
date=date_format(self.course_module.late_submission_deadline),
percent=self.course_module.get_late_submission_point_worth(),
))

warnings = list(str(warning) for warning in warnings)
return success, warnings
Expand All @@ -318,7 +334,7 @@ def get_total_submitter_count(self):
def get_async_hash(self, students):
student_str = "-".join(
sorted(str(userprofile.id) for userprofile in students)
)
) if students else "-"
identifier = "{}.{:d}".format(student_str, self.id)
hash_key = hmac.new(
settings.SECRET_KEY.encode('utf-8'),
Expand Down Expand Up @@ -395,7 +411,7 @@ def _get_lti(self, user, host, add={}):

def get_load_url(self, request, students, url_name="exercise"):
url = super().get_load_url(request, students, url_name=url_name)
if self.lti_service:
if self.lti_service and students:
lti = self._get_lti(students[0].user, request.get_host())
return lti.sign_get_query(url)
return url
Expand Down
4 changes: 0 additions & 4 deletions exercise/migrations/0012_auto_20151218_0858.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,4 @@ class Migration(migrations.Migration):
field=models.CharField(validators=[django.core.validators.RegexValidator(regex='^[\\w\\-\\.]*$')], max_length=255, help_text='Input an URL identifier for this object.'),
preserve_default=True,
),
migrations.AlterUniqueTogether(
name='learningobject',
unique_together=set([('course_module', 'parent', 'url')]),
),
]
4 changes: 4 additions & 0 deletions exercise/migrations/0013_auto_20151222_1320.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ class Migration(migrations.Migration):
]

operations = [
migrations.AlterUniqueTogether(
name='learningobject',
unique_together=set([('course_module', 'parent', 'url')]),
),
migrations.AlterField(
model_name='learningobject',
name='status',
Expand Down
7 changes: 4 additions & 3 deletions exercise/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,13 +42,12 @@ def dispatch(self, request, *args, **kwargs):

def get_after_new_submission(self):
self.submissions = self.exercise.get_submissions_for_student(
self.profile)
self.profile) if self.profile else []
self.summary = UserExerciseSummary(self.exercise, self.request.user)
self.note("submissions", "summary")

def get(self, request, *args, **kwargs):
self.handle()
print('get', self.exercise)
students = self.get_students()
if self.exercise.is_submittable():
self.submission_check(students)
Expand Down Expand Up @@ -106,7 +105,9 @@ def post(self, request, *args, **kwargs):

def get_students(self):
# TODO: group support
return (self.profile,)
if self.profile:
return (self.profile,)
return ()

def submission_check(self, students):
ok, issues = self.exercise.is_submission_allowed(students)
Expand Down
8 changes: 7 additions & 1 deletion lib/email_messages.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import logging
import traceback
from django.conf import settings
from django.core.mail import send_mail
from django.core.urlresolvers import reverse

logger = logging.getLogger('lib.email_messages')


def email_course_error(request, exercise, message, exception=True):
"""
Expand Down Expand Up @@ -30,4 +33,7 @@ def email_course_error(request, exercise, message, exception=True):
error_trace=error_trace,
request_fields=repr(request))
if recipients:
send_mail(subject, body, settings.SERVER_EMAIL, recipients, True)
try:
send_mail(subject, body, settings.SERVER_EMAIL, recipients, True)
except Exception as e:
logger.exception('Failed to send error emails.')
7 changes: 7 additions & 0 deletions notification/views.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from course.viewbase import CourseInstanceBaseView
from lib.viewbase import PagerMixin
from userprofile.viewbase import ACCESS

from .models import NotificationSet

Expand All @@ -8,6 +9,12 @@ class NotificationsView(PagerMixin, CourseInstanceBaseView):
template_name = "notification/notifications.html"
ajax_template_name = "notification/_notifications_list.html"

def get_resource_objects(self):
super().get_resource_objects()

# Always require logged in student
self.access_mode = ACCESS.STUDENT

def get_common_objects(self):
super().get_common_objects()
notifications_set = NotificationSet.get_course(
Expand Down
4 changes: 2 additions & 2 deletions userprofile/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,9 @@ def shortname(self):
return self.user.username

@property
def is_mooc(self):
def is_external(self):
"""
Is this a mooc account rather than from school shibboleth login.
Is this an external rather than internal account.
"""
return self.user.social_auth.exists()

Expand Down

0 comments on commit 4baca30

Please sign in to comment.