Пример #1
0
class CodingIoQuestionAdmin(QuestionAdmin):
    class Meta:
        model = models.CodingIoQuestion

    content_panels = QuestionAdmin.content_panels[:]
    content_panels.insert(
        -1,
        panels.MultiFieldPanel([
            panels.FieldPanel('num_pre_tests'),
            panels.FieldPanel('pre_tests_source'),
        ],
                               heading=_('Pre-tests definitions')))
    content_panels.insert(
        -1,
        panels.MultiFieldPanel([
            panels.FieldPanel('num_post_tests'),
            panels.FieldPanel('post_tests_source'),
        ],
                               heading=_('Post-tests definitions')))
    content_panels.insert(
        -1, panels.InlinePanel('answers', label=_('Answer keys')))

    settings_panels = QuestionAdmin.settings_panels + [
        panels.MultiFieldPanel([
            panels.FieldPanel('language'),
            panels.FieldPanel('timeout'),
        ],
                               heading=_('Options'))
    ]
Пример #2
0
class AttendancePageAdmin(WagtailAdmin):

    class Meta:
        model = models.AttendancePage
        abstract = True

    subpage_types = []

    content_panels = \
        Page.content_panels + [
            panels.InlinePanel('attendance_sheet_single_list',
                               max_num=1, min_num=1),
        ]
Пример #3
0
class FormPage(AbstractEmailForm):
    intro = models.RichTextField(blank=True)
    thank_you_text = models.RichTextField(blank=True)

    content_panels = AbstractEmailForm.content_panels + [
        panels.FieldPanel('intro', classname="full"),
        panels.InlinePanel('form_fields', label="Form fields"),
        panels.FieldPanel('thank_you_text', classname="full"),
        panels.MultiFieldPanel([
            panels.FieldPanel('to_address', classname="full"),
            panels.FieldPanel('from_address', classname="full"),
            panels.FieldPanel('subject', classname="full"),
        ], heading=_("Email"))
    ]
Пример #4
0
class Calendar(models.Page):
    """
    A page that gathers a list of lessons in the course.
    """
    @property
    def course(self):
        return self.get_parent()

    weekly_lessons = delegate_to('course')

    def __init__(self, *args, **kwargs):
        if not args:
            kwargs.setdefault('title', __('Calendar'))
            kwargs.setdefault('slug', 'calendar')
        super().__init__(*args, **kwargs)

    def add_lesson(self, lesson, copy=True):
        """
        Register a new lesson in the course.

        If `copy=True` (default), register a copy.
        """

        if copy:
            lesson = lesson.copy()
        lesson.move(self)
        lesson.save()

    def new_lesson(self, *args, **kwargs):
        """
        Create a new lesson instance by calling the Lesson constructor with the
        given arguments and add it to the course.
        """

        kwargs['parent_node'] = self
        return LessonInfo.objects.create(*args, **kwargs)

    # Wagtail admin
    parent_page_types = ['courses.Course']
    subpage_types = ['courses.Lesson']
    content_panels = models.Page.content_panels + [
        panels.InlinePanel(
            'info',
            label=_('Lessons'),
            help_text=_('List of lessons for this course.'),
        ),
    ]
Пример #5
0
class MultipleChoiceQuestion(Question):
    """
    A very simple question with a simple numeric answer.
    """

    class Meta:
        verbose_name = _('Multiple choice question')
        verbose_name_plural = _('Multiple choice questions')

    instant_autograde = True
    __num_cleans = 0

    def clean(self):
        # FIXME: make validation works
        # Wagtail executes the clean method several times before validating
        # We need a way to validate only when all choices had been updated.
        # self.check_has_valid_choices()
        super().clean()

    def check_has_valid_choices(self):
        msg = _(
            'You must define at least one correct choice!\n'
            'The most valuable choice is worth {max_points} points.'
            'It should be exactly 100 points.')
        choices = self.choices.all()
        if not any(choice.value == 100 for choice in choices):
            max_points = max(choice.value for choice in choices)
            raise ValidationError([msg.format(max_points=max_points)])

    # Serving Pages
    template = 'questions/multiplechoice/detail.jinja2'

    def get_context(self, request, **kwargs):
        ctx = super().get_context(request, **kwargs)
        ctx['choices'] = self.choices.all()
        return ctx

    def filter_user_submission_payload(self, request, payload):
        choice_id = payload['choices']
        return {'choice_id': choice_id}

    # Wagtail admin
    content_panels = Question.content_panels + [
        panels.InlinePanel('choices', label=_('Choices')),
    ]
    content_panels.append(content_panels.pop(-2))
Пример #6
0
class CodeCarouselActivity(Activity):
    """
    In this activity, the students follow a piece of code that someone
    edit and is automatically updated in all of student machines. It keeps
    track of all modifications that were saved by the teacher.
    """
    class Meta:
        verbose_name = _('synchronized code activity')
        verbose_name_plural = _('synchronized code activities')

    default_material_icon = 'code'
    language = models.ForeignKey(
        'ProgrammingLanguage',
        on_delete=models.PROTECT,
        related_name='sync_code_activities',
        help_text=_('Chooses the programming language for the activity'),
    )

    @property
    def last(self):
        try:
            return self.items.order_by('timestamp').last()
        except CodeCarouselItem.DoesNotExist:
            return None

    @property
    def first(self):
        try:
            return self.items.order_by('timestamp').first()
        except CodeCarouselItem.DoesNotExist:
            return None

    # Wagtail admin
    content_panels = models.CodeschoolPage.content_panels + [
        panels.MultiFieldPanel([
            panels.RichTextFieldPanel('short_description'),
            panels.FieldPanel('language'),
        ],
                               heading=_('Options')),
        panels.InlinePanel('items', label='Items'),
    ]
