Exemple #1
0
 def test_userprofile_get_by_student_id(self):
     self.assertEqual(self.student_profile,
                      UserProfile.get_by_student_id("12345X"))
     self.assertEqual(self.grader_profile,
                      UserProfile.get_by_student_id("67890Y"))
     self.assertRaises(UserProfile.DoesNotExist,
                       UserProfile.get_by_student_id, "111111")
    def create(self, validated_data):
        user_dict = validated_data['user']
        tag_dict = validated_data['tag']

        first_in_required = [ f for f in self._required if f in user_dict ][0]
        user = {
            'id': lambda: UserProfile.objects.get(user__id=user_dict['id']),
            'student_id': lambda: UserProfile.get_by_student_id(user_dict['student_id']),
            'username': lambda: UserProfile.objects.get(user__username=user_dict['username']),
            'email': lambda: UserProfile.get_by_email(user_dict['email']),
        }[first_in_required]()
        tag = UserTag.objects.get(
            slug=tag_dict['slug'],
            course_instance=self.context['course_id']
        )

        obj, created = UserTagging.objects.set(user, tag)
        if not created:
            raise serializers.ValidationError(
                'User {user} already has tag {slug}'.format(
                    user=user.user.username,
                    slug=tag.slug,
                )
            )
        return obj
Exemple #3
0
    def create(self, validated_data):
        user_dict = validated_data['user']
        tag_dict = validated_data['tag']

        first_in_required = [f for f in self._required if f in user_dict][0]
        user = {
            'id':
            lambda: UserProfile.objects.get(user__id=user_dict['id']),
            'student_id':
            lambda: UserProfile.get_by_student_id(user_dict['student_id']),
            'username':
            lambda: UserProfile.objects.get(user__username=user_dict['username'
                                                                     ]),
            'email':
            lambda: UserProfile.get_by_email(user_dict['email']),
        }[first_in_required]()
        tag = UserTag.objects.get(slug=tag_dict['slug'],
                                  course_instance=self.context['course_id'])

        obj, created = UserTagging.objects.set(user, tag)
        if not created:
            raise serializers.ValidationError(
                'User {user} already has tag {slug}'.format(
                    user=user.user.username,
                    slug=tag.slug,
                ))
        return obj
Exemple #4
0
 def __init__(self, *args, **kwargs):
     super(StaffSubmissionForStudentForm, self).__init__(*args, **kwargs)
     self.fields["students"] = forms.ModelMultipleChoiceField(
         queryset=self.exercise.course_instance.get_students(),
         required=False)
     self.fields["students_by_student_id"] = forms.TypedMultipleChoiceField(
         empty_value=UserProfile.objects.none(),
         coerce=lambda student_id: UserProfile.get_by_student_id(student_id),
         choices=[(p.student_id, p.student_id) for p in self.exercise.course_instance.get_students().exclude(student_id=None)],
         required=False)
Exemple #5
0
class SubmissionCreateAndReviewForm(SubmissionReviewForm):
    STUDENT_FIELDS = ('students', 'students_by_user_id',
                      'students_by_student_id', 'students_by_email')

    submission_time = forms.DateTimeField()
    students = forms.ModelMultipleChoiceField(
        queryset=UserProfile.objects.none(), required=False)
    students_by_user_id = forms.TypedMultipleChoiceField(
        empty_value=UserProfile.objects.none(),
        coerce=lambda user_id: User.objects.get(id=user_id).userprofile,
        choices=[],
        required=False)
    students_by_student_id = forms.TypedMultipleChoiceField(
        empty_value=UserProfile.objects.none(),
        coerce=lambda student_id: UserProfile.get_by_student_id(student_id),
        choices=[],
        required=False)
    students_by_email = forms.TypedMultipleChoiceField(
        empty_value=UserProfile.objects.none(),
        coerce=lambda email: UserProfile.get_by_email(email),
        choices=[],
        required=False)

    def __init__(self, *args, **kwargs):
        super(SubmissionCreateAndReviewForm, self).__init__(*args, **kwargs)
        self.fields["students"].queryset = \
            UserProfile.objects.all()
        self.fields["students_by_user_id"].choices = \
            [ (p.user_id, p) for p in UserProfile.objects.all() ]
        self.fields["students_by_student_id"].choices = \
            [ (p.student_id, p.student_id) for p in UserProfile.objects.all() ]
        self.fields["students_by_email"].choices = \
            [ (u.email, u.email) for u in User.objects.all() ]

    def clean(self):
        self.cleaned_data = data = super(SubmissionCreateAndReviewForm,
                                         self).clean()
        fields = self.STUDENT_FIELDS
        n = sum((1 if data.get(k) else 0) for k in fields)
        if n == 0:
            raise forms.ValidationError(
                _("One of the student fields must not be blank"))
        if n > 1:
            raise forms.ValidationError(
                _("Only one student field can be given"))
        return data

    @property
    def cleaned_students(self):
        data = self.cleaned_data
        for field in self.STUDENT_FIELDS:
            s = data.get(field)
            if s:
                return s
        raise RuntimeError("Didn't find any students")
