Exemplo n.º 1
0
class CustomFieldDefinition(models.Model):
    """
    Define a custom site-specific field for the user profile.
    """
    name = models.CharField(max_length=40)
    description = models.CharField(max_length=140)
    category = models.ForeignKey(CustomFieldCategory)
    enabled = models.BooleanField(
        _('enabled'),
        help_text=_('Enable or disable a custom field'),
        default=True,
    )
    type = models.CharField(default='text',
                            blank=True,
                            max_length=10,
                            choices=[('text', _('text')), ('int', _('int')),
                                     ('float', _('float')),
                                     ('date', _('date')),
                                     ('datetime', _('datetime'))])

    from_db_conversions = {
        'text': lambda x: x,
        'int': int,
        'float': float,
        'date': lambda x: datetime.date(*map(int, x.split('-'))),
        'datetime': lambda x: strptime("%Y-%m-%dT%H:%M:%S.%f%z", x),
    }

    to_db_conversions = {
        'date': lambda x: x.isoformat(),
        'datetime': lambda x: x.strftime("%Y-%m-%dT%H:%M:%S.%f%z"),
    }
Exemplo n.º 2
0
class SourceItem(models.ListItemModel):
    """A file item for the FileDownloadActivity."""
    class Meta:
        root_field = 'activity'

    activity = models.ForeignKey('SourceCodeActivity')
    format = models.ForeignKey(
        'cs_core.models.fileformat.FileFormat',
        verbose_name=_('format'),
        default='txt',
        help_text=_('The file format for the source code.'),
    )
    name = models.CharField(
        _('name'),
        max_length=140,
        help_text='A short description of the given source code fragment')
    description = models.TextField(
        _('description'),
        blank=True,
        help_text=_(
            'A detailed description of the source code fragment. This field '
            'accepts Markdown.'))
    source = models.TextField(_('source'),
                              help_text=_('The source code fragment.'))
    visible = models.BooleanField(
        _('is visible'),
        default=True,
        help_text=_(
            'Non-visible source items are available for download, but are not '
            'included in the main page'),
    )

    def __str__(self):
        return self.name
Exemplo n.º 3
0
class StringMatchQuestion(Question):
    answer = models.TextField()
    is_regex = models.BooleanField(default=True)

    def grade(self, response):
        if self.is_regex:
            value = response.value

        else:
            return super().grade(response)
Exemplo n.º 4
0
class FileFormat(models.Model):
    """
    Represents a source file format.

    These can be programming languages or some specific data format.
    """

    ref = models.CharField(max_length=10, unique=True)
    name = models.CharField(max_length=140)
    comments = models.TextField(blank=True)
    is_binary = models.BooleanField(default=False)
    is_language = models.BooleanField(default=False)
    is_supported = models.BooleanField(default=False)
    objects = models.Manager.from_queryset(SourceFormatQuerySet)()

    def __init__(self, *args, **kwargs):
        kwargs.setdefault('name', kwargs.get('ref', 'unnamed format').title())
        super().__init__(*args, **kwargs)

    def ace_mode(self):
        """
        Return the ace mode associated with the language.
        """

        return ACE_ALIASES.get(self.ref)

    def pygments_mode(self):
        """
        Returns the Pygments mode associated with the language.
        """

        return self.ref

    def ejudge_ref(self):
        """
        A valid reference for the language in the ejudge framework.
        """

        return self.ref

    def __str__(self):
        return self.name
Exemplo n.º 5
0
class StringMatchQuestion(Question):
    """
    The student response is compared with an answer_key string either by
    simple string comparison or using a regular expression.
    """

    answer_key = models.TextField()
    is_regex = models.BooleanField(default=True)

    def grade(self, response):
        if self.is_regex:
            value = response.value

        else:
            return super().grade(response)
Exemplo n.º 6
0
class FileDownloadActivity(Activity):
    """
    Students complete this activity by downloading all provided files from the
    server.

    This activity allows teachers to share files with the students.
    """
    class Meta:
        verbose_name = _('file download list')
        verbose_name_plural = _('file download activities')

    provide_compressed = models.BooleanField(default=True)
    zip_file = models.FileField(blank=True, null=True)
    targz_file = models.FileField(blank=True, null=True)
    items = models.ListItemSequence.as_items(FileItem)
    files = lazy(lambda x: [item.file for item in x.items])
Exemplo n.º 7
0
class CodingIoFeedback(QuestionFeedback):
    for_pre_test = models.BooleanField(
        _('Grading pre-test?'),
        default=False,
        help_text=_('True if its grading in the pre-test phase.'))
    json_feedback = models.JSONField(blank=True, null=True)
    feedback_status = property(lambda x: x.feedback.status)
    is_wrong_answer = delegate_to('feedback')
    is_presentation_error = delegate_to('feedback')
    is_timeout_error = delegate_to('feedback')
    is_build_error = delegate_to('feedback')
    is_runtime_error = delegate_to('feedback')

    @lazy
    def feedback(self):
        if self.json_feedback:
            return Feedback.from_json(self.json_feedback)
        else:
            return None

    def get_tests(self):
        """
        Return an iospec object with the tests for the current correction.
        """

        if self.for_pre_test:
            return self.question.get_expanded_pre_tests()
        else:
            return self.question.get_expand_post_tests()

    def get_autograde_value(self):
        tests = self.get_tests()
        source = self.submission.source
        language_ref = self.submission.language.ejudge_ref()
        feedback = grade_code(source,
                              tests,
                              lang=language_ref,
                              timeout=self.question.timeout)

        return feedback.grade * 100, {'json_feedback': feedback.to_json()}

    def render_message(self, **kwargs):
        return render(self.feedback)
Exemplo n.º 8
0
class LoginSettings(BaseSetting):
    username_as_school_id = models.BooleanField(
        default=False,
        help_text=_(
            'If true, force the username be equal to the school id for all '
            'student accounts.'))
    school_id_regex = models.TextField(
        default='',
        blank=True,
        help_text=_(
            'A regular expression for matching valid school ids. If blank, no'
            'check will be performed on the validity of the given school ids'),
    )
    panels = [
        panels.MultiFieldPanel([
            panels.FieldPanel('username_as_school_id'),
            panels.FieldPanel('school_id_regex'),
        ],
                               heading=_('School id configuration'))
    ]
Exemplo n.º 9
0
class AttendanceCheck(models.Model):
    """
    Confirms attendance by an user.
    """

    user = models.ForeignKey(models.User)
    event = models.ForeignKey(Event)
    has_attended = models.BooleanField(default=bool)
    attempts = models.SmallIntegerField(default=int)

    def update(self, phrase):
        """
        Update check with the given passphrase.
        """

        sheet = self.event.sheet
        if self.attempts > sheet.max_attempts:
            return
        if string_distance(phrase,
                           self.event.passphrase) <= sheet.max_string_distance:
            self.has_attended = True
        self.attempts += 1
        self.save()
Exemplo n.º 10
0
class BooleanQuestion(Question):
    """
    A question with a single boolean answer.
    """

    answer_key = models.BooleanField()
Exemplo n.º 11
0
class Course(models.DateFramedModel, models.TimeStampedModel):
    """One specific occurrence of a course for a given teacher in a given
    period."""

    discipline = models.ForeignKey(Discipline, related_name='courses')
    teacher = models.ForeignKey(models.User, related_name='owned_courses')
    students = models.ManyToManyField(
        models.User,
        related_name='enrolled_courses',
        blank=True,
    )
    staff = models.ManyToManyField(
        models.User,
        related_name='courses_as_staff',
        blank=True,
    )
    current_lesson_index = models.PositiveIntegerField(default=0, blank=True)
    current_lesson_start = models.DateField(blank=True, null=True)
    is_active = models.BooleanField(_('is active'), default=False)
    objects = CourseQueryset.as_manager()

    # Discipline properties
    name = property(lambda x: x.discipline.name)
    short_description = property(lambda x: x.discipline.short_description)
    long_description = property(lambda x: x.discipline.long_description)
    short_description_html = property(
        lambda x: x.discipline.short_description_html)
    long_description_html = property(
        lambda x: x.discipline.long_description_html)

    # Other properties
    owner = property(lambda x: x.teacher)

    def __str__(self):
        return '%s (%s)' % (self.discipline.name, self.teacher.first_name)

    def to_file(self):
        """Serialize object in a Markdown format."""

    @classmethod
    def from_file(self, file):
        """Load course from file."""

    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)

    # Managers
    @property
    def past_activities(self):
        return (self.activities.filter(status=Activity.STATUS_CLOSED)
                | self.activities.filter(
                    end__lt=timezone.now())).select_subclasses()

    @property
    def open_activities(self):
        return (self.activities.timeframed.all() & self.activities.filter(
            status=Activity.STATUS_OPEN)).select_subclasses()

    @property
    def pending_activities(self):
        return (self.activities.filter(status=Activity.STATUS_DRAFT) |
                (self.activities.filter(status=Activity.STATUS_OPEN)
                 & self.activities.filter(end__lt=timezone.now()))
                ).select_subclasses()

    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 priviledged 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 True

    def can_edit(self, user):
        return user == self.teacher