Пример #7
0
class CodingIoQuestion(Question):
    """
    CodeIo questions evaluate source code and judge them by checking if the
    inputs and corresponding outputs match an expected pattern.
    """
    class Meta:
        verbose_name = _('Programming question (IO-based)')
        verbose_name_plural = _('Programming questions (IO-based)')

    EXT_TO_METHOD_CONVERSIONS = dict(
        Question.EXT_TO_METHOD_CONVERSIONS,
        md='markio',
    )

    iospec_size = models.PositiveIntegerField(
        _('number of iospec template expansions'),
        default=10,
        help_text=_(
            'The desired number of test cases that will be computed after '
            'comparing the iospec template with the answer key. This is only a '
            'suggested value and will only be applied if the response template '
            'uses input commands to generate random input.'),
    )
    iospec_source = models.TextField(
        _('response template'),
        help_text=_(
            'Template used to grade I/O responses. See '
            'http://pythonhosted.org/iospec for a complete reference on the '
            'template format.'),
    )
    iospec_hash = models.CharField(
        max_length=32,
        blank=True,
        help_text=_('A hash to keep track of iospec updates.'),
    )
    timeout = models.FloatField(
        _('timeout in seconds'),
        blank=True,
        default=1.0,
        help_text=_(
            'Defines the maximum runtime the grader will spend evaluating '
            'each test case.'),
    )
    language = models.ForeignKey(
        ProgrammingLanguage,
        on_delete=models.SET_NULL,
        blank=True,
        null=True,
        help_text=_(
            'Programming language associated with question. Leave it blank in '
            'order to accept submissions in any programming language. This '
            'option should be set only for questions that tests specific '
            'programming languages constructs or require techniques that only '
            'make sense in specific programming languages.'),
    )

    __iospec_updated = False
    __answers = ()

    @lazy
    def iospec(self):
        """
        The IoSpec structure corresponding to the iospec_source.
        """

        return parse_iospec(self.iospec_source)

    def __init__(self, *args, **kwargs):
        # Supports automatic conversion between iospec data and iospec_source
        iospec = kwargs.pop('iospec', None)
        if iospec:
            kwargs['iospec_source'] = iospec.source()
            self.iospec = iospec
        super().__init__(*args, **kwargs)

    def load_from_file_data(self, file_data):
        fake_post = super().load_from_file_data(file_data)
        fake_post['iospec_source'] = self.iospec_source
        return fake_post

    def clean(self):
        """
        Validate the iospec_source field.
        """

        super().clean()

        # We first should check if the iospec_source has been changed and would
        # require a possibly expensive validation.
        source = self.iospec_source
        iospec_hash = md5hash(source)
        if self.iospec_hash != iospec_hash:
            try:
                self.iospec = iospec = parse_iospec(self.iospec_source)
            except Exception as ex:
                raise ValidationError(
                    {'iospec_source': _('invalid iospec syntax: %s' % ex)})

            # Now we check if the new iospec requires an answer key code and
            # if it has some answer key defined
            self.__iospec_updated = True
            return
            if (not iospec.is_expanded) and not self.answers.has_program():
                raise ValidationError({
                    'iospec_source':
                    _('You iospec definition uses a command or an @input block '
                      'and thus requires an example grading code. Please define '
                      'an "Answer Key" item with source code for at least one '
                      'programming language.')
                })

    def load_from_markio(self, file_data):
        """
        Load question parameters from Markio file.
        """

        data = markio.parse(file_data)

        # Load simple data from markio
        self.title = data.title or self.title
        self.short_description = (data.short_description
                                  or self.short_description)
        self.timeout = data.timeout or self.timeout
        self.author_name = data.author or self.author_name
        self.iospec_source = data.tests or self.iospec_source

        # Load main description
        # noinspection PyUnresolvedReferences
        self.body = markdown_to_blocks(data.description)

        # Add answer keys
        answer_keys = OrderedDict()
        for (lang, answer_key) in data.answer_key.items():
            language = programming_language(lang)
            key = self.answers.create(question=self,
                                      language=language,
                                      source=answer_key)
            answer_keys[lang] = key
        for (lang, placeholder) in data.placeholder.items():
            if placeholder is None:
                continue
            try:
                answer_keys[lang].placeholder = placeholder
            except KeyError:
                language = ProgrammingLanguage.objects.get(lang)
                self.answer_keys.create(question=self,
                                        language=language,
                                        placeholder=placeholder)
        self.__answers = list(answer_keys.values())

    # Serialization methods: support markio and sets it as the default
    # serialization method for CodingIoQuestion's
    @classmethod
    def load_markio(cls, source):
        """
        Creates a CodingIoQuestion object from a Markio object or source
        string and saves the resulting question in the database.

        This function can run without touching the database if the markio file
        does not define any information that should be saved in an answer key.

        Args:
            source:
                A string with the Markio source code.

        Returns:
            question:
                A question object.
        """

        raise NotImplementedError

    def dump_markio(self):
        """
        Serializes question into a string of Markio source.
        """

        tree = markio.Markio(
            title=self.name,
            author=self.author_name,
            timeout=self.timeout,
            short_description=self.short_description,
            description=self.long_description,
            tests=self.iospec_source,
        )

        for key in self.answer_keys.all():
            tree.add_answer_key(key.source, key.language.ref)
            tree.add_placeholder(key.placeholder, key.language.ref)

        return tree.source()

    def full_clean(self, *args, **kwargs):
        if self.__answers:
            self.answers = self.__answers
        super().full_clean(*args, **kwargs)

    def placeholder(self, language=None):
        """
        Return the placeholder text for the given language.
        """

        key = self.answers[language or self.language]
        if key is None:
            return ''
        return key.placeholder

    def reference_source(self, language=None):
        """
        Return the reference source code for the given language or None, if no
        reference is found.
        """

        key = self.answers[language or self.language]
        if key is None:
            return ''
        return key.source

    def run_code(self, source, language=None, iospec=None):
        """
        Run the given source code string of the given programming language
        using the default or the given IoSpec.

        If no code string is given, runs the reference source code, if it
        exists.
        """

        key = self.answers[language or self.language]
        return key.run(source, iospec)

    def update_iospec_source(self):
        """
        Updates the iospec_source attribute with the current iospec object.

        Any modifications made to `self.iospec` must be saved explicitly to
        persist in the database.
        """

        if 'iospec' in self.__dict__:
            self.iospec_source = self.iospec.source()

    def submit(self, user, source=None, language=None, **kwargs):
        # Fetch info from response_data
        response_data = kwargs.get('response_data', {})
        if source is None and 'source' in response_data:
            source = response_data.pop('source')
        if language is None and 'language' in response_data:
            language = response_data.pop('language')

        # Assure language is valid
        language = language or self.language
        if not language:
            raise ValueError(
                'could not determine the programming language for '
                'the submission')

        # Assure response data is empty
        if response_data:
            key = next(iter(response_data))
            raise TypeError('invalid or duplicate parameter passed to '
                            'response_data: %r' % key)

        # Construct response data and pass it to super
        response_data = {
            'language': language.ref,
            'source': source,
        }

        return super().submit(user, response_data=response_data, **kwargs)

    # Serving pages and routing
    template = 'questions/coding_io/detail.jinja2'
    template_submissions = 'questions/coding_io/submissions.jinja2'

    def get_context(self, request, *args, **kwargs):
        context = dict(super().get_context(request, *args, **kwargs),
                       form=True)

        # Select default mode for the ace editor
        if self.language:
            context['default_mode'] = self.language.ace_mode()
        else:
            context['default_mode'] = get_config('CODESCHOOL_DEFAULT_ACE_MODE',
                                                 'python')

        # Enable language selection
        if self.language is None:
            context['select_language'] = True
            context['languages'] = ProgrammingLanguage.supported.all()
        else:
            context['select_language'] = False

        return context

    @srvice.route(r'^submit-response/$')
    def route_submit(self, client, source=None, language=None, **kwargs):
        """
        Handles student responses via AJAX and a srvice program.
        """

        # User must choose language
        if not language:
            if self.language is None:
                client.dialog('<p class="dialog-text">%s</p>' %
                              _('Please select the correct language'))
                return
            language = self.language
        else:
            language = programming_language(language)

        # Bug with <ace-editor>?
        if not source or source == '\x01\x01':
            client.dialog('<p class="dialog-text">%s</p>' %
                          _('Internal error: please send it again!'))
            return

        super().route_submit(
            client=client,
            language=language,
            source=source,
        )

    @srvice.route(r'^placeholder/$')
    def route_placeholder(self, request, language):
        """
        Return the placeholder code for some language.
        """

        return self.get_placehoder(language)

    # Wagtail admin
    content_panels = Question.content_panels[:]
    content_panels.insert(
        -1,
        panels.MultiFieldPanel([
            panels.FieldPanel('iospec_size'),
            panels.FieldPanel('iospec_source'),
        ],
                               heading=_('IoSpec definitions')))
    content_panels.insert(
        -1, panels.InlinePanel('answers', label=_('Answer keys')))
    settings_panels = Question.settings_panels + [
        panels.MultiFieldPanel([
            panels.FieldPanel('language'),
            panels.FieldPanel('timeout'),
        ],
                               heading=_('Options'))
    ]