Exemple #6
0
class SubmissionCreateAndReviewForm(SubmissionReviewForm):

    submission_time = forms.DateTimeField()
    students = forms.ModelMultipleChoiceField(
        queryset=UserProfile.objects.none(), required=False)
    students_by_student_id = forms.TypedMultipleChoiceField(
        empty_value=UserProfile.objects.none(),
        coerce=lambda student_id: UserProfile.get_by_student_id(student_id),
        choices=[(p.student_id, p.student_id)
                 for p in UserProfile.objects.none()],
        required=False)
    students_by_email = forms.TypedMultipleChoiceField(
        empty_value=UserProfile.objects.none(),
        coerce=lambda email: UserProfile.get_by_email(email),
        choices=[(u.email, u.email) for u in User.objects.none()],
        required=False)

    def __init__(self, *args, **kwargs):
        super(SubmissionCreateAndReviewForm, self).__init__(*args, **kwargs)
        self.fields["students"].queryset = \
            UserProfile.objects.all()
        #self.exercise.course_instance.get_student_profiles()
        self.fields["students_by_student_id"].choices = \
            [ (p.student_id, p.student_id) for p in UserProfile.objects.all()
              #self.exercise.course_instance.get_student_profiles()
            ]
        self.fields["students_by_email"].choices = \
            [ (u.email, u.email) for u in User.objects.all() ]

    def clean(self):
        self.cleaned_data = super(SubmissionCreateAndReviewForm, self).clean()
        n = 0
        if self.cleaned_data.get("students"):
            n += 1
        if self.cleaned_data.get("students_by_student_id"):
            n += 1
        if self.cleaned_data.get("students_by_email"):
            n += 1
        if n == 0:
            raise forms.ValidationError(
                _("One of the student fields must not be blank: students, students_by_student_id, students_by_email"
                  ))
        if n > 1:
            raise forms.ValidationError(
                _("Only one student field can be given: students, students_by_student_id, students_by_email"
                  ))
        return self.cleaned_data
Exemple #7
0
    def enroll_from_sis(self) -> int:
        """
        Enroll students based on the participants information in Student Info System.
        If student has removed herself in SIS, she will also be marked as removed in A+.

        Returns
        -------
        Number of students enrolled based on this call. -1 if there was problem accessing SIS.
        """
        from .sis import get_sis_configuration, StudentInfoSystem
        from .cache.menu import invalidate_content

        sis: StudentInfoSystem = get_sis_configuration()
        if not sis:
            return -1

        count = 0
        try:
            participants = sis.get_participants(self.sis_id)
        except Exception as e:
            logger.exception(f"Error in getting participants from SIS.")
            return -1

        for i in participants:
            try:
                profile = UserProfile.get_by_student_id(i)
                if self.enroll_student(profile.user, from_sis=True):
                    count = count + 1

            except UserProfile.DoesNotExist:
                # This is a common scenario, if the user has enrolled in SIS, but not
                # yet logged in to A+, then the user profile does not exist yet.
                pass

        # Remove SIS-enrolled students who are not anymore in SIS participants,
        # for example, because they have first enrolled in SIS, but then
        # unenrolled themselves.
        students = self.all_students.filter(enrollment__from_sis=True)
        to_remove = students.exclude(student_id__in=participants)
        qs = Enrollment.objects.filter(user_profile__in=to_remove,
                                       course_instance=self)
        qs.update(status=Enrollment.ENROLLMENT_STATUS.REMOVED)
        for e in qs:
            invalidate_content(Enrollment, e)

        return count
Exemple #8
0
    def handle(self, *args, **options):
        try:
            course_instance = CourseInstance.objects.get(
                id=options['course_instance_id'])
        except CourseInstance.DoesNotExist:
            raise CommandError(
                f"CourseInstance id={options['course_instance_id']} does not exist!"
            )

        nonexistent_ids = []
        counter = 0
        try:
            with open(options['student_list_file'], 'r') as f:
                for row in f:
                    identifier = row.strip()
                    if identifier:
                        if options['email']:
                            user = User.objects.filter(
                                email=identifier, ).exclude(
                                    userprofile__student_id__isnull=True,
                                ).exclude(
                                    userprofile__student_id='', ).first()
                            if user is None:
                                nonexistent_ids.append(identifier)
                            elif course_instance.enroll_student(user):
                                counter += 1
                        else:
                            try:
                                profile = UserProfile.get_by_student_id(
                                    identifier)
                                if course_instance.enroll_student(
                                        profile.user):
                                    counter += 1
                            except UserProfile.DoesNotExist:
                                nonexistent_ids.append(identifier)
        except FileNotFoundError:
            raise CommandError(
                f"The student list file {options['student_list_file']} was not found!"
            )
        except OSError as e:
            self.print_results(course_instance, counter, nonexistent_ids)
            raise CommandError("Error in reading the student list file: " +
                               str(e))

        self.print_results(course_instance, counter, nonexistent_ids)