Exemplo n.º 12
0
class Progress(models.CopyMixin, models.StatusModel, models.TimeStampedModel,
               models.PolymorphicModel):
    """
    When an user starts an activity it opens a Progress object which control
    all submissions to the given activity.

    The Progress object also manages individual submissions that may span
    several http requests.
    """
    class Meta:
        unique_together = [('user', 'activity_page')]
        verbose_name = _('student progress')
        verbose_name_plural = _('student progress list')

    STATUS_OPENED = 'opened'
    STATUS_CLOSED = 'closed'
    STATUS_INCOMPLETE = 'incomplete'
    STATUS_WAITING = 'waiting'
    STATUS_INVALID = 'invalid'
    STATUS_DONE = 'done'

    STATUS = models.Choices(
        (STATUS_OPENED, _('opened')),
        (STATUS_CLOSED, _('closed')),
    )

    user = models.ForeignKey(models.User, on_delete=models.CASCADE)
    activity_page = models.ForeignKey(models.Page, on_delete=models.CASCADE)
    final_grade_pc = models.DecimalField(
        _('final score'),
        max_digits=6,
        decimal_places=3,
        default=Decimal,
        help_text=_(
            'Final grade given to considering all submissions, penalties, etc.'
        ),
    )
    given_grade_pc = models.DecimalField(
        _('grade'),
        max_digits=6,
        decimal_places=3,
        default=Decimal,
        help_text=_('Final grade before applying any modifier.'),
    )
    finished = models.DateTimeField(blank=True, null=True)
    best_submission = models.ForeignKey('Submission',
                                        blank=True,
                                        null=True,
                                        related_name='+')
    points = models.IntegerField(default=0)
    score = models.IntegerField(default=0)
    stars = models.FloatField(default=0.0)
    is_correct = models.BooleanField(default=bool)
    has_submissions = models.BooleanField(default=bool)
    has_feedback = models.BooleanField(default=bool)
    has_post_tests = models.BooleanField(default=bool)
    objects = ProgressManager()

    #: The number of submissions
    num_submissions = property(lambda x: x.submissions.count())

    #: Specific activity reference
    activity = property(lambda x: x.activity_page.specific)
    activity_id = property(lambda x: x.activity_page_id)

    #: Has progress mixin interface
    username = property(lambda x: x.user.username)

    def __repr__(self):
        return '<%s: %s>' % (self.__class__.__name__, self)

    def __str__(self):
        tries = self.num_submissions
        user = self.user
        activity = self.activity
        grade = '%s pts' % (self.final_grade_pc or 0)
        fmt = '%s by %s (%s, %s tries)'
        return fmt % (activity, user, grade, tries)

    def __hash__(self):
        return hash(self.id)

    def __eq__(self, other):
        if isinstance(other, Progress):
            if self.pk is None:
                return False
            else:
                return self.pk == other.pk
        return NotImplemented

    def submit(self, request, recycle=True, **kwargs):
        """
        Creates new submission.
        """

        submission_class = self.activity.submission_class
        submission = submission_class(progress=self, **kwargs)
        submission.ip_address = get_ip(request)

        if not recycle:
            submission.save()
            return submission

        # Collect all submissions with the same hash as current one
        recyclable = submission_class.objects\
            .filter(progress=self, hash=submission.compute_hash()) \
            .order_by('created')

        # Then check if any submission is actually equal to the current amongst
        # all candidates
        for possibly_equal in recyclable:
            if submission.is_equal(possibly_equal):
                possibly_equal.recycled = True
                possibly_equal.bump_recycles()
                return possibly_equal
        else:
            submission.save()
            return submission

    def register_feedback(self, feedback):
        """
        This method is called after a submission is graded and produces a
        feedback.
        """

        submission = feedback.submission
        self.update_grades_from_feedback(feedback)

        if not self.activity.has_submissions:
            print('first submission')
            if feedback.is_correct:
                print('first correct submission')

    def update_grades_from_feedback(self, feedback):
        """
        Update grades from the current progress object from the given feedback.
        """

        # Update grades
        if self.given_grade_pc < (feedback.given_grade_pc or 0):
            self.given_grade_pc = feedback.given_grade_pc

        # TODO: decide better update strategy
        if self.final_grade_pc < feedback.final_grade_pc:
            self.final_grade_pc = feedback.final_grade_pc

        # # Register points and stars associated with submission.
        # score_kwargs = {}
        # final_points = feedback.final_points()
        # final_stars = feedback.final_stars()
        # if final_points > self.points:
        #     score_kwargs['points'] = final_points - self.points
        #     self.points = final_points
        # if final_stars > self.stars:
        #     score_kwargs['stars'] = final_stars - self.stars
        #     self.stars = final_stars
        #
        # # If some score has changed, we save the update fields and update the
        # # corresponding UserScore object
        # if score_kwargs:
        #     from codeschool.gamification.models import UserScore
        #     self.save(update_fields=score_kwargs.keys())
        #     score_kwargs['diff'] = True
        #     UserScore.update(self.user, self.activity_page, **score_kwargs)

        # Update the is_correct field
        self.is_correct = self.is_correct or feedback.is_correct
        self.save()

    def update_from_submissions(self,
                                grades=True,
                                score=True,
                                commit=True,
                                refresh=False):
        """
        Update grades and gamification scores for all submissions.

        Args:
            grades, score (bool):
                choose to update final grades and/or final scores.
            commit:
                if True (default), save changes to database.
            refresh:
                if True (default), recompute grade from scratch.
        """

        submissions = self.submissions.all()
        if refresh and submissions.count():
            first = submissions.first()
            if grades:
                self.final_grade_pc = first.given_grade_pc
                self.given_grade_pc = first.given_grade_pc
            if score:
                self.points = first.points
                self.stars = first.stars
                self.score = first.score

        for submission in submissions:
            if grades:
                submission.update_response_grades(commit=False)
            if score:
                submission.update_response_score(commit=False)

        if commit:
            self.save()

    def regrade(self, method=None, force_update=False):
        """
        Return the final grade for the user using the given method.

        If not method is given, it uses the default grading method for the
        activity.
        """

        activity = self.activity

        # Choose grading method
        if method is None and self.final_grade_pc is not None:
            return self.final_grade_pc
        elif method is None:
            grading_method = activity.grading_method
        else:
            grading_method = GradingMethod.from_name(activity.owner, method)

        # Grade response. We save the result to the final_grade_pc attribute if
        # no explicit grading method is given.
        grade = grading_method.grade(self)
        if method is None and (force_update or self.final_grade_pc is None):
            self.final_grade_pc = grade
        return grade
Exemplo n.º 13
0
class Classroom(models.TimeStampedModel, models.DecoupledAdminPage,
                models.RoutablePageExt):
    """
    One specific occurrence of a course for a given teacher in a given period.
    """

    discipline = models.ForeignKey('academic.Discipline',
                                   blank=True,
                                   null=True,
                                   on_delete=models.DO_NOTHING)
    teacher = models.ForeignKey(models.User,
                                related_name='classrooms_as_teacher',
                                on_delete=models.PROTECT)
    students = models.ManyToManyField(
        models.User,
        related_name='classrooms_as_student',
        blank=True,
    )
    staff = models.ManyToManyField(
        models.User,
        related_name='classrooms_as_staff',
        blank=True,
    )
    weekly_lessons = models.BooleanField(
        _('weekly lessons'),
        default=False,
        help_text=_(
            'If true, the lesson spans a whole week. Otherwise, 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)
    description = models.RichTextField()
    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)

    objects = ClassroomManager()

    def save(self, *args, **kwargs):
        self.teacher = self.teacher or self.owner
        super().save(*args, **kwargs)

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

        if student == self.teacher:
            raise ValidationError(_('Teacher cannot enroll as student.'))
        elif student in self.staff.all():
            raise ValidationError(_('Staff member cannot enroll as student.'))
        self.students.add(student)
Exemplo n.º 14
0
class Course(models.DateFramedModel, models.TimeStampedModel):
    """One specific occurrence of a course for a given teacher in a given
    period."""

    # Fields
    discipline = models.ForeignKey(
        Discipline,
        related_name='courses'
    )
    teacher = models.ForeignKey(
        models.User,
        related_name='owned_courses'
    )
    students = models.ManyToManyField(
        models.User,
        related_name='enrolled_courses',
        blank=True,
    )
    staff = models.ManyToManyField(
        models.User,
        related_name='courses_as_staff',
        blank=True,
    )
    current_lesson_index = models.PositiveIntegerField(default=0, blank=True)
    current_lesson_start = models.DateField(blank=True, null=True)
    is_active = models.BooleanField(_('is active'), default=False)

    # Managers
    @property
    def past_activities(self):
        return (
            self.activities.filter(status=Activity.STATUS.concluded) |
            self.activities.filter(end__lt=timezone.now())
        ).select_subclasses()

    @property
    def open_activities(self):
        return (self.activities.timeframed.all() &
                self.activities.filter(status=Activity.STATUS.open)).select_subclasses()

    @property
    def pending_activities(self):
        return (self.activities.filter(status=Activity.STATUS.draft) |
                (self.activities.filter(status=Activity.STATUS.open) &
                 self.activities.filter(end__lt=timezone.now()))).select_subclasses()

    name = property(lambda s: s.discipline.name)
    short_description = property(lambda s: s.discipline.short_description)
    long_description = property(lambda s: s.discipline.long_description)

    def __str__(self):
        return '%s (%s)' % (self.discipline.name, self.teacher.first_name)

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

    def user_activities(self, user):
        """Return a list of all activities that are valid for the given user"""

        return self.activities.select_subclasses()

    def activity_duration(self):
        """Return the default duration 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."""
Exemplo n.º 15
0
class CodingIoAnswerKey(models.Model):
    """Represents an answer to some question given in some specific computer
    language plus the placeholder text that should be displayed"""

    class ValidationError(Exception):
        pass

    class Meta:
        app_label = 'cs_questions'
        verbose_name = _('answer key')
        verbose_name_plural = _('answer keys')
        unique_together = [('question', 'language')]

    question = models.ForeignKey(CodingIoQuestion, related_name='answer_keys')
    language = models.ForeignKey(ProgrammingLanguage)
    source = models.TextField(
            _('Answer source code'),
            blank=True,
            help_text=_('Source code for the correct answer in the given '
                        'programming language'),
    )
    placeholder = models.TextField(
            _('Placeholder source code'),
            blank=True,
            help_text=_('This optional field controls which code should be '
                        'placed in the source code editor when a question is '
                        'opened. This is useful to put boilerplate or even a '
                        'full program that the student should modify. It is '
                        'possible to configure a global per-language '
                        'boilerplate and leave this field blank.'),
    )
    is_valid = models.BooleanField(default=False)
    iospec_hash = models.CharField(max_length=32, blank=True)
    iospec_source = models.TextField(blank=True)

    def __str__(self):
        return 'AnswerKey(%s, %s)' % (self.question, self.language)

    def save(self, *args, **kwds):
        if 'iospec' in self.__dict__:
            self.iospec_source = self.iospec.source()
        super().save(*args, **kwds)

    @lazy
    def iospec(self):
        return parse_iospec(self.iospec_source)

    @property
    def is_update(self):
        return self.iospec_hash == self.compute_iospec_hash()

    @property
    def status(self):
        if not self.is_valid:
            return 'invalid'
        elif not self.is_update:
            return 'pending'
        else:
            return 'valid'

    def assure_is_valid(self, error=None):
        """Raises an error if key is invalid or cannot be updated."""

        if not self.is_update:
            self.update()
        if not self.is_valid:
            raise error or self.ValidationError

    def compute_iospec_hash(self):
        """
        Return the iospec hash from the question current iospec/iospec_size.
        """

        return self.question.hash

    def update(self, save=True, validate=True):
        """
        Force update of answer key by running the source code against the most
        recent iospec template in the parent question.

        This method saves the result in the database unless save=False.
        """

        self.iospec_hash = self.compute_iospec_hash()

        if self.source:
            iospec_data = self.question.iospec.copy()
            iospec_data.expand_inputs(self.question.iospec_size)
            source = self.source
            lang = self.language.ref
            self.iospec = run_code(source, iospec_data, lang=lang)
            self.is_valid = not self.iospec.has_errors()
        else:
            self.iospec_source = ''
            self.is_valid = True

        if save:
            self.save()

        if validate and self.is_valid is False:
            raise self.ValidationError('could not validate Answer key')
Exemplo n.º 16
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')),
    ]