Пример #8
0
class Course(models.RoutablePageMixin, models.TimeStampedModel, models.Page):
    """
    One specific occurrence of a course for a given teacher in a given period.
    """

    discipline = models.ForeignKey('Discipline',
                                   blank=True,
                                   null=True,
                                   on_delete=models.DO_NOTHING)
    teacher = models.ForeignKey(models.User,
                                related_name='courses_as_teacher',
                                on_delete=models.DO_NOTHING)
    students = models.ManyToManyField(
        models.User,
        related_name='courses_as_student',
        blank=True,
    )
    staff = models.ManyToManyField(
        models.User,
        related_name='courses_as_staff',
        blank=True,
    )
    weekly_lessons = models.BooleanField(
        _('weekly lessons'),
        default=False,
        help_text=_(
            'If true, the lesson spans a whole week. Othewise, each lesson '
            'would correspond to a single day/time slot.'),
    )
    accept_subscriptions = models.BooleanField(
        _('accept subscriptions'),
        default=True,
        help_text=_('Set it to false to prevent new student subscriptions.'),
    )
    is_public = models.BooleanField(
        _('is it public?'),
        default=True,
        help_text=_(
            'If true, all students will be able to see the contents of the '
            'course. Most activities will not be available to non-subscribed '
            'students.'),
    )
    subscription_passphrase = models.CharField(
        _('subscription passphrase'),
        default=random_subscription_passphase,
        max_length=140,
        help_text=_(
            'A passphrase/word that students must enter to subscribe in the '
            'course. Leave empty if no passphrase should be necessary.'),
        blank=True,
    )
    short_description = models.CharField(max_length=140, blank=True)
    description = models.RichTextField(blank=True)
    activities_template = models.CharField(
        max_length=20,
        choices=[
            ('programming-beginner', _('A beginner programming course')),
            ('programming-intermediate',
             _('An intermediate programming course')),
            ('programming-marathon', _('A marathon-level programming course')),
        ],
        blank=True)

    @lazy
    def academic_description(self):
        return getattr(self.discipline, 'description', '')

    @lazy
    def syllabus(self):
        return getattr(self.discipline, 'syllabus', '')

    objects = CourseManager()

    @property
    def calendar_page(self):
        content_type = models.ContentType.objects.get(app_label='cs_core',
                                                      model='calendarpage')
        return apps.get_model('cs_core', 'CalendarPage').objects.get(
            depth=self.depth + 1,
            path__startswith=self.path,
            content_type_id=content_type,
        )

    @property
    def activities_page(self):
        content_type = models.ContentType.objects.get(app_label='cs_questions',
                                                      model='questionlist')
        return apps.get_model('cs_questions', 'QuestionList').objects.get(
            depth=self.depth + 1,
            path__startswith=self.path,
            content_type_id=content_type,
        )

    def save(self, *args, **kwargs):
        with transaction.atomic():
            created = self.id is None

            if not self.path:
                created = False
                root = model_reference.load('course-list')
                root.add_child(instance=self)
            else:
                super().save(*args, **kwargs)

            if created:
                self.create_calendar_page()
                self.create_activities_page()

    def create_calendar_page(self):
        """
        Creates a new calendar page if it does not exist.
        """

        model = apps.get_model('courses', 'calendar')
        calendar = model()
        self.add_child(instance=calendar)

    def create_activities_page(self):
        """
        Creates a new activities page if it does not exist.
        """

        model = apps.get_model('activities', 'activitylist')
        activities = model(
            title=_('Activities'),
            slug='activities',
            short_description=ACTIVITY_DESCRIPTION % {'name': self.title},
        )
        self.add_child(instance=activities)

    def enroll_student(self, student):
        """
        Register a new student in the course.
        """

        self.students.add(student)
        self.update_friendship_status(student)

    def is_registered(self, user):
        """
        Check if user is associated with the course in any way.
        """

        if self.teacher == user:
            return True
        elif user in self.students.all():
            return True
        elif user in self.staff.all():
            return True
        else:
            return False

    def update_friendship_status(self, student=None):
        """
        Recompute the friendship status for a single student by marking it as
        a colleague of all participants in the course.

        If no student is given, update the status of all enrolled students.
        """

        update = self._update_friendship_status
        if student is None:
            for student in self.students.all():
                update(student)
        else:
            update(student)

    def _update_friendship_status(self, student):
        for colleague in self.students.all():
            if colleague != student:
                FriendshipStatus.objects.get_or_create(
                    owner=student,
                    other=colleague,
                    status=FriendshipStatus.STATUS_COLLEAGUE)

    def get_user_role(self, user):
        """Return a string describing the most privileged role the user has
        as in the course. The possible values are:

        teacher:
            Owns the course and can do any kind of administrative tasks in
            the course.
        staff:
            Teacher assistants. May have some privileges granted by the teacher.
        student:
            Enrolled students.
        visitor:
            Have no relation to the course. If course is marked as public,
            visitors can access the course contents.
        """

        if user == self.teacher:
            return 'teacher'
        if user in self.staff.all():
            return 'staff'
        if user in self.students.all():
            return 'student'
        return 'visitor'

    def info_dict(self):
        """
        Return an ordered dictionary with relevant internationalized
        information about the course.
        """
        def yn(x):
            return _('Yes' if x else 'No')

        data = [
            ('Teacher', hyperlink(self.teacher)),
            ('Created', self.created),
            ('Accepting new subscriptions?', yn(self.accept_subscriptions)),
            ('Private?', yn(not self.is_public)),
        ]
        if self.academic_description:
            data.append(('Description', self.academic_description))
        if self.syllabus:
            data.append(('Description', self.academic_description))

        return OrderedDict([(_(k), v) for k, v in data])

    # Serving pages
    template = 'courses/detail.jinja2'

    def get_context(self, request, *args, **kwargs):
        return dict(
            super().get_context(request, *args, **kwargs),
            course=self,
        )

    def serve(self, request, *args, **kwargs):
        if self.is_registered(request.user):
            return super().serve(request, *args, **kwargs)
        return self.serve_registration(request, *args, **kwargs)

    def serve_registration(self, request, *args, **kwargs):
        context = self.get_context(request)
        if request.method == 'POST':
            form = PassPhraseForm(request.POST)
            if form.is_valid():
                self.enroll_student(request.user)
                return super().serve(request, *args, **kwargs)
        else:
            form = PassPhraseForm()

        context['form'] = form
        return render(request, 'courses/course-enroll.jinja2', context)

    # Wagtail admin
    parent_page_types = ['courses.CourseList']
    subpage_types = []
    content_panels = models.Page.content_panels + [
        panels.MultiFieldPanel([
            panels.FieldPanel('short_description'),
            panels.FieldPanel('description'),
            panels.FieldPanel('teacher')
        ],
                               heading=_('Options')),
        panels.InlinePanel(
            'time_slots',
            label=_('Time slots'),
            help_text=_('Define when the weekly classes take place.'),
        ),
    ]
    settings_panels = models.Page.settings_panels + [
        panels.MultiFieldPanel([
            panels.FieldPanel('weekly_lessons'),
        ],
                               heading=_('Options')),
        panels.MultiFieldPanel([
            panels.FieldPanel('accept_subscriptions'),
            panels.FieldPanel('is_public'),
            panels.FieldPanel('subscription_passphrase'),
        ],
                               heading=_('Subscription')),
    ]
