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')) ]
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), ]
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")) ]
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.'), ), ]
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))
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'), ]
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')) ]
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')), ]
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')), ]
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'), ]
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')))
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'), ]