Exemplo n.º 17
0
class ResponseItem(models.CopyMixin,
                   models.TimeStampedStatusModel,
                   models.PolymorphicModel):
    """
    Represents a student's response to some activity.

    Response objects have 4 different states:

    pending:
        The response has been sent, but was not graded. Grading can be manual or
        automatic, depending on the activity.
    waiting:
        Waiting for manual feedback.
    incomplete:
        For long-term activities, this tells that the student started a response
        and is completing it gradually, but the final response was not achieved
        yet.
    invalid:
        The response has been sent, but contains malformed data.
    done:
        The response was graded and evaluated and it initialized a feedback
        object.

    A response always starts at pending status. We can request it to be graded
    by calling the :func:`Response.autograde` method. This method must raise
    an InvalidResponseError if the response is invalid or ManualGradingError if
    the response subclass does not implement automatic grading.
    """

    class Meta:
        verbose_name = _('response')
        verbose_name_plural = _('responses')

    STATUS_PENDING = 'pending'
    STATUS_INCOMPLETE = 'incomplete'
    STATUS_WAITING = 'waiting'
    STATUS_INVALID = 'invalid'
    STATUS_DONE = 'done'
    STATUS = models.Choices(
        (STATUS_PENDING, _('pending')),
        (STATUS_INCOMPLETE, _('incomplete')),
        (STATUS_WAITING, _('waiting')),
        (STATUS_INVALID, _('invalid')),
        (STATUS_DONE, _('done')),
    )

    response = models.ParentalKey(
        'Response',
        verbose_name=_('response'),
        related_name='items',
    )
    feedback_data = models.JSONField(
        null=True,
        blank=True,
    )
    response_data = models.JSONField(
        null=True,
        blank=True,
    )
    response_hash = models.CharField(
        max_length=32,
        blank=True,
    )
    given_grade = models.DecimalField(
        _('Percentage of maximum grade'),
        help_text=_(
            'This grade is given by the auto-grader and represents the grade '
            'for the response before accounting for any bonuses or penalties.'),
        max_digits=6,
        decimal_places=3,
        blank=True,
        null=True,
    )
    final_grade = models.DecimalField(
        _('Final grade'),
        help_text=_(
            'Similar to given_grade, but can account for additional factors '
            'such as delay penalties or for any other reason the teacher may '
            'want to override the student\'s grade.'),
        max_digits=6,
        decimal_places=3,
        blank=True,
        null=True,
    )
    manual_override = models.BooleanField(
        default=False
    )

    # Status properties
    is_done = property(lambda x: x.status == x.STATUS_DONE)
    is_pending = property(lambda x: x.status == x.STATUS_PENDING)
    is_waiting = property(lambda x: x.status == x.STATUS_WAITING)
    is_invalid = property(lambda x: x.status == x.STATUS_INVALID)

    # Delegate properties
    activity = property(lambda x: x.response.activity.specific)
    user = property(lambda x: x.response.user)
    context = property(lambda x: x.response.context)
    course = property(lambda x: x.activity.course)

    def __init__(self, *args, **kwargs):
        # Django is loading object from the database -- we step out the way
        if args and not kwargs:
            super().__init__(*args, **kwargs)
            return

        # We create the response_data and feedback_data manually always using
        # copies of passed dicts. We save these variables here, init object and
        # then copy this data to the initialized dictionaries
        response_data = kwargs.pop('response_data', None) or {}
        feedback_data = kwargs.pop('feedback_data', None) or {}

        # This part makes a ResponseItem instance initialize from a user +
        # activity + context instead of requiring a response object. The
        # response is automatically created on demand.
        user = kwargs.pop('user', None)
        if user:
            context = kwargs.pop('context', None)
            try:
                activity = kwargs.pop('activity')
            except KeyError:
                raise TypeError(
                    'ReponseItem objects bound to a user must also provide an '
                    'activity parameter.'
                )

            # User-bound constructor tries to obtain the Response object by
            # searching for an specific (user, context, activity) tuple.
            response, created = Response.objects.get_or_create(
                user=user,
                context=context,
                activity=activity
            )
            kwargs['response'] = response

        if 'context' in kwargs or 'activity' in kwargs:
            raise TypeError(
                'Must provide an user to instantiate a bound response item.'
            )
        super().__init__(*args, **kwargs)

        # Now that we have initialized the response item, we fill the data
        # passed in the response_data and feedback_data dictionaries.
        self.response_data = dict(self.response_data or {}, **response_data)
        self.feedback_data = dict(self.response_data or {}, **feedback_data)

    def __str__(self):
        if self.given_grade is None:
            grade = self.status
        else:
            grade = '%s pts' % self.final_grade
        user = self.user
        activity = self.activity
        return '<ResponseItem: %s by %s (%s)>' % (activity, user, grade)

    def save(self, *args, **kwargs):
        if not self.response_hash:
            self.response_hash = self.get_response_hash(self.response_hash)
        super().save(*args, **kwargs)

    def get_feedback_data(self, commit=True):
        """Return the feedback object associated to the given response.

        This method may trigger the autograde() method, if grading was not
        performed yet. If you want to defer database access, call it with
        commit=False to prevent saving any modifications to the response object
        to the database.
        """

        if self.status == self.STATUS_PENDING:
            self.autograde(commit)
        elif self.status == self.STATUS_INVALID:
            raise self.feedback_data
        elif self.status == self.STATUS_WAITING:
            return None
        return self.feedback_data

    def autograde(self, commit=True, force=False, silent=False):
        """
        Performs automatic grading.

        Response subclasses must implement the autograde_compute() method in
        order to make automatic grading work. This method may write any
        relevant information to the `feedback_data` attribute and must return
        a numeric value from 0 to 100 with the given automatic grade.

        Args:
            commit:
                If false, prevents saving the object when grading is complete.
                The user must save the object manually after calling this
                method.
            force:
                If true, force regrading the item even if it has already been
                graded.
            silent:
                Prevents the autograde_signal from triggering in the end of
                a successful autograde.
        """

        if self.status == self.STATUS_PENDING or force:
            try:
                value = self.autograde_compute()
            except self.InvalidResponseError as ex:
                self.status = self.STATUS_INVALID
                self.feedback_data = ex
                self.given_grade = self.final_grade = decimal.Decimal(0)
                if commit:
                    self.save()
                raise

            if value is None:
                self.status = self.STATUS_WAITING
            else:
                self.given_grade = decimal.Decimal(value)
                if self.final_grade is None:
                    self.final_grade = self.given_grade
                self.status = self.STATUS_DONE
                if not silent:
                    autograde_signal.send(
                        self.__class__,
                        response_item=self,
                        given_grade=self.given_grade
                    )
            if commit and self.pk:
                self.save(update_fields=['status', 'feedback_data',
                                         'given_grade', 'final_grade'])
            elif commit:
                self.save()

        elif self.status == self.STATUS_INVALID:
            raise self.feedback_data

    def autograde_compute(self):
        """This method should be implemented in subclasses."""

        raise ImproperlyConfigured(
            'Response subclass %r must implement the autograde_compute().'
            'This method should perform the automatic grading and return the '
            'resulting grade. Any additional relevant feedback data might be '
            'saved to the `feedback_data` attribute, which is then is pickled '
            'and saved into the database.' % type(self).__name__
        )

    def regrade(self, method, commit=True):
        """
        Recompute the grade for the given response item.

        If status != 'done', it simply calls the .autograde() method. Otherwise,
        it accept different strategies for updating to the new grades:
            'update':
                Recompute the grades and replace the old values with the new
                ones. Only saves the response item if the feedback_data or the
                given_grade attributes change.
            'best':
                Only update if the if the grade increase.
            'worst':
                Only update if the grades decrease.
            'best-feedback':
                Like 'best', but updates feedback_data even if the grades
                change.
            'worst-feedback':
                Like 'worst', but updates feedback_data even if the grades
                change.

        Return a boolean telling if the regrading was necessary.
        """
        if self.status != self.STATUS_DONE:
            return self.autograde()

        # We keep a copy of the state, if necessary. We only have to take some
        # action if the state changes.
        def rollback():
            self.__dict__.clear()
            self.__dict__.update(state)

        state = self.__dict__.copy()
        self.autograde(force=True, commit=False)

        # Each method deals with the new state in a different manner
        if method == 'update':
            if state != self.__dict__:
                if commit:
                    self.save()
                return False
            return True
        elif method in ('best', 'best-feedback'):
            if self.given_grade <= state.get('given_grade', 0):
                new_feedback_data = self.feedback_data
                rollback()
                if new_feedback_data != self.feedback_data:
                    self.feedback_data = new_feedback_data
                    if commit:
                        self.save()
                    return True
                return False
            elif commit:
                self.save()
            return True

        elif method in ('worst', 'worst-feedback'):
            if self.given_grade >= state.get('given_grade', 0):
                new_feedback_data = self.feedback_data
                rollback()
                if new_feedback_data != self.feedback_data:
                    self.feedback_data = new_feedback_data
                    if commit:
                        self.save()
                    return True
                return False
            elif commit:
                self.save()
            return True
        else:
            rollback()
            raise ValueError('invalid method: %s' % method)

    @classmethod
    def get_response_hash(cls, response_data):
        """
        Computes a hash for the response_data attribute.
        """

        if response_data:
            data = json.dumps(response_data, default=json_default)
            return md5hash(data)
        return ''

    # Feedback and visualization
    ok_message = _('*Congratulations!* Your response is correct!')
    ok_with_penalties = _('Your response is correct, but you did not achieved '
                          'the maximum grade.')
    wrong_message = _('I\'m sorry, your response is wrong.')
    partial_message = _('Your answer is partially correct: you achieved only '
                        '%(grade)d%% of the total grade.')

    def html_feedback(self):
        """
        A string of html source representing the feedback.
        """

        if self.is_done:
            data = {'grade': (self.final_grade or 0)}

            if self.final_grade == 100:
                return markdown(self.ok_message)
            elif self.given_grade == 100:
                return markdown(self.ok_with_penalties_message)
            elif not self.given_grade:
                return markdown(self.wrong_message)
            else:
                return markdown(self.partial_message % data)
        else:
            return markdown(_('Your response has not been graded yet!'))

    # Permissions
    def can_edit(self, user):
        return False

    def can_view(self, user):
        return user == self.user