Пример #9
0
class Course(models.RoutablePageMixin, models.CodeschoolPage):
    """
    One specific occurrence of a course for a given teacher in a given period.
    """
    class Meta:
        parent_init_attribute = 'discipline'

    teachers = models.ManyToManyField(
        models.User,
        related_name='courses_as_teacher',
        blank=True,
    )
    students = models.ManyToManyField(
        models.User,
        related_name='courses_as_student',
        blank=True,
    )
    staff = models.ManyToManyField(
        models.User,
        related_name='courses_as_staff_p',
        blank=True,
    )
    weekly_lessons = models.BooleanField(
        _('weekly lessons'),
        default=False,
        help_text=_(
            'If true, the lesson spans a whole week. Othewise, each lesson '
            'would correspond to a single day/time slot.'),
    )
    accept_subscriptions = models.BooleanField(
        _('accept subscriptions'),
        default=True,
        help_text=_('Set it to false to prevent new student subscriptions.'),
    )
    is_public = models.BooleanField(
        _('is it public?'),
        default=False,
        help_text=_(
            'If true, all students will be able to see the contents of the '
            'course. Most activities will not be available to non-subscribed '
            'students.'),
    )
    subscription_passphrase = models.CharField(
        _('subscription passphrase'),
        max_length=140,
        help_text=_(
            'A passphrase/word that students must enter to subscribe in the '
            'course. Leave empty if no passphrase should be necessary.'),
        blank=True,
    )
    objects = PageManager.from_queryset(CourseQueryset)()

    short_description = delegate_to('discipline', True)
    long_description = delegate_to('discipline', True)
    short_description_html = delegate_to('discipline', True)
    long_description_html = delegate_to('discipline', True)
    lessons = property(lambda x: x.calendar_page.lessons)

    @property
    def calendar_page(self):
        content_type = models.ContentType.objects.get(app_label='cs_core',
                                                      model='calendarpage')
        return apps.get_model('cs_core', 'CalendarPage').objects.get(
            depth=self.depth + 1,
            path__startswith=self.path,
            content_type_id=content_type,
        )

    @property
    def questions_page(self):
        content_type = models.ContentType.objects.get(app_label='cs_questions',
                                                      model='questionlist')
        return apps.get_model('cs_questions', 'QuestionList').objects.get(
            depth=self.depth + 1,
            path__startswith=self.path,
            content_type_id=content_type,
        )

    @property
    def gradables_page(self):
        content_type = models.ContentType.objects.get(app_label='cs_core',
                                                      model='gradablespage')
        return apps.get_model('cs_core', 'GradablesPage').objects.get(
            depth=self.depth + 1,
            path__startswith=self.path,
            content_type_id=content_type,
        )

    @property
    def discipline(self):
        return self.get_parent().specific

    @discipline.setter
    def discipline(self, value):
        self.set_parent(value)

    @property
    def questions(self):
        return self.questions_page.questions

    def add_question(self, question, copy=True):
        """
        Register a new question to the course.

        If `copy=True` (default), register a copy.
        """

        self.questions.add_question(question, copy)

    def new_question(self, cls, *args, **kwargs):
        """
        Create a new question instance by calling the cls with the given
        arguments and add it to the course.
        """

        self.questions.new_question(cls, *args, **kwargs)

    def add_lesson(self, lesson, copy=True):
        """
        Register a new lesson in the course.

        If `copy=True` (default), register a copy.
        """

        self.lessons.add_lesson(lesson, copy)

    def new_lesson(self, *args, **kwargs):
        """
        Create a new lesson instance by calling the Lesson constructor with the
        given arguments and add it to the course.
        """

        self.lessons.new_lesson(*args, **kwargs)

    def register_student(self, student):
        """
        Register a new student in the course.
        """

        self.students.add(student)
        self.update_friendship_status(student)

    def update_friendship_status(self, student=None):
        """
        Recompute the friendship status for a single student by marking it as
        a colleague of all participants in the course..

        If no student is given, update the status of all enrolled students.
        """

        update = self._update_friendship_status
        if student is None:
            for student in self.students.all():
                update(student)
        else:
            update(student)

    def _update_friendship_status(self, student):
        # Worker function for update_friendship_status
        colleague_status = FriendshipStatus.STATUS_COLLEAGUE
        for colleague in self.students.all():
            if colleague != student:
                try:
                    FriendshipStatus.objects.get(owner=student,
                                                 other=colleague)
                except FriendshipStatus.DoesNotExist:
                    FriendshipStatus.objects.create(owner=student,
                                                    other=colleague,
                                                    status=colleague_status)

    def get_absolute_url(self):
        return url_reverse('course-detail', args=(self.pk, ))

    def get_user_role(self, user):
        """Return a string describing the most privileged role the user
        as in the course. The possible values are:

        teacher:
            Owns the course and can do any kind of administrative tasks in
            the course.
        staff:
            Teacher assistants. May have some privileges granted by the teacher.
        student:
            Enrolled students.
        visitor:
            Have no relation to the course. If course is marked as public,
            visitors can access the course contents.
        """

        if user == self.teacher:
            return 'teacher'
        if user in self.staff.all():
            return 'staff'
        if user in self.students.all():
            return 'student'
        return 'visitor'

    def get_user_activities(self, user):
        """
        Return a sequence of all activities that are still open for the user.
        """

        activities = self.activities.filter(status=Activity.STATUS_OPEN)
        return activities.select_subclasses()

    def activity_duration(self):
        """
        Return the default duration (in minutes) for an activity starting from
        now.
        """

        return 120

    def next_time_slot(self):
        """Return the start and end times for the next class in the course.

        If a time slot is currently open, return it."""

        now = timezone.now()
        return now, now + timezone.timedelta(self.activity_duration())

    def next_date(self, date=None):
        """Return the date of the next available time slot."""

    def can_view(self, user):
        return user != annonymous_user()

    def can_edit(self, user):
        return user in self.teachers.all() or user == self.owner

    def get_context(self, request, *args, **kwargs):
        context = super().get_context(request, *args, **kwargs)
        context['activities'] = self.questions
        return context

    # Wagtail admin
    parent_page_types = ['cs_core.Discipline']
    subpage_types = []
    content_panels = Page.content_panels + [
        panels.InlinePanel(
            'time_slots',
            label=_('Time slots'),
            help_text=_('Define when the weekly classes take place.'),
        ),
    ]
    settings_panels = Page.settings_panels + [
        panels.MultiFieldPanel([
            panels.FieldPanel('weekly_lessons'),
            panels.FieldPanel('is_public'),
        ],
                               heading=_('Options')),
        panels.MultiFieldPanel([
            panels.FieldPanel('accept_subscriptions'),
            panels.FieldPanel('subscription_passphrase'),
        ],
                               heading=_('Subscription')),
    ]