Exemple #9
0
    def create(self, validated_data):
        user_dict = validated_data['user']
        tag_dict = validated_data['tag']

        first_in_required = [f for f in self._required if f in user_dict][0]
        try:
            user = {
                'id':
                lambda: UserProfile.objects.get(user__id=user_dict['id']),
                'student_id':
                lambda: UserProfile.get_by_student_id(user_dict['student_id']),
                'username':
                lambda: UserProfile.objects.get(user__username=user_dict[
                    'username']),
                'email':
                lambda: UserProfile.get_by_email(user_dict['email']),
            }[first_in_required]()
            tag = UserTag.objects.get(
                slug=tag_dict['slug'],
                course_instance=self.context['course_id'])
        except UserTag.DoesNotExist:
            # 404 with description
            raise exceptions.NotFound(
                "Tag with slug {slug} was not found".format(
                    slug=tag_dict['slug']))
        except UserProfile.DoesNotExist:
            raise exceptions.NotFound(
                "User identified with {key}:{value} was not found".format(
                    key=first_in_required, value=user_dict[first_in_required]))
        obj, created = UserTagging.objects.set(user, tag)
        if not created:
            raise serializers.ValidationError(
                "User {user} already has tag {slug}".format(
                    user=user.user.username,
                    slug=tag.slug,
                ))
        return obj