Exemplo n.º 18
0
class Response(models.CopyMixin, models.InheritableModel,
               models.TimeStampedStatusModel):
    """
    Represents a student's response to some activity.

    Response objects have 4 different states:

    pending:
        The response has been sent, but was not graded. Grading can be manual or
        automatic, depending on the activity.
    waiting:
        Waiting for manual feedback.
    invalid:
        The response has been sent, but contains malformed data.
    done:
        The response was graded and evaluated and it initialized a feedback
        object.

    A response always starts at pending status. We can request it to be graded
    by calling the :func:`Response.autograde` method. This method must raise
    an InvalidResponseError if the response is invalid or ManualGradingError if
    the response subclass does not implement automatic grading.
    """
    class Meta:
        verbose_name = _('response')
        verbose_name_plural = _('responses')

    STATUS_PENDING = 'pending'
    STATUS_WAITING = 'waiting'
    STATUS_INVALID = 'invalid'
    STATUS_DONE = 'done'
    STATUS = models.Choices(
        (STATUS_PENDING, _('pending')),
        (STATUS_WAITING, _('waiting')),
        (STATUS_INVALID, _('invalid')),
        (STATUS_DONE, _('done')),
    )
    activity = models.ForeignKey(
        Activity,
        blank=True,
        null=True,
        related_name='responses',
        on_delete=models.CASCADE,
    )
    user = models.ForeignKey(
        models.User,
        blank=True,
        null=True,
    )
    feedback_data = models.PickledObjectField(blank=True, null=True)
    given_grade = models.DecimalField(
        _('Percentage of maximum grade'),
        help_text=_(
            'This grade is given by the auto-grader and represents the grade '
            'for the response before accounting for any bonuses or penalties.'
        ),
        max_digits=6,
        decimal_places=3,
        blank=True,
        null=True,
    )
    final_grade = models.DecimalField(
        _('Final grade'),
        help_text=_(
            'Similar to given_grade, but can account for additional factors '
            'such as delay penalties or for any other reason the teacher may '
            'want to override the student\'s grade.'),
        max_digits=6,
        decimal_places=3,
        blank=True,
        null=True,
    )
    manual_override = models.BooleanField(default=False)
    parent = models.ForeignKey(
        'self',
        blank=True,
        null=True,
        on_delete=models.SET_NULL,
        related_name='children',
    )
    is_converted = models.BooleanField(default=False)

    # Status properties
    is_done = property(lambda x: x.status == x.STATUS_DONE)
    is_pending = property(lambda x: x.status == x.STATUS_PENDING)
    is_waiting = property(lambda x: x.status == x.STATUS_WAITING)
    is_invalid = property(lambda x: x.status == x.STATUS_INVALID)

    # Delegate properties
    course = property(lambda x: getattr(x.activity, 'course', None))

    # Other properties
    @property
    def grade(self):
        if self.final_grade is None:
            return self.given_grade or decimal.Decimal(0)
        else:
            return self.final_grade

    grade.setter(lambda x, v: setattr(x, 'final_grade', v))

    class InvalidResponseError(Exception):
        """Raised by compute_response() when the response is invalid."""

    # Compute grades
    def get_response_group(self, user):
        """Return the response group associated to this response."""

    def get_feedback(self, commit=True):
        """Return the feedback object associated to the given response.

        This method may trigger the autograde() method, if grading was not
        performed yet. If you want to defer database access, call it with
        commit=False to prevent saving any modifications to the response object
        to the database.
        """

        if self.status == self.STATUS_PENDING:
            self.autograde(commit)
        elif self.status == self.STATUS_INVALID:
            raise self.feedback_data
        elif self.status == self.STATUS_WAITING:
            return None
        return self.feedback_data

    def autograde(self, commit=True, force=False):
        """
        Performs automatic grading.

        Response subclasses must implement the autograde_compute() method in
        order to make automatic grading work. This method may write any
        relevant information to the `feedback_data` attribute and must return
        a numeric value from 0 to 100 with the given automatic grade.
        """

        if self.status == self.STATUS_PENDING or force:
            try:
                value = self.autograde_compute()
            except self.InvalidResponseError as ex:
                self.status = self.STATUS_INVALID
                self.feedback_data = ex
                self.given_grade = self.final_grade = decimal.Decimal(0)
                if commit:
                    self.save()
                raise

            if value is None:
                self.status = self.STATUS_WAITING
            else:
                self.given_grade = decimal.Decimal(value)
                self.final_grade = self.given_grade
                self.status = self.STATUS_DONE
            if commit and self.pk:
                self.save(update_fields=[
                    'status', 'feedback_data', 'given_grade', 'final_grade'
                ])
            elif commit:
                self.save()

        elif self.status == self.STATUS_INVALID:
            raise self.feedback_data

    def autograde_compute(self):
        """This method should be implemented in subclasses."""

        raise ImproperlyConfigured(
            'Response subclass %r must implement the autograde_compute().'
            'This method should perform the automatic grading and return the '
            'resulting grade. Any additional relevant feedback data might be '
            'saved to the `feedback_data` attribute, which is then is pickled '
            'and saved into the database.' % type(self).__name__)

    def __str__(self):
        return '%s(%s)' % (type(self).__name__, self.status)

    # Feedback and visualization
    ok_message = _('*Congratulations!* Your response is correct!')
    wrong_message = _('I\'m sorry, your response is wrong.')
    partial_message = _('Your answer is partially correct: you achieved only '
                        '%(grade)d%% of the total grade.')

    def html_feedback(self):
        """
        A string of html source representing the feedback.
        """

        if self.is_done:
            data = {'grade': (self.grade or 0)}

            if self.grade == 100:
                return markdown(self.ok_message)
            elif not self.grade:
                return markdown(self.wrong_message)
            else:
                return markdown(aself.partial_message % data)
        else:
            return markdown(_('Your response has not been graded yet!'))

    # Permissions
    def can_edit(self, user):
        return False

    def can_view(self, user):
        return user == self.user
Exemplo n.º 19
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)')

    num_pre_tests = models.PositiveIntegerField(
        _('# of pre-test examples'),
        default=3,
        validators=[validators.positive_integer_validator],
        help_text=_(
            'The desired number of test cases that will be computed after '
            'comparing the iospec template with the correct answer. This is '
            'only a suggested value and will only be applied if the response '
            'template uses input commands to generate random input.'),
    )
    pre_tests_source = models.TextField(
        _('response template'),
        blank=True,
        validators=[validators.iospec_source_validator],
        help_text=_(
            'Template used to grade I/O responses. See '
            'http://pythonhosted.org/iospec for a complete reference on the '
            'template format.'),
    )
    num_post_tests = models.PositiveIntegerField(
        _('# of post-test examples'),
        validators=[validators.positive_integer_validator],
        default=20)
    post_tests_source = models.TextField(
        _('response template (post evaluation)'),
        validators=[validators.iospec_source_validator],
        blank=True,
        help_text=_(
            'These tests are used only in a second round of corrections and is '
            'not immediately shown to users.'),
    )
    test_state_hash = models.CharField(
        max_length=32,
        blank=True,
        help_text=_('A hash to keep track of iospec sources updates.'),
    )
    timeout = models.FloatField(
        _('timeout in seconds'),
        validators=[validators.timeout_validator],
        blank=True,
        default=1.0,
        help_text=_(
            'Defines the maximum runtime the grader will spend evaluating '
            'each test case.'),
    )
    default_placeholder = models.TextField(
        _('placeholder'),
        blank=True,
        help_text=_('Default placeholder message that is used if it is not '
                    'defined for the given language. This will appear as a '
                    'block of comment in the beginning of the submission.'))
    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 for specific programming languages.'),
    )

    # Fields for storing the results of an async post-validation.
    error_field = models.CharField(max_length=20, blank=True)
    error_message = models.TextField(blank=True)
    ignore_programming_errors = models.BooleanField(
        default=False,
        help_text=_(
            'Mark this if you want to ignore programming errors this time. It '
            'will ignore errors once, but you still have to fix the source '
            'of those errors to make the question become operational.'))

    __answers = ()
    _iospec_expansion_is_dirty = False

    @property
    def pre_tests(self):
        try:
            return self._pre_tests
        except AttributeError:
            self._pre_tests = parse_iospec(self.pre_tests_source)
            return self._pre_tests

    @pre_tests.setter
    def pre_tests(self, value):
        self._pre_tests = value
        self.pre_tests_source = value.source()

    @pre_tests.deleter
    def pre_tests(self):
        try:
            del self._pre_tests
        except AttributeError:
            pass

    @property
    def post_tests(self):
        try:
            return self._post_tests
        except AttributeError:
            if self.post_tests_source:
                post_tests = parse_iospec(self.post_tests_source)
            else:
                post_tests = IoSpec()
            self._post_tests = ejudge.combine_iospec(self.pre_tests,
                                                     post_tests)
            return self._post_tests

    @post_tests.setter
    def post_tests(self, value):
        pre_tests = self.pre_tests
        value = IoSpec([test for test in value if test not in pre_tests])
        self._post_tests = ejudge.combine_iospec(self.pre_tests, value)
        self.post_tests_source = value.source()

    @post_tests.deleter
    def post_tests(self):
        try:
            del self._post_tests
        except AttributeError:
            pass

    submission_class = CodingIoSubmission

    def load_post_file_data(self, file_data):
        fake_post = super().load_post_file_data(file_data)
        fake_post['pre_tests_source'] = self.pre_tests_source
        fake_post['post_tests_source'] = self.post_tests_source
        return fake_post

    # Expanding and controlling the tests state
    def has_test_state_changed(self):
        """
        Return True if test state has changed.
        """

        return self.test_state_hash == compute_test_state_hash(self)

    def get_current_test_state(self, update=False):
        """
        Return a current TestState object synchronized with the current
        pre and post tests.

        It raises a ValidationError if an error is encountered during the
        recreation of the test state.
        """

        if update:
            hash = compute_test_state_hash(self)
        else:
            hash = self.test_state_hash

        try:
            return TestState.objects.get(question=self, hash=hash)
        except TestState.DoesNotExist:
            pre_tests = self.pre_tests
            post_tests = self.post_tests

            def expand(x):
                result = expand_tests(self, x)
                check_expansions_with_all_programs(self, result)
                return result

            pre_source = expand(pre_tests).source()
            post_source = expand(post_tests).source()

            return TestState.objects.create(
                question=self,
                hash=hash,
                pre_tests_source=self.pre_tests_source,
                post_tests_source=self.post_tests_source,
                pre_tests_source_expansion=pre_source,
                post_tests_source_expansion=post_source,
            )

    def get_expanded_pre_tests(self):
        """
        Return an IoSpec object with the result of pre tests expansions.
        """

        state = self.get_current_test_state()
        source = state.pre_tests_source_expansion
        return parse_iospec(source)

    def get_expand_post_tests(self):
        """
        Return an IoSpec object with the result of post tests expansions.
        """

        state = self.get_current_test_state()
        source = state.post_tests_source_expansion
        return parse_iospec(source)

    def __expand_tests_to_source(self, tests):
        """
        Return the source of a iospec object full expansion.

        Similar to .expand_tests(), but return a string with the source code
        expansion.
        """

        if tests is None:
            return ''
        return self._expand_tests(tests)

    # Code runners
    def check_with_code(self, source, tests, language=None, timeout=None):
        """
        Wrapped version of check_with_code() that uses question's own timeout
        and language as default.
        """

        language = get_programming_language(language or self.language)
        timeout = timeout or self.timeout
        ejudge.check_with_code(source, tests, language, timeout)

    def run_code(self, source, tests, language=None, timeout=None):
        """
        Wrapped version of run_code() that uses question's own timeout
        and language as default.
        """

        language = get_programming_language(language or self.language)
        timeout = timeout or self.timeout
        return ejudge.run_code(source, tests, language, timeout)

    def grade_code(self, source, inputs, language=None, timeout=None):
        """
        Wrapped version of grade_code() that uses question's own timeout
        and language as default.
        """

        language = get_programming_language(language or self.language)
        timeout = timeout or self.timeout
        return ejudge.grade_code(source, inputs, language, timeout)

    def expand_from_code(self, source, inputs, language=None, timeout=None):
        """
        Wrapped version of expand_from_code() that uses question's own timeout
        and language as default.
        """

        language = get_programming_language(language or self.language)
        timeout = timeout or self.timeout
        return ejudge.expand_from_code(source, inputs, language, timeout)

    # Saving & validation
    def save(self, *args, **kwargs):
        self.test_state_hash = compute_test_state_hash(self)

        if not self.author_name and self.owner:
            name = self.owner.get_full_name() or self.owner.username
            email = self.owner.email
            self.author_name = '%s <%s>' % (name, email)

        super().save(*args, **kwargs)

    def clean(self):
        super().clean()

        if self.has_test_state_changed() or self.has_code_changed():
            logger.debug('%r: recomputing tests' % self.title)
            self.schedule_validation()

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

    def full_clean_expansions(self):
        self.get_current_test_state(update=True)

    def full_clean_answer_keys(self):
        """
        Performs a full_clean() validation step on all answer key objects.
        """

        for key in self.answers.all():
            try:
                key.question = self
                key.full_clean()
            except ValidationError as ex:
                raise validators.invalid_related_answer_key_error(key, ex)

    def full_clean_all(self, *args, **kwargs):
        self.full_clean(*args, **kwargs)
        self.full_clean_answer_keys()
        self.full_clean_expansions()

    def schedule_validation(self):
        """
        Schedule full validation to be performed in the background.

        This executes the full_clean_code() method
        """

        print('scheduling full code validation... (we are now executing on the'
              'foreground).')
        self.mark_invalid_code_fields()

    def mark_invalid_code_fields(self):
        """
        Performs a full code validation with .full_clean_code() and marks all
        errors found in the question.
        """

        return
        try:
            self.full_clean(force_expansions=True)
        except ValidationError as ex:
            print(ex)
            print(dir(ex))
            raise

    def validate_tests(self):
        """
        Triggered when (pre|post)_test_source changes or on the first time the
        .clean() method is called.
        """

        # Check if new source is valid
        for attr in ['pre_tests_source', 'post_tests_source']:
            try:
                source = getattr(self, attr)
                if source:
                    iospec = parse_iospec(source)
                else:
                    iospec = None
                setattr(self, attr[:-7], iospec)
            except Exception as ex:
                self.clear_tests()
                raise ValidationError(
                    {attr: _('invalid iospec syntax: %s' % ex)})

        # Computes temporary expansions for all sources. A second step may be
        # required in which we use the reference source in answer key to further
        # expand iospec data structures
        iospec = self.pre_tests.copy()
        iospec.expand_inputs(self.number_of_pre_expansions)
        self.pre_tests_expanded = iospec

        if self.pre_tests_source and self.post_tests_source:
            iospec = ejudge.combine_iospecs(self.pre_tests, self.post_tests)
        elif self.post_tests_source:
            iospec = self.post_tests.copy()
        elif self.pre_tests_source:
            iospec = self.pre_tests.copy()
        else:
            raise ValidationError(
                _('either pre_tests_source or post_tests_source must be given!'
                  ))
        iospec.expand_inputs(self.number_of_post_expansions)
        # assert len(iospec) >= self.number_of_expansions, iospec
        self.post_tests_expanded = iospec

        if self.pre_tests_expanded.is_expanded and \
                self.post_tests_expanded.is_expanded:
            self.pre_tests_expanded_source = self.pre_tests_expanded.source()
            self.post_tests_expanded_source = self.post_tests_expanded.source()

        else:
            self._expand_from_answer_keys()

        # Iospec is valid: save the hash
        self.tests_state_hash = self.current_tests_hash

    def _expand_from_answer_keys(self):
        # If the source requires expansion, we have to check all answer keys
        # to see if one of them defines a valid source and compute the expansion
        # from this source. All languages must produce the same expansion,
        # otherwise it is considered to be an error.
        #
        # If no answer key is available, leave pre_tests_expanded_source blank
        assert self.pre_tests_expanded is not None
        assert self.post_tests_expanded is not None
        pre, post = self.pre_tests_expanded, self.post_tests_expanded

        useful_keys = list(self.answers_with_code())
        if useful_keys:
            ex_pre = pre.copy()
            ex_pre.expand_inputs(self.number_of_pre_expansions)
            ex_post = post.copy()
            ex_post.expand_inputs(self.number_of_post_expansions)
            pre_list = self.answers.expand_all(ex_pre)
            post_list = self.answers.expand_all(ex_post)

            if len(pre_list) == len(post_list) == 1:
                ex_pre = pre_list[0]
                ex_post = post_list[0]
            else:

                def validate(L, field):
                    first, *tail = L
                    for i, elem in enumerate(tail, 1):
                        if first == elem:
                            continue

                        lang1 = useful_keys[0].language
                        lang2 = useful_keys[i].language
                        first.language = lang1
                        elem.language = lang2
                        self.clear_tests()
                        raise validators.inconsistent_testcase_error(
                            first, elem, field)

                validate(pre_list, 'pre_tests_expanded_source')
                validate(post_list, 'post_tests_expanded_source')
                ex_pre, ex_post = pre_list[0], post_list[0]

            # Update values
            self.pre_tests_expanded = ex_pre
            self.pre_tests_expanded_source = ex_pre.source()
            self.post_tests_expanded = ex_pre
            self.post_tests_expanded_source = ex_post.source()

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

        key = self.answers.get(language or self.language, None)
        if key is None:
            return self.default_placeholder
        return key.placeholder

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

        if language is None:
            language = self.language
        qs = self.answers.all().filter(
            language=get_programming_language(language))
        if qs:
            return qs.get().source
        return ''

    def get_submission_kwargs(self, request, kwargs):
        return dict(language=kwargs['language'], source=kwargs['source'])

    # Access answer key queryset
    def answers_with_code(self):
        """
        Filter only answers that define a program.
        """

        return self.answers.exclude(source='')

    def has_code_changed(self):
        """
        True if some answer source for a valid code has changed.
        """

        keys = self.answers_with_code()
        for key in keys:
            if key.has_changed_source():
                return True
        return False

    # Actions
    def submit(self, user_or_request, language=None, **kwargs):
        if language and self.language:
            if language != self.language:
                args = language, self.language
                raise ValueError('cannot set language: %r != %r' % args)
        if self.language:
            language = self.language
        language = get_programming_language(language)
        return super().submit(user_or_request, language=language, **kwargs)

    def run_post_grading(self, **kwargs):
        """
        Runs post tests for all submissions made to this question.
        """

        for response in self.responses.all():
            response.run_post_grading(tests=self.post_tests_expanded, **kwargs)
        self.closed = True
        self.save()

    def nav_section_for_activity(self, request):
        url = self.get_absolute_url
        section = NavSection(__('Question'),
                             url(),
                             title=__('Back to question'))
        if self.rules.test(request.user, 'activities.edit_activity'):
            section.add_link(__('Edit'),
                             self.get_admin_url(),
                             title=__('Edit question'))
        section.add_link(__('Submissions'),
                         url('submissions'),
                         title=__('View your submissions'))
        return section

    # Serving pages and routing
    template = 'questions/coding_io/detail.jinja2'
    template_submissions = 'questions/coding_io/submissions.jinja2'
    template_statistics = 'questions/coding_io/statistics.jinja2'
    template_debug = 'questions/coding_io/debug.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

    def serve_ajax_submission(self,
                              client,
                              source=None,
                              language=None,
                              **kwargs):
        """
        Handles student responses via AJAX and a srvice program.
        """

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

        return super().serve_ajax_submission(
            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)

    #
    # Actions
    #
    def regrade_post(self):
        """
        Regrade all submissions using the post tests.
        """

        self.responses.regrade_with(self.post_tests_expanded)

    def action_expand_tests(self, client, *args, **kwargs):
        self._expand_tests()
        pre = escape(self.pre_tests_expanded_source)
        post = escape(self.post_tests_expanded_source)
        client.dialog('<h2>Pre-tests</h2><pre>%s</pre>'
                      '<h2>Post-test</h2><pre>%s</pre>' % (pre, post))

    def action_grade_with_post_tests(self, client, *args, **kwargs):
        self.regrade_post()
        client.dialog('<p>Successful operation!</p>')
