class B(object): x = delegate_to('data') y = delegate_to('data', readonly=True) z = delegate_ro('data') def __init__(self, data): self.data = data
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')
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')
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)
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')
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)
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.'), ), ]
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'), ]
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'), ]
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)
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'), ]
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'))
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)
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()
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)
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)