Exemple #10
0
def configure_content(instance: CourseInstance,
                      url: str) -> Tuple[bool, List[str]]:
    """
    Configures course content by trusted remote URL.
    """
    if not url:
        return False, [_('COURSE_CONFIG_URL_REQUIRED')]

    # save the url before fetching config. The JWT system requires this to be
    # set, so that A+ knows which service to trust to have access to the course
    # instance. The aplus config url might need access to the course instance.
    # The other service might also need to have access to the course instance
    # before it can be configured from the url.
    instance.configure_url = url
    instance.save()

    try:
        url = url.strip()
        permissions = Permissions()
        permissions.instances.add(Permission.READ, id=instance.id)
        permissions.instances.add(Permission.WRITE, id=instance.id)
        response = aplus_get(url, permissions=permissions)
        response.raise_for_status()
    except Exception as e:
        return False, [
            format_lazy(
                _('COURSE_CONFIG_ERROR_REQUEST_FAILED -- {error!s}'),
                error=e,
            )
        ]

    try:
        config = json.loads(response.text)
    except Exception as e:
        return False, [
            format_lazy(
                _('COURSE_CONFIG_ERROR_JSON_PARSER_FAILED -- {error!s}'),
                error=e,
            )
        ]

    if not isinstance(config, dict):
        return False, [_("COURSE_CONFIG_ERROR_INVALID_JSON")]

    errors = config.get('errors', [])
    if not isinstance(errors, list):
        errors = [str(errors)]

    if not config.get('success', True):
        errors.insert(0, _("COURSE_CONFIG_ERROR_SERVICE_FAILED_TO_EXPORT"))
        return False, errors

    # wrap everything in a transaction to make sure invalid configuration isn't saved
    with transaction.atomic():
        # Configure course instance attributes.
        if "start" in config:
            dt = parse_date(config["start"], errors)
            if dt:
                instance.starting_time = dt
        if "end" in config:
            dt = parse_date(config["end"], errors)
            if dt:
                instance.ending_time = dt
        if "enrollment_start" in config:
            instance.enrollment_starting_time = parse_date(
                config["enrollment_start"], errors, allow_null=True)
        if "enrollment_end" in config:
            instance.enrollment_ending_time = parse_date(
                config["enrollment_end"], errors, allow_null=True)
        if "lifesupport_time" in config:
            instance.lifesupport_time = parse_date(config["lifesupport_time"],
                                                   errors,
                                                   allow_null=True)
        if "archive_time" in config:
            instance.archive_time = parse_date(config["archive_time"],
                                               errors,
                                               allow_null=True)
        if "enrollment_audience" in config:
            enroll_audience = parse_choices(
                config["enrollment_audience"], {
                    'internal':
                    CourseInstance.ENROLLMENT_AUDIENCE.INTERNAL_USERS,
                    'external':
                    CourseInstance.ENROLLMENT_AUDIENCE.EXTERNAL_USERS,
                    'all': CourseInstance.ENROLLMENT_AUDIENCE.ALL_USERS,
                }, "enrollment_audience", errors)
            if enroll_audience is not None:
                instance.enrollment_audience = enroll_audience
        if "view_content_to" in config:
            view_content_to = parse_choices(
                config["view_content_to"], {
                    'enrolled': CourseInstance.VIEW_ACCESS.ENROLLED,
                    'enrollment_audience':
                    CourseInstance.VIEW_ACCESS.ENROLLMENT_AUDIENCE,
                    'all_registered':
                    CourseInstance.VIEW_ACCESS.ALL_REGISTERED,
                    'public': CourseInstance.VIEW_ACCESS.PUBLIC,
                }, "view_content_to", errors)
            if view_content_to is not None:
                instance.view_content_to = view_content_to
        if "index_mode" in config:
            index_mode = parse_choices(
                config["index_mode"], {
                    'results': CourseInstance.INDEX_TYPE.RESULTS,
                    'toc': CourseInstance.INDEX_TYPE.TOC,
                    'last': CourseInstance.INDEX_TYPE.LAST,
                    'experimental': CourseInstance.INDEX_TYPE.EXPERIMENT,
                }, "index_mode", errors)
            if index_mode is not None:
                instance.index_mode = index_mode

        numbering_choices = {
            'none': CourseInstance.CONTENT_NUMBERING.NONE,
            'arabic': CourseInstance.CONTENT_NUMBERING.ARABIC,
            'roman': CourseInstance.CONTENT_NUMBERING.ROMAN,
            'hidden': CourseInstance.CONTENT_NUMBERING.HIDDEN,
        }
        if "content_numbering" in config:
            numbering = parse_choices(config["content_numbering"],
                                      numbering_choices, "content_numbering",
                                      errors)
            if numbering is not None:
                instance.content_numbering = numbering
        if "module_numbering" in config:
            numbering = parse_choices(config["module_numbering"],
                                      numbering_choices, "module_numbering",
                                      errors)
            if numbering is not None:
                instance.module_numbering = numbering
        if "course_description" in config:
            # Course index.yaml files have previously used the field "description"
            # for a hidden description, so we use "course_description" for
            # the visible description.
            instance.description = str(config["course_description"])
        if "course_footer" in config:
            instance.footer = str(config["course_footer"])
        if "lang" in config:
            langs = config["lang"]
            if isinstance(langs, list):
                langs = [
                    lang for lang in langs if instance.is_valid_language(lang)
                ]
                if langs:
                    instance.language = "|{}|".format("|".join(langs))
            elif instance.is_valid_language(langs):
                instance.language = str(langs)[:5]
        if "contact" in config:
            instance.technical_error_emails = str(config["contact"])
        if "head_urls" in config:
            head_urls = config["head_urls"]
            instance.head_urls = "\n".join(head_urls) if isinstance(
                head_urls, list) else str(head_urls)
        if "assistants" in config:
            if not isinstance(config["assistants"], list):
                errors.append(_('COURSE_CONFIG_ERROR_ASSISTANTS_AS_SID_ARRAY'))
            else:
                assistants = []
                for sid in config["assistants"]:
                    try:
                        profile = UserProfile.get_by_student_id(student_id=sid)
                    except UserProfile.DoesNotExist as err:
                        errors.append(
                            format_lazy(
                                _('COURSE_CONFIG_ERROR_ASSISTANT_NO_USER_WITH_SID -- {id}'
                                  ),
                                id=sid,
                            ))
                    else:
                        assistants.append(profile)
                instance.set_assistants(assistants)
        instance.build_log_url = str(
            config['build_log_url']) if 'build_log_url' in config else ''
        # configure_url excluded from validation because the default Django URL
        # validation does not accept dotless domain names such as "grader"
        instance.full_clean(exclude=['configure_url', 'build_log_url'])
        instance.save()

        if not "categories" in config or not isinstance(
                config["categories"], dict):
            errors.append(_('COURSE_CONFIG_ERROR_CATEGORIES_REQUIRED_OBJECT'))
            transaction.set_rollback(True)
            return False, errors
        if not "modules" in config or not isinstance(config["modules"], list):
            errors.append(_('COURSE_CONFIG_ERROR_MODULES_REQUIRED_ARRAY'))
            transaction.set_rollback(True)
            return False, errors

        # Configure learning object categories.
        category_map = {}
        seen = []
        for key, c in config.get("categories", {}).items():
            if not "name" in c:
                errors.append(_('COURSE_CONFIG_ERROR_CATEGORY_REQUIRES_NAME'))
                continue
            try:
                category = instance.categories.get(
                    name=format_localization(c["name"]))
            except LearningObjectCategory.DoesNotExist:
                category = LearningObjectCategory(course_instance=instance,
                                                  name=format_localization(
                                                      c["name"]))
            if "status" in c:
                category.status = str(c["status"])
            if "description" in c:
                category.description = str(c["description"])
            if "points_to_pass" in c:
                i = parse_int(c["points_to_pass"], errors)
                if not i is None:
                    category.points_to_pass = i
            for field in [
                    "confirm_the_level",
                    "accept_unofficial_submits",
            ]:
                if field in c:
                    setattr(category, field, parse_bool(c[field]))
            category.full_clean()
            category.save()
            category_map[key] = category
            seen.append(category.id)

        for category in instance.categories.all():
            if not category.id in seen:
                category.status = LearningObjectCategory.STATUS.HIDDEN
                category.save()

        # Configure course modules.
        seen_modules = []
        seen_objects = []
        nn = 0
        n = 0
        for m in config.get("modules", []):
            if not "key" in m:
                errors.append(_('COURSE_CONFIG_ERROR_MODULE_REQUIRES_KEY'))
                continue
            try:
                module = instance.course_modules.get(url=str(m["key"]))
            except CourseModule.DoesNotExist:
                module = CourseModule(course_instance=instance,
                                      url=str(m["key"]))

            if "order" in m:
                module.order = parse_int(m["order"], errors)
            else:
                n += 1
                module.order = n

            if "title" in m:
                module.name = format_localization(m["title"])
            elif "name" in m:
                module.name = format_localization(m["name"])
            if not module.name:
                module.name = "-"
            if "status" in m:
                module.status = str(m["status"])
            if "points_to_pass" in m:
                i = parse_int(m["points_to_pass"], errors)
                if not i is None:
                    module.points_to_pass = i
            if "introduction" in m:
                module.introduction = str(m["introduction"])

            if "open" in m:
                dt = parse_date(m["open"], errors)
                if dt:
                    module.opening_time = dt
            if not module.opening_time:
                module.opening_time = instance.starting_time

            if "close" in m:
                dt = parse_date(m["close"], errors)
                if dt:
                    module.closing_time = dt
            elif "duration" in m:
                dt = parse_duration(module.opening_time, m["duration"], errors)
                if dt:
                    module.closing_time = dt
            if not module.closing_time:
                module.closing_time = instance.ending_time

            if "read-open" in m:
                module.reading_opening_time = parse_date(m["read-open"],
                                                         errors,
                                                         allow_null=True)

            if "late_close" in m:
                dt = parse_date(m["late_close"], errors)
                if dt:
                    module.late_submission_deadline = dt
                    module.late_submissions_allowed = True
            elif "late_duration" in m:
                dt = parse_duration(module.closing_time, m["late_duration"],
                                    errors)
                if dt:
                    module.late_submission_deadline = dt
                    module.late_submissions_allowed = True
            if "late_penalty" in m:
                f = parse_float(m["late_penalty"], errors)
                if not f is None:
                    module.late_submission_penalty = f

            module.full_clean()
            module.save()
            seen_modules.append(module.id)

            if not ("numerate_ignoring_modules" in config \
                    and parse_bool(config["numerate_ignoring_modules"])):
                nn = 0
            if "children" in m:
                nn = configure_learning_objects(category_map, module,
                                                m["children"], None,
                                                seen_objects, errors, nn)

        for module in instance.course_modules.all():
            # cache invalidation uses the parent when learning object is saved:
            # prefetch parent so that it wont be fetched after the it was deleted
            for lobject in module.learning_objects.all().prefetch_related(
                    'parent'):
                if lobject.id not in seen_objects:
                    exercise = lobject.as_leaf_class()
                    if (not isinstance(exercise, BaseExercise)
                            or exercise.submissions.count() == 0):
                        exercise.delete()
                    else:
                        lobject.status = LearningObject.STATUS.HIDDEN
                        lobject.order = 9999
                        # .parent may have been deleted: only save status and order
                        lobject.save(update_fields=["status", "order"])
            if module.id not in seen_modules:
                if module.learning_objects.count() == 0:
                    module.delete()
                else:
                    module.status = CourseModule.STATUS.HIDDEN
                    module.save()

        # Clean up obsolete categories.
        for category in instance.categories.filter(
                status=LearningObjectCategory.STATUS.HIDDEN):
            if category.learning_objects.count() == 0:
                category.delete()

        if "publish_url" in config:
            success = False
            publish_errors = []
            try:
                permissions = Permissions()
                permissions.instances.add(Permission.READ, id=instance.id)
                permissions.instances.add(Permission.WRITE, id=instance.id)
                response = aplus_get(config["publish_url"],
                                     permissions=permissions)
            except ConnectionError as e:
                publish_errors = [str(e)]
            else:
                if response.status_code != 200:
                    publish_errors = [
                        format_lazy(
                            _("PUBLISH_RESPONSE_NON_200 -- {status_code}"),
                            status_code=response.status_code)
                    ]

                if response.text:
                    try:
                        publish_errors = json.loads(response.text)
                    except Exception as e:
                        publish_errors = [
                            format_lazy(_(
                                "PUBLISH_ERROR_JSON_PARSER_FAILED -- {e}, {text}"
                            ),
                                        e=e,
                                        text=response.text)
                        ]
                    else:
                        if isinstance(publish_errors, dict):
                            success = publish_errors.get("success", True)
                            publish_errors = publish_errors.get("errors", [])

                        if isinstance(publish_errors, list):
                            publish_errors = (str(e) for e in publish_errors)
                        else:
                            publish_errors = [str(publish_errors)]

            if publish_errors:
                if not success:
                    errors.append(
                        format_lazy(
                            _("PUBLISHED_WITH_ERRORS -- {publish_url}"),
                            publish_url=config['publish_url']))
                errors.extend(str(e) for e in publish_errors)

            if not success:
                transaction.set_rollback(True)
                return False, errors

    return True, errors
