Ejemplo n.º 1
0
    class B(object):
        x = delegate_to('data')
        y = delegate_to('data', readonly=True)
        z = delegate_ro('data')

        def __init__(self, data):
            self.data = data
Ejemplo n.º 2
0
class Response:
    """
    Base class for all responses.

    Attributes:
        content (bytes):
            A raw byte-string with the response data.
        data (str):
            Content as a decoded string.
        status_code:
            Numeric HTTP status code (e.g., 200, 404, etc)
        encoding (str):
            Data encoding
        url (str):
            Request absolute URL
        header (dict):
            A dictionary-like object with the HTTP headers.
    """
    @lazy
    def data(self):
        return self.content.decode(self.encoding)

    content = delegate_to('_data')
    status_code = delegate_to('_data')
    encoding = delegate_to('_data')
    url = delegate_to('_data')
    header = delegate_to('_data')
Ejemplo n.º 3
0
class ActivityFixtures:
    """
    Expose an "activity" and a "progress" fixtures that do not access the
    database by default.

    Users of this class must define an activity_class class attribute with
    the class that should be tested.
    """

    activity_class = Activity
    submission_payload = {}
    use_db = False

    @pytest.fixture
    def activity(self):
        "An activity instance that does not touch the db."
        return self.activity_class(title='Test', id=1)

    @pytest.fixture
    def activity_db(self):
        "A saved activity instance."
        return self.activity_class(title='Test', id=1)

    @pytest.yield_fixture
    def progress(self, activity, user):
        "A progress instance for some activity."

        cls = self.progress_class

        if cls._meta.abstract:
            pytest.skip('Progress class is abstract')

        with patch.object(cls, 'user', user):
            yield cls(activity_page=activity, id=1)

    @pytest.fixture
    def progress_db(self, progress):
        "A progress instance saved to the db."

        progress.user.save()
        progress.activity.save()
        progress.save()
        return progress

    @pytest.fixture
    def user(self):
        "An user"

        return UserFactory.build(id=2, alias='user')

    # Properties
    progress_class = delegate_to('activity_class')
    submission_class = delegate_to('activity_class')
    feedback_class = delegate_to('activity_class')
Ejemplo n.º 4
0
class FromProgressAttributesMixin:
    """
    Mixin class for submissions and feedback.

    Imports attributes from obj.progress
    """

    activity = delegate_to('progress', readonly=True)
    activity_id = delegate_to('progress', readonly=True)
    activity_title = property(lambda x: x.progress.activity_page.title)
    user = delegate_to('progress', readonly=True)
Ejemplo n.º 5
0
class GivenBadge(models.TimeStampedModel):
    """
    Associate users with badges.
    """

    badge = models.ForeignKey(Badge)
    user = models.ForeignKey(models.User)

    # Delegate attributes
    track = delegate_to('badge')
    name = delegate_to('badge')
    image = delegate_to('badge')
    description = delegate_to('badge')
    details = delegate_to('badge')
Ejemplo n.º 6
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)
Ejemplo n.º 7
0
class Calendar(models.Model):
    """
    A page that gathers a list of lessons in the course.
    """

    @property
    def course(self):
        return self.get_parent()

    weekly_lessons = delegate_to('course')

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

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

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

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

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

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

    # Wagtail admin
    parent_page_types = ['courses.Classroom']
    subpage_types = ['courses.Lesson']
    content_panels = models.Page.content_panels + [
        panels.InlinePanel(
            'info',
            label=_('Lessons'),
            help_text=_('List of lessons for this course.'),
        ),
    ]
Ejemplo n.º 8
0
class Lesson(models.Page):
    """
    A single lesson in an ordered list.
    """

    class Meta:
        verbose_name = _('Lesson')
        verbose_name_plural = _('Lessons')

    body = models.StreamField([
        ('paragraph', blocks.RichTextBlock()),
    ],
        blank=True,
        null=True
    )

    date = delegate_to('lesson')
    calendar = property(lambda x: x.get_parent())

    def save(self, *args, **kwargs):
        lesson = getattr(self, '_created_for_lesson', None)
        if self.pk is None and lesson is None:
            calendar = lesson.calendar
            ordering = calendar.info.values_list('sort_order', flat=True)
            calendar.lessons.add(Lesson(
                title=self.title,
                page=self,
                sort_order=max(ordering) + 1,
            ))
            calendar.save()

    # Wagtail admin
    parent_page_types = ['courses.Calendar']
    subpage_types = []
    content_panels = models.Page.content_panels + [
        panels.StreamFieldPanel('body'),
    ]