Exemplo n.º 20
0
class Feedback(HasProgressMixin, models.TimeStampedModel,
               models.PolymorphicModel):
    """
    Feedback for user.

    Usually there will be one feedback per submission, but this figure may
    vary from case to case.
    """
    TITLE_OK = _('Correct answer!')
    TITLE_PARTIAL = _('Partially correct.')
    TITLE_WRONG = _('Wrong answer.')
    TITLE_NOT_GRADED = _('Not graded.')

    MESSAGE_OK = _('*Congratulations!* Your response is correct!')
    MESSAGE_OK_WITH_PENALTIES = _(
        'Your response is correct, but you did not achieved the maximum grade.'
    )
    MESSAGE_WRONG = _('I\'m sorry. Wrong response response!')
    MESSAGE_PARTIAL = _(
        'Your answer is partially correct: you achieved %(grade)d%% of '
        'the total grade.')
    MESSAGE_NOT_GRADED = _('Your response has not been graded yet!')

    submission = models.OneToOneField('Submission', related_name='feedback')
    manual_grading = models.BooleanField(
        default=True,
        help_text=_('True if feedback was created manually by a human.'))
    grader_user = models.ForeignKey(
        models.User,
        blank=True,
        null=True,
        help_text=_('User that performed the manual grading.'))
    given_grade_pc = models.DecimalField(
        _('percentage of maximum grade'),
        help_text=_(
            'This grade is given by the auto-grader and represents the grade '
            'for the response before accounting for any bonuses or penalties.'
        ),
        max_digits=6,
        decimal_places=3,
        validators=[grade_validator],
        blank=True,
        null=True,
    )
    final_grade_pc = models.DecimalField(
        _('final grade'),
        help_text=_(
            'Similar to given_grade, but can account for additional factors '
            'such as delay penalties or for any other reason the teacher may '
            'want to override the student\'s grade.'),
        max_digits=6,
        decimal_places=3,
        validators=[grade_validator],
        blank=True,
        null=True,
    )
    is_correct = models.BooleanField(default=False)
    progress = lazy(lambda x: x.submission.progress)

    def get_feedback_title(self):
        """
        Return a title summarizing the feedback result. The default set of
        titles come from the list:

            * Correct answer!
            * Partially correct.
            * Wrong answer.
            * Not graded.

        Different question types may define additional values to this list.
        """

        grade = self.given_grade_pc

        if grade == 100:
            return self.TITLE_OK
        elif grade is not None and grade > 0:
            return self.TITLE_PARTIAL
        elif grade == 0:
            return self.TITLE_WRONG
        else:
            return self.TITLE_NOT_GRADED

    def update_autograde(self):
        """
        Compute and set self.given_grade.

        This function may change other states in the feedback object, depending
        on the activity.
        """

        activity = self.activity
        submission = self.submission
        self.given_grade_pc = self.get_given_autograde(submission, activity)

    def get_given_autograde(self, submission, activity):
        """
        Atomic and testable version of autograde_update().

        Subclasses should overide this method.

        Args:
            submission: a submission object
            activity: the activity the submission refers to

        Returns:
            A numeric value between 0 and 100 with the assigned grade.
        """

        name = self.__class__.__name__
        raise ImproperlyConfigured(
            'Class %s must implement the .autograde() method.' % name)

    def update_final_grade(self):
        """
        Compute final grade applying all possible penalties and bonuses.
        """

        self.final_grade_pc = self.given_grade_pc
        if self.given_grade_pc == 100:
            self.is_correct = True

    def render_message(self, **kwargs):
        """
        Renders feedback message.
        """

        if self.is_correct and self.final_grade_pc >= self.given_grade_pc:
            msg = self.MESSAGE_OK
        elif self.is_correct and self.final_grade_pc < self.given_grade_pc:
            msg = self.MESSAGE_OK_WITH_PENALTIES
        elif not self.is_correct and self.given_grade_pc > 0:
            msg = self.MESSAGE_PARTIAL
        else:
            msg = self.MESSAGE_WRONG
        return p(msg, cls='cs-feedback-message').render(**kwargs)