Пример #10
0
class CodeExhibit(Activity):
    """
    Students can publish code + an associated image (think about turtle art or
    Processing) and then vote in the best submissions.
    """
    class Meta:
        verbose_name = _('Code exhibit')
        verbose_name_plural = _('Code exhibits')

    description = models.RichTextField()
    language = models.ForeignKey(
        'core.ProgrammingLanguage',
        on_delete=models.PROTECT,
    )

    def get_submit_form(self, *args, **kwargs):
        class ExhibitEntryForm(forms.ModelForm):
            class Meta:
                model = ExhibitEntry
                fields = ['name', 'image', 'source']

        return ExhibitEntryForm(*args, **kwargs)

    # Page rendering and views
    template = 'code_exhibit/code_exhibit.jinja2'

    def get_context(self, request, *args, **kwargs):
        context = super().get_context(request, *args, **kwargs)
        context['entries'] = self.entries.all()
        return context

    @bricks.route(r'^get-form/$')
    def route_submit_form(self, client):
        form = self.get_submit_form()
        context = {'form': form, 'language': self.language}
        html = render_to_string('code_exhibit/submit.jinja2',
                                context,
                                request=client.request)
        client.dialog(html=html)

    @models.route(r'^submit-entry/$')
    def route_submit(self, request):
        if request.method == 'POST':
            print(request.POST)
            form = self.get_submit_form(request.POST, request.FILES)
            if form.is_valid():
                instance = form.save(commit=False)
                print('saving model', instance)
                instance.user = request.user
                instance.exhibit = self
                instance.save()
            else:
                print(form.errors)
        return redirect(self.get_absolute_url())

    # Wagtail Admin
    content_panels = Activity.content_panels + [
        panels.FieldPanel('description'),
        panels.FieldPanel('language'),
        panels.InlinePanel('entries'),
    ]