Exemple #11
0
 def test_userprofile_get_by_student_id(self):
     self.assertEqual(self.student_profile, UserProfile.get_by_student_id("12345X"))
     self.assertEqual(self.grader_profile, UserProfile.get_by_student_id("67890Y"))
     self.assertRaises(UserProfile.DoesNotExist, UserProfile.get_by_student_id, "111111")
Exemple #12
0
class SubmissionCreateAndReviewForm(SubmissionReviewForm):
    STUDENT_FIELDS = ('students', 'students_by_user_id',
                      'students_by_student_id', 'students_by_email')

    submission_time = forms.DateTimeField(label=_('LABEL_SUBMISSION_TIME'), )
    students = forms.ModelMultipleChoiceField(
        queryset=UserProfile.objects.none(),
        required=False,
        label=_('LABEL_STUDENTS'),
    )
    students_by_user_id = forms.TypedMultipleChoiceField(
        empty_value=UserProfile.objects.none(),
        coerce=lambda user_id: User.objects.get(id=user_id).userprofile,
        choices=[],
        required=False,
        label=_('LABEL_STUDENTS_BY_USER_ID'),
    )
    students_by_student_id = forms.TypedMultipleChoiceField(
        empty_value=UserProfile.objects.none(),
        coerce=lambda student_id: UserProfile.get_by_student_id(student_id),
        choices=[],
        required=False,
        label=_('LABEL_STUDENTS_BY_STUDENT_ID'),
    )
    students_by_email = forms.TypedMultipleChoiceField(
        empty_value=UserProfile.objects.none(),
        coerce=lambda email: UserProfile.get_by_email(email),
        choices=[],
        required=False,
        label=_('LABEL_STUDENTS_BY_EMAIL'),
    )

    def __init__(self, *args, **kwargs):
        super(SubmissionCreateAndReviewForm, self).__init__(*args, **kwargs)
        self.fields["students"].queryset = \
            UserProfile.objects.all()
        self.fields["students_by_user_id"].choices = \
            [ (p.user_id, p) for p in UserProfile.objects.all() ]
        self.fields["students_by_student_id"].choices = \
            [ (p.student_id, p.student_id) for p in UserProfile.objects.all() ]
        self.fields["students_by_email"].choices = \
            [ (u.email, u.email) for u in User.objects.all() ]

    def clean(self):
        self.cleaned_data = data = super(SubmissionCreateAndReviewForm,
                                         self).clean()
        fields = self.STUDENT_FIELDS
        n = sum((1 if data.get(k) else 0) for k in fields)
        if n == 0:
            raise forms.ValidationError(
                _('SUBMISSION_CREATE_AND_REVIEW_ERROR_ALL_STUDENT_FIELDS_BLANK'
                  ))
        if n > 1:
            raise forms.ValidationError(
                _('SUBMISSION_CREATE_AND_REVIEW_ERROR_ONLY_ONE_STUDENT_FIELD_CAN_BE_GIVEN'
                  ))
        return data

    @property
    def cleaned_students(self):
        data = self.cleaned_data
        for field in self.STUDENT_FIELDS:
            s = data.get(field)
            if s:
                return s
        raise RuntimeError("Didn't find any students")