Exemplo n.º 21
0
class Progress(CommitMixin,
               models.CopyMixin,
               models.StatusModel,
               models.TimeStampedModel,
               models.PolymorphicModel):
    """
    When an user starts an activity it opens a Progress object which control
    all submissions to the given activity.

    The Progress object also manages individual submissions that may span
    several http requests.
    """

    class Meta:
        unique_together = [('user', 'activity_page')]
        verbose_name = _('student progress')
        verbose_name_plural = _('student progress list')

    STATUS_OPENED = 'opened'
    STATUS_CLOSED = 'closed'
    STATUS_INCOMPLETE = 'incomplete'
    STATUS_WAITING = 'waiting'
    STATUS_INVALID = 'invalid'
    STATUS_DONE = 'done'

    STATUS = models.Choices(
        (STATUS_OPENED, _('opened')),
        (STATUS_CLOSED, _('closed')),
    )

    user = models.ForeignKey(models.User, on_delete=models.CASCADE)
    activity_page = models.ForeignKey(models.Page, on_delete=models.CASCADE)
    final_grade_pc = models.DecimalField(
        _('final score'),
        max_digits=6, decimal_places=3, default=Decimal,
        help_text=_(
            'Final grade given to considering all submissions, penalties, etc.'
        ),
    )
    given_grade_pc = models.DecimalField(
        _('grade'),
        max_digits=6, decimal_places=3, default=Decimal,
        help_text=_('Final grade before applying any modifier.'),
    )
    finished = models.DateTimeField(blank=True, null=True)
    best_submission = models.ForeignKey('Submission', blank=True, null=True,
                                        related_name='+')
    points = models.IntegerField(default=0)
    score = models.IntegerField(default=0)
    stars = models.FloatField(default=0.0)
    is_correct = models.BooleanField(default=bool)
    has_submissions = models.BooleanField(default=bool)
    has_feedback = models.BooleanField(default=bool)
    has_post_tests = models.BooleanField(default=bool)
    objects = ProgressManager()

    #: The number of submissions
    num_submissions = property(lambda x: x.submissions.count())

    #: Specific activity reference
    activity = property(lambda x: x.activity_page.specific)
    activity_id = property(lambda x: x.activity_page_id)

    #: Has progress mixin interface
    username = property(lambda x: x.user.username)

    def __repr__(self):
        return '<%s: %s>' % (self.__class__.__name__, self)

    def __str__(self):
        tries = self.num_submissions
        user = self.user
        activity = self.activity
        grade = '%s pts' % (self.final_grade_pc or 0)
        fmt = '%s by %s (%s, %s tries)'
        return fmt % (activity, user, grade, tries)

    def submit(self, request, payload, recycle=True, commit=True):
        """
        Creates new submission.

        Args:
            recycle:
                If True, recycle submission objects with the same content as the
                current submission. If a submission exists with the same content
                as the current submission, it simply returns the previous
                submission. If recycled, sets the submission.recycled to True.
        """

        submission_class = self.activity.submission_class
        submission = submission_class(progress=self, **payload)
        submission.ip_address = get_ip(request)
        submission.hash = submission.compute_hash()
        submission.full_clean()

        # Then check if any submission is equal to some past submission and
        # then recycle it
        recyclable = submission_class.objects.recyclable(submission)
        recyclable = recyclable if recycle else ()
        for possibly_equal in recyclable:
            if submission.is_equal(possibly_equal):
                possibly_equal.recycled = True
                possibly_equal.bump_recycles()
                return possibly_equal
        else:
            return submission.commit(commit)

    def register_feedback(self, feedback, commit=True):
        """
        This method is called after a submission is graded and produces a
        feedback.
        """

        submission = feedback.submission

        # Check if it is the best submission
        grade = feedback.given_grade_pc
        if (self.best_submission is None or
                self.best_submission.feedback.given_grade_pc < grade):
            self.best_submission = submission

        # Update grades for activity considering past submissions
        self.update_grades_from_feedback(feedback)
        self.commit(commit)

    def update_grades_from_feedback(self, feedback):
        """
        Update grades from the current progress object from the given feedback.
        """

        # Update grades, keeping always the best grade
        if self.given_grade_pc < (feedback.given_grade_pc or 0):
            self.given_grade_pc = feedback.given_grade_pc
        if self.final_grade_pc < feedback.final_grade_pc:
            self.final_grade_pc = feedback.final_grade_pc

        # Update the is_correct field
        self.is_correct = self.is_correct or feedback.is_correct
Exemplo n.º 22
0
class User(AbstractBaseUser, PermissionsMixin):
    """
    Base user model.
    """

    REQUIRED_FIELDS = ['alias', 'name', 'school_id', 'role']
    USERNAME_FIELD = 'email'
    ROLE_STUDENT, ROLE_TEACHER, ROLE_STAFF, ROLE_ADMIN = range(4)
    ROLE_CHOICES = [
        (ROLE_STUDENT, _('Student')),
        (ROLE_TEACHER, _('Teacher')),
        (ROLE_STAFF, _('School staff')),
        (ROLE_ADMIN, _('Administrator')),
    ]

    email = models.EmailField(
        _('E-mail'),
        db_index=True,
        unique=True,
        help_text=_(
            'Users can register additional e-mail addresses. This is the '
            'main e-mail address which is used for login.'))
    name = models.CharField(_('Name'),
                            max_length=140,
                            help_text=_('Full name of the user.'))
    alias = models.CharField(
        _('Alias'),
        max_length=20,
        help_text=_('Public alias used to identify the user.'))
    school_id = models.CharField(
        _('School id'),
        max_length=20,
        blank=False,
        unique=True,
        validators=[],  # TODO: validate school id number
        help_text=_('Identification number in your school issued id card.'))
    role = models.IntegerField(
        _('Main'),
        choices=ROLE_CHOICES,
        default=ROLE_STUDENT,
        help_text=_('User main role in the codeschool platform.'))
    is_staff = models.BooleanField(
        _('staff status'),
        default=False,
        help_text=_(
            'Designates whether the user can log into this admin site.'),
    )
    is_active = models.BooleanField(
        _('active'),
        default=True,
        help_text=_(
            'Designates whether this user should be treated as active. '
            'Unselect this instead of deleting accounts.'),
    )
    date_joined = models.DateTimeField(_('date joined'), default=timezone.now)

    objects = UserManager()

    # Temporary properties defined for compatibility
    username = property(lambda x: x.email)

    @property
    def profile(self):
        if self.id is None:
            return self._lazy_profile

        try:
            return self.profile_ref
        except AttributeError:
            self.profile_ref = Profile(user=self)
            return self.profile_ref

    @profile.setter
    def profile(self, value):
        if self.id is None:
            self._lazy_profile = value
        else:
            self.profile_ref = value

    @lazy
    def _lazy_profile(self):
        return Profile(user=self)

    def save(self, *args, **kwargs):
        new = self.id is None

        if new:
            with transaction.atomic():
                super().save(*args, **kwargs)
                self.profile.save()
        else:
            super().save(*args, **kwargs)

    def get_full_name(self):
        return self.name.strip()

    def get_short_name(self):
        return self.alias

    def get_absolute_url(self):
        return reverse('users:profile-detail', args=(self.id, ))
Exemplo n.º 23
0
class Conditions(models.PolymorphicModel):
    """
    Each activity can be bound to different sets of conditions that control
    aspects on how the activity should be graded and may place restrictions on
    how the students may respond to the given activity.
    """
    class Meta:
        verbose_name = _('conditions')
        verbose_name_plural = _('conditions')

    name = models.CharField(_('name'),
                            max_length=140,
                            blank=True,
                            help_text=_('A string identifier.'))
    single_submission = models.BooleanField(
        _('single submission'),
        default=False,
        help_text=_(
            'If set, students will be allowed to send only a single '
            'submission per activity.', ),
    )
    delay_feedback = models.BooleanField(
        _('delay feedback'),
        default=False,
        help_text=_(
            'If set, students will be only be able to see the feedback after '
            'the activity expires its deadline.'))
    programming_language = models.ForeignKey(
        'core.ProgrammingLanguage',
        blank=True,
        null=True,
        related_name='+',
        help_text=_(
            'Defines the required programming language for code-based student '
            'responses, when applicable. Leave it blank if you do not want to '
            'enforce any programming language.'))
    text_format = models.ForeignKey(
        'core.FileFormat',
        blank=True,
        null=True,
        related_name='+',
        help_text=_(
            'Defines the required file format for text-based responses, when '
            'applicable. Leave it blank if you do not want to enforce any '
            'text format.'))

    def __str__(self):
        return self.name or 'Untitled condition object.'

    panels = [
        panels.FieldPanel('name'),
        panels.MultiFieldPanel([
            panels.FieldPanel('single_submission'),
            panels.FieldPanel('delay_feedback'),
        ],
                               heading=_('Submissions')),
        panels.MultiFieldPanel([
            panels.FieldPanel('deadline'),
            panels.FieldPanel('hard_deadline'),
            panels.FieldPanel('delay_penalty'),
        ],
                               heading=_('Deadline')),
        panels.MultiFieldPanel([
            panels.FieldPanel('get_programming_language'),
            panels.FieldPanel('text_format'),
        ],
                               heading=_('Deadline')),
    ]
Exemplo n.º 24
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')),
    ]