Пример #11
0
class CodingIoQuestion(Question):
    """
    CodeIo questions evaluate source code and judge them by checking if the
    inputs and corresponding outputs match an expected pattern.
    """
    class Meta:
        verbose_name = _('input/output question')
        verbose_name_plural = _('input/output questions')

    iospec_size = models.PositiveIntegerField(
        _('number of iospec template expansions'),
        default=10,
        help_text=_(
            'The desired number of test cases that will be computed after '
            'comparing the iospec template with the answer key. This is only a '
            'suggested value and will only be applied if the response template '
            'uses input commands to generate random input.'),
    )
    iospec_source = models.TextField(
        _('response template'),
        help_text=_(
            'Template used to grade I/O responses. See '
            'http://pythonhosted.org/iospec for a complete reference on the '
            'template format.'),
    )
    iospec_hash = models.CharField(
        max_length=32,
        blank=True,
        help_text=_('A hash to keep track of iospec updates.'),
    )
    timeout = models.FloatField(
        _('timeout in seconds'),
        blank=True,
        default=1.0,
        help_text=_(
            'Defines the maximum runtime the grader will spend evaluating '
            'each test case.'),
    )
    is_usable = models.BooleanField(
        _('is usable'),
        help_text=_(
            'Tells if the question has at least one usable iospec entry. A '
            'complete iospec may be given from a single iospec source or by a '
            'combination of a valid source and a reference computer program.'))
    is_consistent = models.BooleanField(
        _('is consistent'),
        help_text=_(
            'Checks if all given answer keys are consistent with each other. '
            'The question might become inconsistent by the addition of an '
            'reference program that yields different results from the '
            'equivalent program in a different language.'))

    @lazy
    def iospec(self):
        """
        The IoSpec structure corresponding to the iospec_source.
        """

        return parse_iospec(self.iospec_source)

    @property
    def is_answer_key_complete(self):
        """
        Return True if an answer key item exists for all programming languages.
        """

        refs = self.is_answer_keys.values('language__ref', flatten=True)
        all_refs = ProgrammingLanguage.objects.values('ref', flatten=True)
        return set(all_refs) == set(refs)

    @bound_property
    def language(self):
        """
        Instances can be bound to a programming language.
        """

        return getattr(self, '_language_bind', None)

    @language.setter
    def language(self, value):
        self._language_bind = programming_language(value, raises=False)

    @property
    def is_language_bound(self):
        return self.language is not None

    @property
    def default_language(self):
        """
        The main language associated with this question if a single answer key
        is defined.
        """

        return self.answer_key_items.get().language

    def _language(self, language=None, raises=True):
        # Shortcut used internally to normalize the given language
        if language is None:
            return self.language or self.default_language
        return programming_language(language, raises)

    def __init__(self, *args, **kwargs):
        # Supports automatic conversion between iospec data and iospec_source
        iospec = kwargs.pop('iospec', None)
        if iospec:
            kwargs['iospec_source'] = iospec.source()
            self.iospec = iospec
        super().__init__(*args, **kwargs)

    def clean(self):
        """
        Validate the iospec_source field.
        """

        super().clean()

        # We first should check if the iospec_source has been changed and thus
        # requires a possibly expensive validation.
        source = self.iospec_source
        iospec_hash = md5hash(source)
        if self.iospec_hash != iospec_hash:
            try:
                self.iospec = iospec.parse_string(self.iospec_source)
            except Exception:
                raise ValidationError(
                    {'iospec_source': _('invalid iospec syntax')})
            else:
                self.iospec_hash = iospec_hash
                if self.pk is None:
                    self.is_usable = self.iospec.is_simple
                    self.is_consistent = True
                else:
                    self.is_usable = self._is_usable(self.iospec)
                    self.is_consistent = self._is_consistent(self.iospec)

    def _is_usable(self, iospec):
        """
        This function is triggered during the clean() validation when a new
        iospec data is inserted into the database.
        """

        # Simple iospecs are always valid since they can be compared with
        # arbitrary programs.
        if iospec.is_simple_io:
            return True

        # For now we reject all complex iospec structures
        return False

    def _is_consistent(self, iospec):
        """
        This function is triggered during the clean() validation when a new
        iospec data is inserted into the database.
        """

        # Simple iospecs always produce consistent answer keys since we prevent
        # invalid reference programs of being inserted into the database
        # during AnswerKeyItem validation.
        if iospec.is_simple_io:
            return True

        # For now we reject all complex iospec structures
        return False

    # Serialization methods: support markio and sets it as the default
    # serialization method for CodingIoQuestion's
    @classmethod
    def load_markio(cls, source):
        """
        Creates a CodingIoQuestion object from a Markio object or source
        string and saves the resulting question in the database.

        This function can run without touching the database if the markio file
        does not define any information that should be saved in an answer key.

        Args:
            source:
                A string with the Markio source code.

        Returns:
            question:
                A question object.
        """

        import markio

        if isinstance(source, markio.Markio):
            data = source
        else:
            data = markio.parse_string(source)

        # Create question object from parsed markio data
        question = CodingIoQuestion.objects.create(
            title=data.title,
            author_name=data.author,
            timeout=data.timeout,
            short_description=data.short_description,
            long_description=data.description,
            iospec_source=data.tests,
        )

        # Add answer keys
        answer_keys = {}
        for (lang, answer_key) in data.answer_key.items():
            language = programming_language(lang)
            key = question.answer_keys.create(language=language,
                                              source=answer_key)
            answer_keys[lang] = key
        for (lang, placeholder) in data.placeholder.items():
            if placeholder is None:
                continue
            try:
                answer_keys[lang].placeholder = placeholder
                answer_keys[lang].save(update_fields=['placeholder'])
            except KeyError:
                language = ProgrammingLanguage.objects.get(lang)
                question.answer_keys.create(language=language,
                                            placeholder=placeholder)
        return question

    @classmethod
    def load(cls, format='markio', **kwargs):
        return super().load(format=format, **kwargs)

    def dump_markio(self):
        """
        Serializes question into a string of Markio source.
        """

        import markio

        tree = markio.Markio(
            title=self.name,
            author=self.author_name,
            timeout=self.timeout,
            short_description=self.short_description,
            description=self.long_description,
            tests=self.iospec_source,
        )

        for key in self.answer_keys.all():
            tree.add_answer_key(key.source, key.language.ref)
            tree.add_placeholder(key.placeholder, key.language.ref)

        return tree.source()

    def answer_key_item(self, language=None):
        """
        Return the AnswerKeyItem instance for the requested language or None if
        no object is found.
        """

        language = self._language(language)
        try:
            return self.answer_key_items.get(language=language)
        except AnswerKeyItem.DoesNotExist:
            return None

    def answer_key(self, language=None):
        """
        Return the answer key IoSpec object associated with the given language.
        """

        key = self.answer_key_item(language)
        if key is None or key.iospec_source is None:
            new_key = self.answer_key_item()
            if key == new_key:
                if self.iospec.is_simple:
                    raise ValueError('no valid iospec is defined for the '
                                     'question')
                return iospec.expand_inputs(self.iospec_size)
            key = new_key

        # We check if the answer key item is synchronized with the parent hash
        if key.iospec_hash != key.parent_hash():
            try:
                key.update(self.iospec)
            except ValidationError:
                return self.iospec
        return key.iospec

    def placeholder(self, language=None):
        """
        Return the placeholder text for the given language.
        """

        if key is None:
            return ''
        return key.placeholder

    def reference_source(self, language=None):
        """
        Return the reference source code for the given language or None, if no
        reference is found.
        """

        key = self.answer_key_item(language)
        if key is None:
            return ''
        return key.source

    def run_code(self, source=None, iospec=None, language=None):
        """
        Run the given source code string for the programming language using the
        default IoSpec.

        If no code string is given, runs the reference source code, it it
        exists.
        """

        if language is None:
            language = self.answer_key_items.get().language
        key = self.answer_key_item(language)
        return key.run(source, iospec)

    def update_iospec_source(self):
        """
        Updates the iospec_source attribute with the current iospec object.

        Any modifications made to `self.iospec` must be saved explicitly to
        persist on the database.
        """

        if 'iospec' in self.__dict__:
            self.iospec_source = self.iospec.source()

    def register_response_item(self, source, language=None, **kwargs):
        response_data = {
            'language': self._language(language).ref,
            'source': source,
        }
        kwargs.update(response_data=response_data)
        return super().register_response_item(**kwargs)

    # Serving pages and routing
    @srvice.route(r'^submit-response/$')
    def respond_route(self, client, source=None, language=None, **kwargs):
        """
        Handles student responses via AJAX and a srvice program.
        """

        if not language:
            client.dialog('<p>Please select the correct language</p>')
            return

        # Bug with <ace-editor>?
        if not source or source == '\x01\x01':
            client.dialog('<p>Internal error: please send it again!</p>')
            return

        language = programming_language(language)
        self.bind(client.request, language=language, **kwargs)
        response = self.register_response_item(source, autograde=True)
        html = render_html(response.feedback)
        client.dialog(html)

    @srvice.route(r'^placeholder/$')
    def get_placeholder_route(self, request, language):
        """
        Return the placeholder code for some language.
        """

        return self.get_placehoder(language)

    def get_context(self, request, *args, **kwargs):
        context = super().get_context(request, *args, **kwargs)
        context['form'] = ResponseForm(request.POST)
        return context

    # Wagtail admin
    content_panels = Question.content_panels[:]
    content_panels.insert(
        -1,
        panels.MultiFieldPanel([
            panels.FieldPanel('iospec_size'),
            panels.FieldPanel('iospec_source'),
        ],
                               heading=_('IoSpec definitions')))
    content_panels.insert(
        -1, panels.InlinePanel('answer_key_items', label=_('Answer keys')))