Ejemplo n.º 9
0
class Profile(UserenaBaseProfile, models.Page):
    """
    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')),
        )

    user = models.OneToOneField(
        User,
        unique=True,
        blank=True,
        null=True,
        on_delete=models.SET_NULL,
        verbose_name=_('user'),
        related_name='profile',
    )
    school_id = models.CharField(
        _('school id'),
        help_text=_('Identification number in your school issued id card.'),
        max_length=50,
        blank=True,
        null=True)
    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=[(0, _('male')),
                                               (1, _('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)
    objects = ProfileManager()

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

    @property
    def short_description(self):
        return '%s (id: %s)' % (self.get_full_name_or_username(),
                                self.school_id)

    @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 save(self, *args, **kwargs):
        user = self.user
        if not self.title:
            self.title = self.title or __("%(name)s's profile") % {
                'name': user.get_full_name() or user.username
            }
        if not self.slug:
            self.slug = user.username.replace('.', '-')

        # Set parent page, if necessary
        if not self.path:
            root = ProfileList.objects.instance()
            root.add_child(instance=self)
        else:
            super().save(*args, **kwargs)

    def get_full_name_or_username(self):
        name = self.user.get_full_name()
        if name:
            return name
        else:
            return 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
    parent_page_types = ['ProfileList']
    content_panels = models.Page.content_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'),
    ]
Ejemplo n.º 10
0
class IndividualBase:
    """
    Base class for regular Individual and IndividualProxy objects.
    """

    # Shape
    num_loci = fn_lazy(_.data.shape[0])
    ploidy = fn_lazy(_.data.shape[1])
    dtype = delegate_to('data')
    flatten = delegate_to('data')

    # Biallelic data
    is_biallelic = fn_lazy(_.num_alleles == 2)

    # Missing data
    has_missing = fn_property(lambda _: 0 in _.data)
    missing_data_total = fn_property(lambda _: (_.data == 0).sum())

    @property
    def missing_data_ratio(self):
        return self.missing_data_total / (self.num_loci * self.ploidy)

    # Other
    _allele_names = None
    population = None
    data = None
    admixture_q = None

    # Simple queries
    is_individual = True
    is_population = False

    @lazy
    def num_alleles(self):
        if self.population:
            return self.population.num_alleles
        else:
            return self.data.max()

    @property
    def allele_names(self):
        if self._allele_names is None:
            if self.population:
                return self.population.allele_names
        return self._allele_names

    @lazy
    def admixture_vector(self):
        if self.admixture_q is None:
            return None
        values = sorted(self.admixture_q.items())
        return np.array([y for x, y in values])

    def __getitem__(self, idx):
        return self.data[idx]

    def __iter__(self):
        return iter(self.data)

    def __repr__(self):
        return 'Individual(%r)' % self.render(max_loci=20)

    def __str__(self):
        return self.render()

    def __len__(self):
        return len(self.data)

    def __eq__(self, other):
        if isinstance(other, IndividualBase):
            return (self.data == other.data).all()
        elif isinstance(other, (np.ndarray, list, tuple)):
            return (self.data == other).all()
        else:
            return NotImplemented

    def haplotypes(self):
        """
        Return a sequence of ploidy arrays with each haplotype.

        This operation is a simple transpose of genotype data.
        """

        return self.data.T

    def copy(self, data=None, *, meta=NOT_GIVEN, **kwargs):
        """
        Creates a copy of individual.
        """

        kwargs.setdefault('id', self.id)
        kwargs.setdefault('population', self.population)
        kwargs.setdefault('allele_names', self.allele_names)
        dtype = kwargs.setdefault('dtype', self.dtype)
        kwargs['meta'] = self.meta if meta is NOT_GIVEN else meta
        if data is None:
            data = np.array(self.data, dtype=dtype)
        return Individual(data, **kwargs)

    def render(self, id_align=None, max_loci=None):
        """
        Renders individual genotype.
        """

        # Choose locus rendering function
        if self.allele_names is None:

            def render_locus(idx):
                locus = self[idx]
                return ''.join(map(str, locus))
        else:

            def render_locus(idx):
                locus = self[idx]
                try:
                    mapping = self.allele_names[idx]
                except IndexError:
                    mapping = {}

                return ''.join(str(mapping.get(x, x)) for x in locus)

        size = len(self)
        id_align = -1 if id_align is None else int(id_align)
        data = [('%s:' % (self.id or 'ind')).rjust(id_align + 1)]

        # Select items
        if max_loci and size > max_loci:
            good_idx = set(range(max_loci // 2))
            good_idx.update(range(size - max_loci // 2, size))
        else:
            good_idx = set(range(size))

        # Render locus
        for i in range(len(self)):
            if i in good_idx:
                data.append(render_locus(i))

        # Add ellipsis for large data
        if max_loci and size > max_loci:
            data.insert(max_loci // 2 + 1, '...')

        return ' '.join(data)

    def render_ped(self,
                   family_id='FAM001',
                   individual_id=0,
                   paternal_id=0,
                   maternal_id=0,
                   sex=0,
                   phenotype=0,
                   memo=None):
        """
        Render individual as a line in a plink's .ped file.

        Args:
            family_id:
                A string or number representing the individual's family.
            individual_id:
                A number representing the individual's id.
            paternal_id, maternal_id:
                A number representing the individuals father/mother's id.
            sex:
                The sex (1=male, 2=female, other=unknown).
            phenotype:
                A number representing the optional phenotype.
        """

        data = '  '.join(' '.join(map(str, locus)) for locus in self.data)
        return '%s  %s  %s %s  %s  %s  %s' % (family_id, individual_id,
                                              paternal_id, maternal_id, sex,
                                              phenotype, data)

    def render_csv(self, sep=','):
        """
        Render individual in CSV.
        """

        data = [self.id]
        data.extend(''.join(map(str, x)) for x in self)
        return sep.join(data)

    def breed(self, other, id=None, **kwargs):
        """
        Breeds with other individual.

        Creates a new genotype in which features are selected from both
        parents.
        """

        # Haploid individuals mix parent's genome freely
        if self.ploidy == 1:
            which = np.random.randint(0, 2, size=self.num_loci)
            data1 = self.data[:, 0]
            data2 = other.data[:, 0]
            data = np.where(which, data2, data1)
            data = data[:, None]

        # Diploid create 2 segments for each parent and fuse the results
        elif self.ploidy == 2:
            which = np.random.randint(0, 2, size=self.num_loci)
            data = np.where(which, self.data[:, 0], self.data[:, 1])

            which = np.random.randint(0, 2, size=other.num_loci)
            data_other = np.where(which, other.data[:, 0], other.data[:, 1])

            data = np.stack([data, data_other], axis=1)
        else:
            raise NotImplementedError

        kwargs['id'] = id or id_from_parents(self.id, other.id)
        return self.copy(data, **kwargs)
Ejemplo n.º 11
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'),
    ]
Ejemplo n.º 12
0
 def _update_model(cls):
     for k, v in vars(AttendanceSheet).items():
         if k.startswith('_') or not isinstance(v, FunctionType):
             continue
         setattr(cls, k, delegate_to('attendance_sheet'))
Ejemplo n.º 13
0
class AttendancePage(models.DecoupledAdminPage, models.RoutableViewsPage):
    """
    A Page object that exhibit an attendance sheet.
    """

    rules = Rules()

    @property
    def attendance_sheet(self):
        try:
            return self._attendance_sheet
        except AttributeError:
            pass

        try:
            self._attendance_sheet = self.attendance_sheet_single_list.first()
            return self._attendance_sheet
        except models.ObjectDoesNotExist:
            return None

    @attendance_sheet.setter
    def attendance_sheet(self, value):
        sheet = AttendanceSheetChild(owner=self.owner)
        self.attendance_sheet_single_list = [sheet]
        self._attendance_sheet = sheet

    expiration_interval = delegate_to('attendance_sheet')
    last_event = delegate_to('attendance_sheet')
    max_attempts = delegate_to('attendance_sheet')
    max_string_distance = delegate_to('attendance_sheet')

    @classmethod
    def _update_model(cls):
        for k, v in vars(AttendanceSheet).items():
            if k.startswith('_') or not isinstance(v, FunctionType):
                continue
            setattr(cls, k, delegate_to('attendance_sheet'))

    def clean(self):
        if self.attendance_sheet is None:
            self.attendance_sheet = AttendanceSheet(owner=self.owner)
        self.attendance_sheet.owner = self.attendance_sheet.owner or self.owner
        self.title = str(self.title or _('Attendance sheet'))
        super().clean()

    def get_context(self, request, *args, **kwargs):
        from . import forms

        is_teacher = self.rules.has_perm(request.user,
                                         'attendance.see_passphrase')

        ctx = super().get_context(request)
        ctx['is_teacher'] = is_teacher
        ctx['attendance_sheet'] = self.attendance_sheet
        ctx['form'] = forms.PassphraseForm() if not is_teacher else None
        ctx['passphrase'] = self.current_passphrase() if is_teacher else None
        ctx['is_expired'] = self.is_expired()
        return ctx

    @bricks.rpc.route(r'^check.api/$')
    def check_presence(self, client, passphrase, **kwargs):
        html = ('<div class="cs-attendance-dialog cs-attendance-dialog--%s">'
                '<h1>%s</h1>'
                '<p>%s</p>'
                '</div>')

        if self.is_valid(passphrase):
            html = html % ('success', _('Yay!'), _('Presence confirmed!'))
        else:
            html = html % (
                'failure', _('Oh oh!'),
                _('Could not validate this passphrase :-('))
        client.dialog(html=html)
Ejemplo n.º 14
0
class Profile(models.TimeStampedModel):
    """
    Social information about users.
    """

    GENDER_MALE, GENDER_FEMALE, GENDER_OTHER = 0, 1, 2
    GENDER_CHOICES = [
        (GENDER_MALE, _('Male')),
        (GENDER_FEMALE, _('Female')),
        (GENDER_OTHER, _('Other')),
    ]

    VISIBILITY_PUBLIC, VISIBILITY_FRIENDS, VISIBILITY_HIDDEN = range(3)
    VISIBILITY_CHOICES = enumerate(
        [_('Any Codeschool user'),
         _('Only friends'),
         _('Private')])

    visibility = models.IntegerField(
        _('Visibility'),
        choices=VISIBILITY_CHOICES,
        default=VISIBILITY_FRIENDS,
        help_text=_('Who do you want to share information in your profile?'))
    user = models.OneToOneField(
        User,
        verbose_name=_('user'),
        related_name='profile_ref',
    )
    phone = models.CharField(
        _('Phone'),
        max_length=20,
        blank=True,
        null=True,
    )
    gender = models.SmallIntegerField(
        _('gender'),
        choices=GENDER_CHOICES,
        blank=True,
        null=True,
    )
    date_of_birth = models.DateField(
        _('date of birth'),
        blank=True,
        null=True,
    )
    website = models.URLField(
        _('Website'),
        blank=True,
        null=True,
        help_text=_('A website that is shown publicly in your profile.'))
    about_me = models.RichTextField(
        _('About me'),
        blank=True,
        help_text=_('A small description about yourself.'))

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

    class Meta:
        permissions = (
            ('student', _('Can access/modify data visible to student\'s')),
            ('teacher',
             _('Can access/modify data visible only to Teacher\'s')),
        )

    @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_absolute_url(self):
        self.user.get_absolute_url()
Ejemplo n.º 15
0
class TurtleWidget(QtWidgets.QWidget):
    """
    Main widget of application: it has a GraphicsScene and a ReplEditor
    components. The full application simply wraps this widget inside a window
    with some menus.
    """

    zoomIn = delegate_to('_view')
    zoomOut = delegate_to('_view')
    saveImage = delegate_to('_view')
    flushExecution = delegate_to('_scene')
    increaseFont = delegate_to('_repl_editor')
    decreaseFont = delegate_to('_repl_editor')
    toggleTheme = delegate_to('_repl_editor')
    text = delegate_to('_repl_editor')
    setText = delegate_to('_repl_editor')

    def __init__(self,
                 transpyler,
                 parent=None,
                 text='', header_text=None, **kwds):
        super().__init__(parent=parent)
        assert transpyler

        # Configure scene
        self._scene = TurtleScene()
        self._view = TurtleView(self._scene)

        # Configure editor
        self._transpyler = transpyler
        self._repl_editor = ReplEditor(header_text=header_text,
                                       transpyler=transpyler)
        self._repl_editor.setText(text)
        self._repl_editor.initNamespace()
        self._repl_editor.sizePolicy().setHorizontalPolicy(7)

        # Configure layout
        self._splitter = QtWidgets.QSplitter()
        self._splitter.addWidget(self._view)
        self._splitter.addWidget(self._repl_editor)
        self._layout = QtWidgets.QHBoxLayout(self)
        self._layout.setContentsMargins(0, 0, 0, 0)
        self._layout.addWidget(self._splitter)
        self._splitter.setSizes([200, 120])

        # Connect signals
        self._repl_editor.turtleMessageSignal.connect(self._scene.handleMessage)
        self._scene.messageReplySignal.connect(
            self._repl_editor.handleMessageReply)

    def scene(self):
        return self._scene

    def view(self):
        return self._view

    def namespace(self):
        return self._namespace

    def replEditor(self):
        return self._repl_editor

    def fontZoomIn(self):
        self._repl_editor.zoomIn()

    def fontZoomOut(self):
        self._repl_editor.zoomOut()

    def fontZoomTo(self, factor):
        self._repl_editor.zoomTo(factor)
Ejemplo n.º 16
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)