Exemplo n.º 25
0
class Activity(CommitMixin, metaclass=ActivityMeta):
    """
    Represents a gradable activity inside a course. Activities may not have an
    explicit grade, but yet may provide points to the students via the
    gamefication features of Codeschool.

    Activities can be scheduled to be done in the class or as a homework
    assignment.

    Each concrete activity is represented by a different subclass.
    """

    VISIBILITY_PRIVATE, VISIBILITY_STAFF, VISIBILITY_PUBLIC = range(3)
    VISIBILITY_CHOICES = [(VISIBILITY_PRIVATE, _('Private')),
                          (VISIBILITY_STAFF, _('STAFF')),
                          (VISIBILITY_PUBLIC, _('Public'))]

    owner = models.ForeignKey(
        models.User,
        verbose_name=_('Owner'),
        help_text=_('The activity\'s owner.'),
    )
    visibility = models.PositiveSmallIntegerField(
        _('Visibility'),
        choices=VISIBILITY_CHOICES,
        help_text=_('Makes activity invisible to users.'),
    )
    closed = models.BooleanField(
        _('Closed to submissions'),
        default=bool,
        help_text=_(
            'A closed activity does not accept new submissions, but users can '
            'see that they still exist.'))
    group_submission = models.BooleanField(
        _('Group submissions'),
        default=bool,
        help_text=_(
            'If enabled, submissions are registered to groups instead of '
            'individual students.'))
    max_group_size = models.IntegerField(
        _('Maximum group size'),
        default=6,
        help_text=_(
            'If group submission is enabled, define the maximum size of a '
            'group.'),
    )
    disabled = models.BooleanField(
        _('Disabled'),
        default=bool,
        help_text=_(
            'Activities can be automatically disabled when Codeshool '
            'encounters an error. This usually produces a message saved on '
            'the .disabled_message attribute. '
            'This field is not controlled directly by users.'))
    disabled_message = models.TextField(
        _('Disabled message'),
        blank=True,
        help_text=_('Messsage explaining why the activity was disabled.'))
    has_submissions = models.BooleanField(default=bool)
    has_correct_submissions = models.BooleanField(default=bool)
    section_title = property(lambda self: _(self._meta.verbose_name))

    objects = ActivityManager()
    rules = Rules()

    class Meta:
        abstract = True
        verbose_name = _('activity')
        verbose_name_plural = _('activities')
        permissions = [
            ('interact', 'Interact'),
            ('view_submissions', 'View submissions'),
        ]

    # These properties dynamically define the progress/submission/feedback
    # classes associated with the current class.
    progress_class = AuxiliaryClassIntrospection('progress')
    submission_class = AuxiliaryClassIntrospection('submission')
    feedback_class = AuxiliaryClassIntrospection('feedback')

    @property
    def submissions(self):
        return self.submission_class.objects.filter(
            progress__activity_page_id=self.id)

    def clean(self):
        super().clean()

        if not self.author_name and self.owner:
            name = self.owner.get_full_name()
            email = self.owner.email
            self.author_name = '%s <%s>' % (name, email)

        if self.disabled:
            raise ValidationError(self.disabled_message)

    def disable(self, error_message=_('Internal error'), commit=True):
        """
        Disable activity.

        Args:
            message:
                An error message explaining why activity was disabled.
        """

        self.disabled = True
        self.disabled_message = error_message
        self.commit(commit, update_fields=['disabled', 'disabled_message'])

    def submit(self, request, _commit=True, **kwargs):
        """
        Create a new Submission object for the given question and saves it on
        the database.

        Args:
            request:
                The request object for the current submission. The user is
                obtained from the request object.

        This code loads the :cls:`Progress` object for the given user and
        calls it :meth:`Progress.submit`` passing all named arguments to it.

        Subclasses should personalize the submit() method of the Progress object
        instead of the one in this class.
        """

        assert hasattr(request, 'user'), 'request do not have a user attr'

        # Test if activity is active
        if self.closed or self.disabled:
            raise RuntimeError('activity is closed to new submissions')

        # Fetch submission class
        submission_class = self.submission_class
        if submission_class is None:
            raise ImproperlyConfigured(
                '%s must define a submission_class attribute with the '
                'appropriate submission class.' % self.__class__.__name__)

        # Dispatch to the progress object
        user = request.user
        logger.info('%r, submission from user %r' %
                    (self.title, user.username))
        progress = self.progress_set.for_user(user)
        return progress.submit(request, kwargs, commit=_commit)

    def filter_user_submission_payload(self, request, payload):
        """
        Filter a dictionary of arguments supplied by an user and return a
        dictionary with only those arguments that should be passed to the
        .submit() function.
        """

        data_fields = self.submission_class.data_fields()
        return {k: v for (k, v) in payload.items() if k in data_fields}

    def submit_with_user_payload(self, request, payload):
        """
        Return a submission from a dictionary of user provided kwargs.

        It first process the keyword arguments and pass them to the .submit()
        method.
        """

        payload = self.filter_user_submission_payload(request, payload)
        return self.submit(request, **payload)
Exemplo n.º 26
0
class Submission(ResponseDataMixin, FeedbackDataMixin, models.CopyMixin,
                 models.StatusModel, models.TimeStampedModel,
                 models.PolymorphicModel):
    """
    Represents a student's simple submission in response to some activity.

    Submissions can be in 4 different states:

    pending:
        The response has been sent, but was not graded. Grading can be manual or
        automatic, depending on the activity.
    waiting:
        Waiting for manual feedback.
    incomplete:
        For long-term activities, this tells that the student started a response
        and is completing it gradually, but the final response was not achieved
        yet.
    invalid:
        The response has been sent, but contains malformed data.
    done:
        The response was graded and evaluated and it initialized a feedback
        object.

    A response always starts at pending status. We can request it to be graded
    by calling the :func:`Response.autograde` method. This method must raise
    an InvalidResponseError if the response is invalid or ManualGradingError if
    the response subclass does not implement automatic grading.
    """
    class Meta:
        verbose_name = _('submission')
        verbose_name_plural = _('submissions')

    # Feedback messages
    MESSAGE_OK = _('*Congratulations!* Your response is correct!')
    MESSAGE_OK_WITH_PENALTIES = _(
        'Your response is correct, but you did not achieved the maximum grade.'
    )
    MESSAGE_WRONG = _('I\'m sorry, your response is wrong.')
    MESSAGE_PARTIAL = _(
        'Your answer is partially correct: you achieved only %(grade)d%% of '
        'the total grade.')
    MESSAGE_NOT_GRADED = _('Your response has not been graded yet!')

    # Status
    STATUS_PENDING = 'pending'
    STATUS_INCOMPLETE = 'incomplete'
    STATUS_WAITING = 'waiting'
    STATUS_INVALID = 'invalid'
    STATUS_DONE = 'done'

    # Fields
    STATUS = models.Choices(
        (STATUS_PENDING, _('pending')),
        (STATUS_INCOMPLETE, _('incomplete')),
        (STATUS_WAITING, _('waiting')),
        (STATUS_INVALID, _('invalid')),
        (STATUS_DONE, _('done')),
    )

    response = models.ParentalKey(
        'Response',
        related_name='submissions',
    )
    given_grade = models.DecimalField(
        _('percentage of maximum grade'),
        help_text=_(
            'This grade is given by the auto-grader and represents the grade '
            'for the response before accounting for any bonuses or penalties.'
        ),
        max_digits=6,
        decimal_places=3,
        blank=True,
        null=True,
    )
    final_grade = models.DecimalField(
        _('final grade'),
        help_text=_(
            'Similar to given_grade, but can account for additional factors '
            'such as delay penalties or for any other reason the teacher may '
            'want to override the student\'s grade.'),
        max_digits=6,
        decimal_places=3,
        blank=True,
        null=True,
    )
    manual_override = models.BooleanField(default=False)
    points = models.IntegerField(default=0)
    score = models.IntegerField(default=0)
    stars = models.FloatField(default=0)
    objects = SubmissionManager()

    # Status properties
    is_done = property(lambda x: x.status == x.STATUS_DONE)
    is_pending = property(lambda x: x.status == x.STATUS_PENDING)
    is_waiting = property(lambda x: x.status == x.STATUS_WAITING)
    is_invalid = property(lambda x: x.status == x.STATUS_INVALID)

    @property
    def is_correct(self):
        if self.given_grade is None:
            raise AttributeError('accessing attribute of non-graded response.')
        else:
            return self.given_grade == 100

    # Delegate properties
    activity = delegate_to('response')
    activity_id = delegate_to('response')
    activity_page = delegate_to('response')
    activity_page_id = delegate_to('response')
    user = delegate_to('response')
    user_id = delegate_to('response')
    stars_total = delegate_to('activity')
    points_total = delegate_to('activity')

    @classmethod
    def response_data_hash(cls, response_data):
        """
        Computes a hash for the response_data attribute.

        Data must be given as a JSON-like structure or as a string of JSON data.
        """

        if response_data:
            if isinstance(response_data, str):
                data = response_data
            else:
                data = json.dumps(response_data, default=json_default)
            return md5hash(data)
        return ''

    def __init__(self, *args, **kwargs):
        # Django is loading object from the database -- we step out the way
        if args and not kwargs:
            super().__init__(*args, **kwargs)
            return

        # We create the response_data and feedback_data manually always using
        # copies of passed dicts. We save these variables here, init object and
        # then copy this data to the initialized dictionaries
        response_data = kwargs.pop('response_data', None) or {}
        feedback_data = kwargs.pop('feedback_data', None) or {}

        # This part makes a Submission instance initialize from a user +
        # activity instead of requiring a response object. The response is
        # automatically created on demand.
        user = kwargs.pop('user', None)
        if 'response' in kwargs and user and user != kwargs['response'].user:
            response_user = kwargs['response'].user
            raise ValueError('Inconsistent user definition: %s vs. %s' %
                             (user, response_user))
        elif 'response' not in kwargs and user:
            try:
                activity = kwargs.pop('activity')
            except KeyError:
                raise TypeError(
                    '%s objects bound to a user must also provide an '
                    'activity parameter.' % type(self).__name__)
            else:
                # User-bound constructor tries to obtain the response object by
                # searching for an specific (user, activity) tuple.
                response, created = Response.objects.get_or_create(
                    user=user, activity=activity)
                kwargs['response'] = response

        if 'context' in kwargs or 'activity' in kwargs:
            raise TypeError(
                'Must provide an user to instantiate a bound submission.')
        super().__init__(*args, **kwargs)

        # Now that we have initialized the submission, we fill the data
        # passed in the response_data and feedback_data dictionaries.
        self.response_data = dict(self.response_data or {}, **response_data)
        self.feedback_data = dict(self.response_data or {}, **feedback_data)

    def __str__(self):
        if self.given_grade is None:
            grade = self.status
        else:
            grade = '%s pts' % self.final_grade
        user = self.user
        activity = self.activity
        name = self.__class__.__name__
        return '<%s: %s by %s (%s)>' % (name, activity, user, grade)

    def __html__(self):
        """
        A string of html source representing the feedback.
        """

        if self.is_done:
            data = {'grade': (self.final_grade or 0)}

            if self.final_grade == 100:
                return markdown(self.MESSAGE_OK)
            elif self.given_grade == 100:
                return markdown(self.ok_with_penalties_message)
            elif not self.given_grade:
                return markdown(self.MESSAGE_WRONG)
            else:
                return markdown(self.MESSAGE_PARTIAL % data)
        else:
            return markdown(self.MESSAGE_NOT_GRADED)

    def save(self, *args, **kwargs):
        if not self.response_hash:
            self.response_hash = self.response_hash_from_data(
                self.response_hash)
        super().save(*args, **kwargs)

    def final_points(self):
        """
        Return the amount of points awarded to the submission after
        considering all penalties and bonuses.
        """

        return self.points

    def final_stars(self):
        """
        Return the amount of stars awarded to the submission after
        considering all penalties and bonuses.
        """

        return self.stars

    def given_stars(self):
        """
        Compute the number of stars that should be awarded to the submission
        without taking into account bonuses and penalties.
        """

        return self.stars_total * (self.given_grade / 100)

    def given_points(self):
        """
        Compute the number of points that should be awarded to the submission
        without taking into account bonuses and penalties.
        """

        return int(self.points_total * (self.given_grade / 100))

    def feedback(self, commit=True, force=False, silent=False):
        """
        Return the feedback object associated to the given response.

        This method may trigger the autograde() method, if grading was not
        performed yet. If you want to defer database access, call it with
        commit=False to prevent saving any modifications to the response object
        to the database.

        The commit, force and silent arguments have the same meaning as in
        the :func:`Submission.autograde` method.
        """

        if self.status == self.STATUS_PENDING:
            self.autograde(commit=commit, force=force, silent=silent)
        elif self.status == self.STATUS_INVALID:
            raise self.feedback_data
        elif self.status == self.STATUS_WAITING:
            return None
        return self.feedback_data

    def autograde(self, commit=True, force=False, silent=False):
        """
        Performs automatic grading.

        Response subclasses must implement the autograde_compute() method in
        order to make automatic grading work. This method may write any
        relevant information to the `feedback_data` attribute and must return
        a numeric value from 0 to 100 with the given automatic grade.

        Args:
            commit:
                If false, prevents saving the object when grading is complete.
                The user must save the object manually after calling this
                method.
            force:
                If true, force regrading the item even if it has already been
                graded. The default behavior is to ignore autograde from a
                graded submission.
            silent:
                Prevents the submission_graded_signal from triggering in the
                end of a successful grading.
        """

        if self.status == self.STATUS_PENDING or force:
            # Evaluate grade using the autograde_value() method of subclass.
            try:
                value = self.autograde_value()
            except self.InvalidSubmissionError as ex:
                self.status = self.STATUS_INVALID
                self.feedback_data = ex
                self.given_grade = self.final_grade = decimal.Decimal(0)
                if commit:
                    self.save()
                raise

            # If no value is returned, change to STATUS_WAITING. This probably
            # means that response is partial and we need other submissions to
            # complete the final response
            if value is None:
                self.status = self.STATUS_WAITING

            # A regular submission has a decimal grade value. We save it and
            # change state to STATUS_DONE
            else:
                self.given_grade = decimal.Decimal(value)
                if self.final_grade is None:
                    self.final_grade = self.given_grade
                self.status = self.STATUS_DONE

            # Commit results
            if commit and self.pk:
                self.save(update_fields=[
                    'status', 'feedback_data', 'given_grade', 'final_grade'
                ])
            elif commit:
                self.save()

            # If STATUS_DONE, we submit the submission_graded signal.
            if self.status == self.STATUS_DONE:
                self.stars = self.given_stars()
                self.points = self.given_points()
                self.response.register_submission(self)
                if not silent:
                    submission_graded_signal.send(
                        Submission,
                        submission=self,
                        given_grade=self.given_grade,
                        automatic=True,
                    )

        elif self.status == self.STATUS_INVALID:
            raise self.feedback_data

    def manual_grade(self, grade, commit=True, raises=False, silent=False):
        """
        Saves result of manual grading.

        Args:
            grade (number):
                Given grade, as a percentage value.
            commit:
                If false, prevents saving the object when grading is complete.
                The user must save the object manually after calling this
                method.
            raises:
                If submission has already been graded, raises a GradingError.
            silent:
                Prevents the submission_graded_signal from triggering in the
                end of a successful grading.
        """

        if self.status != self.STATUS_PENDING and raises:
            raise GradingError('Submission has already been graded!')

        raise NotImplementedError('TODO')

    def autograde_value(self):
        """
        This method should be implemented in subclasses.
        """

        raise ImproperlyConfigured(
            'Response subclass %r must implement the autograde_value().'
            'This method should perform the automatic grading and return the '
            'resulting grade. Any additional relevant feedback data might be '
            'saved to the `feedback_data` attribute, which is then is pickled '
            'and saved into the database.' % type(self).__name__)

    def regrade(self, method, commit=True):
        """
        Recompute the grade for the given submission.

        If status != 'done', it simply calls the .autograde() method. Otherwise,
        it accept different strategies for updating to the new grades:
            'update':
                Recompute the grades and replace the old values with the new
                ones. Only saves the submission if the feedback_data or the
                given_grade attributes change.
            'best':
                Only update if the if the grade increase.
            'worst':
                Only update if the grades decrease.
            'best-feedback':
                Like 'best', but updates feedback_data even if the grades
                change.
            'worst-feedback':
                Like 'worst', but updates feedback_data even if the grades
                change.

        Return a boolean telling if the regrading was necessary.
        """
        if self.status != self.STATUS_DONE:
            return self.autograde()

        # We keep a copy of the state, if necessary. We only have to take some
        # action if the state changes.
        def rollback():
            self.__dict__.clear()
            self.__dict__.update(state)

        state = self.__dict__.copy()
        self.autograde(force=True, commit=False)

        # Each method deals with the new state in a different manner
        if method == 'update':
            if state != self.__dict__:
                if commit:
                    self.save()
                return False
            return True
        elif method in ('best', 'best-feedback'):
            if self.given_grade <= state.get('given_grade', 0):
                new_feedback_data = self.feedback_data
                rollback()
                if new_feedback_data != self.feedback_data:
                    self.feedback_data = new_feedback_data
                    if commit:
                        self.save()
                    return True
                return False
            elif commit:
                self.save()
            return True

        elif method in ('worst', 'worst-feedback'):
            if self.given_grade >= state.get('given_grade', 0):
                new_feedback_data = self.feedback_data
                rollback()
                if new_feedback_data != self.feedback_data:
                    self.feedback_data = new_feedback_data
                    if commit:
                        self.save()
                    return True
                return False
            elif commit:
                self.save()
            return True
        else:
            rollback()
            raise ValueError('invalid method: %s' % method)