Exemple #13
0
def configure_content(instance, url):
    """
    Configures course content by trusted remote URL.
    """
    if not url:
        return [_("Configuration URL required.")]
    try:
        url = url.strip()
        response = requests.get(url)
        response.raise_for_status()
    except Exception as e:
        return [
            format_lazy(
                _("Request for a course configuration failed with error '{error!s}'. "
                  "Configuration of course aborted."),
                error=e,
            )
        ]

    instance.configure_url = url
    instance.save()

    try:
        config = json.loads(response.text)
    except Exception as e:
        return [
            format_lazy(
                _("JSON parser raised error '{error!s}'. "
                  "Configuration of course aborted."),
                error=e,
            )
        ]

    errors = []

    # Configure course instance attributes.
    if "start" in config:
        dt = parse_date(config["start"], errors)
        if dt:
            instance.starting_time = dt
    if "end" in config:
        dt = parse_date(config["end"], errors)
        if dt:
            instance.ending_time = dt
    if "enrollment_start" in config:
        instance.enrollment_starting_time = parse_date(
            config["enrollment_start"], errors, allow_null=True)
    if "enrollment_end" in config:
        instance.enrollment_ending_time = parse_date(config["enrollment_end"],
                                                     errors,
                                                     allow_null=True)
    if "lifesupport_time" in config:
        instance.lifesupport_time = parse_date(config["lifesupport_time"],
                                               errors,
                                               allow_null=True)
    if "archive_time" in config:
        instance.archive_time = parse_date(config["archive_time"],
                                           errors,
                                           allow_null=True)
    if "enrollment_audience" in config:
        enroll_audience = parse_choices(
            config["enrollment_audience"], {
                'internal': CourseInstance.ENROLLMENT_AUDIENCE.INTERNAL_USERS,
                'external': CourseInstance.ENROLLMENT_AUDIENCE.EXTERNAL_USERS,
                'all': CourseInstance.ENROLLMENT_AUDIENCE.ALL_USERS,
            }, "enrollment_audience", errors)
        if enroll_audience is not None:
            instance.enrollment_audience = enroll_audience
    if "view_content_to" in config:
        view_content_to = parse_choices(
            config["view_content_to"], {
                'enrolled': CourseInstance.VIEW_ACCESS.ENROLLED,
                'enrollment_audience':
                CourseInstance.VIEW_ACCESS.ENROLLMENT_AUDIENCE,
                'all_registered': CourseInstance.VIEW_ACCESS.ALL_REGISTERED,
                'public': CourseInstance.VIEW_ACCESS.PUBLIC,
            }, "view_content_to", errors)
        if view_content_to is not None:
            instance.view_content_to = view_content_to
    if "index_mode" in config:
        index_mode = parse_choices(
            config["index_mode"], {
                'results': CourseInstance.INDEX_TYPE.RESULTS,
                'toc': CourseInstance.INDEX_TYPE.TOC,
                'last': CourseInstance.INDEX_TYPE.LAST,
                'experimental': CourseInstance.INDEX_TYPE.EXPERIMENT,
            }, "index_mode", errors)
        if index_mode is not None:
            instance.index_mode = index_mode

    numbering_choices = {
        'none': CourseInstance.CONTENT_NUMBERING.NONE,
        'arabic': CourseInstance.CONTENT_NUMBERING.ARABIC,
        'roman': CourseInstance.CONTENT_NUMBERING.ROMAN,
        'hidden': CourseInstance.CONTENT_NUMBERING.HIDDEN,
    }
    if "content_numbering" in config:
        numbering = parse_choices(config["content_numbering"],
                                  numbering_choices, "content_numbering",
                                  errors)
        if numbering is not None:
            instance.content_numbering = numbering
    if "module_numbering" in config:
        numbering = parse_choices(config["module_numbering"],
                                  numbering_choices, "module_numbering",
                                  errors)
        if numbering is not None:
            instance.module_numbering = numbering
    if "course_description" in config:
        # Course index.yaml files have previously used the field "description"
        # for a hidden description, so we use "course_description" for
        # the visible description.
        instance.description = str(config["course_description"])
    if "course_footer" in config:
        instance.footer = str(config["course_footer"])
    if "lang" in config:
        langs = config["lang"]
        if isinstance(langs, list):
            langs = [
                lang for lang in langs if instance.is_valid_language(lang)
            ]
            if langs:
                instance.language = "|{}|".format("|".join(langs))
        elif instance.is_valid_language(langs):
            instance.language = str(langs)[:5]
    if "contact" in config:
        instance.technical_error_emails = str(config["contact"])
    if "head_urls" in config:
        head_urls = config["head_urls"]
        instance.head_urls = "\n".join(head_urls) if isinstance(
            head_urls, list) else str(head_urls)
    if "assistants" in config:
        if not isinstance(config["assistants"], list):
            errors.append(_("Assistants must be given as a student ID array."))
        else:
            assistants = []
            for sid in config["assistants"]:
                try:
                    profile = UserProfile.get_by_student_id(student_id=sid)
                except UserProfile.DoesNotExist as err:
                    errors.append(
                        _("Adding the assistant failed, because an associated "
                          "user with student ID {id} does not exist.").format(
                              id=sid))
                else:
                    assistants.append(profile)
            instance.assistants.set(assistants)
    instance.build_log_url = str(
        config['build_log_url']) if 'build_log_url' in config else ''
    # configure_url excluded from validation because the default Django URL
    # validation does not accept dotless domain names such as "grader"
    instance.full_clean(exclude=['configure_url', 'build_log_url'])
    instance.save()

    if not "categories" in config or not isinstance(config["categories"],
                                                    dict):
        errors.append(_("Categories required as an object."))
        return errors
    if not "modules" in config or not isinstance(config["modules"], list):
        errors.append(_("Modules required as an array."))
        return errors

    # Configure learning object categories.
    category_map = {}
    seen = []
    for key, c in config.get("categories", {}).items():
        if not "name" in c:
            errors.append(_("Category requires a name."))
            continue
        try:
            category = instance.categories.get(
                name=format_localization(c["name"]))
        except LearningObjectCategory.DoesNotExist:
            category = LearningObjectCategory(course_instance=instance,
                                              name=format_localization(
                                                  c["name"]))
        if "status" in c:
            category.status = str(c["status"])
        if "description" in c:
            category.description = str(c["description"])
        if "points_to_pass" in c:
            i = parse_int(c["points_to_pass"], errors)
            if not i is None:
                category.points_to_pass = i
        for field in [
                "confirm_the_level",
                "accept_unofficial_submits",
        ]:
            if field in c:
                setattr(category, field, parse_bool(c[field]))
        category.full_clean()
        category.save()
        category_map[key] = category
        seen.append(category.id)

    for category in instance.categories.all():
        if not category.id in seen:
            category.status = LearningObjectCategory.STATUS.HIDDEN
            category.save()

    # Configure course modules.
    seen_modules = []
    seen_objects = []
    nn = 0
    n = 0
    for m in config.get("modules", []):
        if not "key" in m:
            errors.append(_("Module requires a key."))
            continue
        try:
            module = instance.course_modules.get(url=str(m["key"]))
        except CourseModule.DoesNotExist:
            module = CourseModule(course_instance=instance, url=str(m["key"]))

        if "order" in m:
            module.order = parse_int(m["order"], errors)
        else:
            n += 1
            module.order = n

        if "title" in m:
            module.name = format_localization(m["title"])
        elif "name" in m:
            module.name = format_localization(m["name"])
        if not module.name:
            module.name = "-"
        if "status" in m:
            module.status = str(m["status"])
        if "points_to_pass" in m:
            i = parse_int(m["points_to_pass"], errors)
            if not i is None:
                module.points_to_pass = i
        if "introduction" in m:
            module.introduction = str(m["introduction"])

        if "open" in m:
            dt = parse_date(m["open"], errors)
            if dt:
                module.opening_time = dt
        if not module.opening_time:
            module.opening_time = instance.starting_time

        if "close" in m:
            dt = parse_date(m["close"], errors)
            if dt:
                module.closing_time = dt
        elif "duration" in m:
            dt = parse_duration(module.opening_time, m["duration"], errors)
            if dt:
                module.closing_time = dt
        if not module.closing_time:
            module.closing_time = instance.ending_time

        if "read-open" in m:
            module.reading_opening_time = parse_date(m["read-open"],
                                                     errors,
                                                     allow_null=True)

        if "late_close" in m:
            dt = parse_date(m["late_close"], errors)
            if dt:
                module.late_submission_deadline = dt
                module.late_submissions_allowed = True
        elif "late_duration" in m:
            dt = parse_duration(module.closing_time, m["late_duration"],
                                errors)
            if dt:
                module.late_submission_deadline = dt
                module.late_submissions_allowed = True
        if "late_penalty" in m:
            f = parse_float(m["late_penalty"], errors)
            if not f is None:
                module.late_submission_penalty = f

        module.full_clean()
        module.save()
        seen_modules.append(module.id)

        if not ("numerate_ignoring_modules" in config \
                and parse_bool(config["numerate_ignoring_modules"])):
            nn = 0
        if "children" in m:
            nn = configure_learning_objects(category_map, module,
                                            m["children"], None, seen_objects,
                                            errors, nn)

    for module in list(instance.course_modules.all()):
        for lobject in list(module.learning_objects.all()):
            if not lobject.id in seen_objects:
                exercise = lobject.as_leaf_class()
                if (not isinstance(exercise, BaseExercise)
                        or exercise.submissions.count() == 0):
                    exercise.delete()
                else:
                    lobject.status = LearningObject.STATUS.HIDDEN
                    lobject.order = 9999
                    lobject.save()
        if not module.id in seen_modules:
            if module.learning_objects.count() == 0:
                module.delete()
            else:
                module.status = CourseModule.STATUS.HIDDEN
                module.save()

    # Clean up obsolete categories.
    for category in instance.categories.filter(
            status=LearningObjectCategory.STATUS.HIDDEN):
        if category.learning_objects.count() == 0:
            category.delete()

    return errors