Пример #12
0
class Quiz(Activity):
    """
    A quiz that may contain several different questions.
    """
    class Meta:
        verbose_name = _('quiz activity')
        verbose_name_plural = _('quiz activities')

    body = models.StreamField(
        QUESTION_STEM_BLOCKS,
        blank=True,
        null=True,
        verbose_name=_('Quiz description'),
        help_text=_(
            'This field should contain a text with any instructions, tips, or '
            'information that is relevant to the current quiz. Remember to '
            'explain clearly the rules and what is expected from each student.'
        ),
    )
    language = models.ForeignKey(
        ProgrammingLanguage,
        on_delete=models.SET_NULL,
        blank=True,
        null=True,
        related_name='quizzes',
        verbose_name=_('Programming language'),
        help_text=_(
            'Forces an specific programming language for all programing '
            'related questions. If not given, will accept responses in any '
            'programming language. This has no effect in non-programming '
            'activities.'),
    )

    # Derived attributes
    questions = property(lambda x: [i.question for i in x.quiz_items.all()])
    num_questions = property(lambda x: x.quiz_items.count())

    def add_question(self, question, weight=1.0):
        """
        Add a question to the quiz.
        """

        self.quiz_items.create(question=question, weight=weight)
        item = QuizItem(quiz=self, question=question)
        item.save()
        self.items.append(item)

    def register_response_item(self, *, user=None, context=None, **kwargs):
        """
        Return a response object for the given user.

        For now, users can only have one response.
        """

        # Silently ignore autograde
        kwargs.pop('autograde', None)

        # Quiz responses do not accept any extra parameters in the constructor
        if kwargs:
            param = kwargs.popitem()[0]
            raise TypeError('invalid parameter: %r' % param)

        # We force that quiz responses have a single response_item which is
        # only updated by the process_response_item() method.
        response = self.get_response(user, context)
        if response.items.count() != 0:
            return response.items.first()

        return super().register_response_item(user=user, context=context)

    def process_response_item(self, response, recycled=False):
        """
        Process a question response and adds a reference to it in the related
        QuizResponseItem.
        """

        # We do not register recycled responses
        if not recycled:
            user = response.user
            context = response.context
            quiz_response_item = self.register_response_item(user=user,
                                                             context=context)
            quiz_response_item.register_response(response)

    # Wagtail admin
    parent_page_types = ['cs_questions.QuizList']
    content_panels = Activity.content_panels + [
        panels.StreamFieldPanel('body'),
        panels.InlinePanel('quiz_items', label=_('Questions')),
    ]
    settings_panels = Activity.settings_panels + [
        panels.FieldPanel('language'),
    ]