Exemplo n.º 27
0
class Profile(UserenaBaseProfile):
    """
    Social information about users.
    """
    class Meta:
        permissions = (
            ('student', _('Can access/modify data visible to student\'s')),
            ('teacher',
             _('Can access/modify data visible only to Teacher\'s')),
        )

    GENDER_MALE, GENDER_FEMALE = 0, 1

    user = models.OneToOneField(
        User,
        verbose_name=_('user'),
        unique=True,
        blank=True,
        null=True,
        on_delete=models.SET_NULL,
        related_name='profile',
    )
    school_id = models.CharField(
        _('school id'),
        max_length=50,
        blank=True,
        null=True,
        help_text=_('Identification number in your school issued id card.'),
    )
    is_teacher = models.BooleanField(default=False)
    nickname = models.CharField(max_length=50, blank=True, null=True)
    phone = models.CharField(max_length=20, blank=True, null=True)
    gender = models.SmallIntegerField(_('gender'),
                                      choices=[(GENDER_MALE, _('Male')),
                                               (GENDER_FEMALE, _('Female'))],
                                      blank=True,
                                      null=True)
    date_of_birth = models.DateField(_('date of birth'), blank=True, null=True)
    website = models.URLField(blank=True, null=True)
    about_me = models.RichTextField(blank=True, null=True)

    # Delegates and properties
    username = delegate_to('user', True)
    first_name = delegate_to('user')
    last_name = delegate_to('user')
    email = delegate_to('user')

    @property
    def age(self):
        if self.date_of_birth is None:
            return None
        today = timezone.now().date()
        birthday = self.date_of_birth
        years = today.year - birthday.year
        birthday = datetime.date(today.year, birthday.month, birthday.day)
        if birthday > today:
            return years - 1
        else:
            return years

    def __str__(self):
        if self.user is None:
            return __('Unbound profile')
        full_name = self.user.get_full_name() or self.user.username
        return __('%(name)s\'s profile') % {'name': full_name}

    def get_full_name_or_username(self):
        name = self.user.get_full_name()
        if name:
            return name
        else:
            return self.user.username

    def get_absolute_url(self):
        return reverse('auth:profile-detail',
                       kwargs={'username': self.user.username})

    # Serving pages
    template = 'cs_auth/profile-detail.jinja2'

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

    # Wagtail admin
    panels = [
        panels.MultiFieldPanel([
            panels.FieldPanel('school_id'),
        ],
                               heading='Required information'),
        panels.MultiFieldPanel([
            panels.FieldPanel('nickname'),
            panels.FieldPanel('phone'),
            panels.FieldPanel('gender'),
            panels.FieldPanel('date_of_birth'),
        ],
                               heading=_('Personal Info')),
        panels.MultiFieldPanel([
            panels.FieldPanel('website'),
        ],
                               heading=_('Web presence')),
        panels.RichTextFieldPanel('about_me'),
    ]
Exemplo n.º 28
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')))
Exemplo n.º 29
0
class Activity(models.RoutablePageExt, metaclass=ActivityMeta):
    """
    Represents a gradable activity inside a course. Activities may not have an
    explicit grade, but yet may provide points to the students via the
    gamefication features of Codeschool.

    Activities can be scheduled to be done in the class or as a homework
    assignment.

    Each concrete activity is represented by a different subclass.
    """

    class Meta:
        abstract = True
        verbose_name = _('activity')
        verbose_name_plural = _('activities')
        permissions = [
            ('interact', 'Interact'),
            ('view_submissions', 'View submissions'),
        ]

    author_name = models.CharField(
        _('Author\'s name'),
        max_length=100,
        blank=True,
        help_text=_(
            'The author\'s name, if not the same user as the question owner.'
        ),
    )
    visible = models.BooleanField(
        _('Invisible'),
        default=bool,
        help_text=_(
            'Makes activity invisible to users.'
        ),
    )
    closed = models.BooleanField(
        _('Closed to submissions'),
        default=bool,
        help_text=_(
            'A closed activity does not accept new submissions, but users can '
            'see that they still exist.'
        )
    )
    group_submission = models.BooleanField(
        _('Group submissions'),
        default=bool,
        help_text=_(
            'If enabled, submissions are registered to groups instead of '
            'individual students.'
        )
    )
    max_group_size = models.IntegerField(
        _('Maximum group size'),
        default=6,
        help_text=_(
            'If group submission is enabled, define the maximum size of a '
            'group.'
        ),
    )
    disabled = models.BooleanField(
        _('Disabled'),
        default=bool,
        help_text=_(
            'Activities can be automatically disabled when Codeshool '
            'encounters an error. This usually produces a message saved on '
            'the .disabled_message attribute.'
        )
    )
    disabled_message = models.TextField(
        _('Disabled message'),
        blank=True,
        help_text=_(
            'Messsage explaining why the activity was disabled.'
        )
    )
    has_submissions = models.BooleanField(default=bool)
    has_correct_submissions = models.BooleanField(default=bool)
    section_title = property(lambda self: _(self._meta.verbose_name))

    objects = ActivityManager()
    rules = Rules()

    # These properties dynamically define the progress/submission/feedback
    # classes associated with the current class.
    progress_class = AuxiliaryClassIntrospection('progress')
    submission_class = AuxiliaryClassIntrospection('submission')
    feedback_class = AuxiliaryClassIntrospection('feedback')

    @property
    def submissions(self):
        return self.submission_class.objects.filter(
            progress__activity_page_id=self.id
        )

    def clean(self):
        super().clean()

        if not self.author_name and self.owner:
            name = self.owner.get_full_name()
            email = self.owner.email
            self.author_name = '%s <%s>' % (name, email)

        if self.disabled:
            raise ValidationError(self.disabled_message)

    def submit(self, request, user=None, **kwargs):
        """
        Create a new Submission object for the given question and saves it on
        the database.

        Args:
            request:
                The request object for the current submission.
            recycle:
                If true, recycle submission objects with the same content as the
                current submission. If a submission exists with the same content
                as the current submission, it simply returns the previous
                submission.
                If recycled, sets the submission.recycled to True.
            user:
                The user who submitted the response. If not given, uses the user
                in the request object.
        """

        if hasattr(request, 'username'):
            raise ValueError

        # Test if activity is active
        if self.closed:
            raise ValueError('activity is closed to new submissions')

        # Fetch submission class
        submission_class = self.submission_class
        if submission_class is None:
            raise ImproperlyConfigured(
                '%s must define a submission_class attribute with the '
                'appropriate submission class.' % self.__class__.__name__
            )

        # Add progress information to the given submission kwargs
        if user is None:
            user = request.user
        logger.info('%r, submission from user %r' %
                    (self.title, user.username))
        progress = self.progress_set.for_user(user)
        return progress.submit(request, **kwargs)
Exemplo n.º 30
0
class BooleanQuestion(Question):
    answer = models.BooleanField()