Newer
Older
validate_session_start_rule(ctx, six.text_type(self), rule, tags)
elif self.kind == flow_rule_kind.access:
validate_session_access_rule(ctx, six.text_type(self), rule, tags)
elif self.kind == flow_rule_kind.grading:
validate_session_grading_rule(
ctx, six.text_type(self), rule, tags,
grade_identifier)
# the rule refers to FlowRuleException rule
raise ValidationError(_("invalid rule kind: ")+self.kind)
except ContentValidationError as e:
# the rule refers to FlowRuleException rule
raise ValidationError(_("invalid existing_session_rules: ")+str(e))
verbose_name_plural = _("Flow rule exceptions")
# }}}
# {{{ grading
class GradingOpportunity(models.Model):
verbose_name=_('Course'), on_delete=models.CASCADE)
identifier = models.CharField(max_length=200, blank=False, null=False,
# Translators: format of identifier for GradingOpportunity
name = models.CharField(max_length=200, blank=False, null=False,
ifaint
committed
# Translators: name for GradingOpportunity
help_text=_("A human-readable identifier for the grade."),
flow_id = models.CharField(max_length=200, blank=True, null=True,
help_text=_("Flow identifier that this grading opportunity "
aggregation_strategy = models.CharField(max_length=20,
# Translators: strategy on how the grading of mutiple sessioins
# are aggregated.
due_time = models.DateTimeField(default=None, blank=True, null=True,
creation_time = models.DateTimeField(default=now,
shown_in_grade_book = models.BooleanField(default=True,
shown_in_student_grade_book = models.BooleanField(default=True,
verbose_name=_('Shown in student grade book'))
verbose_name = _("Grading opportunity")
verbose_name_plural = _("Grading opportunities")
ordering = ("course", "due_time", "identifier")
unique_together = (("course", "identifier"),)
def __unicode__(self):
_("%(opportunity_name)s (%(opportunity_id)s) in %(course)s")
% {
"opportunity_name": self.name,
"opportunity_id": self.identifier,
"course": self.course})
if six.PY3:
__str__ = __unicode__
def get_aggregation_strategy_descr(self):
return dict(GRADE_AGGREGATION_STRATEGY_CHOICES).get(
self.aggregation_strategy)
class GradeChange(models.Model):
"""Per 'grading opportunity', each participant may accumulate multiple grades
that are aggregated according to :attr:`GradingOpportunity.aggregation_strategy`.
In addition, for each opportunity, grade changes are grouped by their 'attempt'
identifier, where later grades with the same :attr:`attempt_id` supersede earlier
ones.
"""
opportunity = models.ForeignKey(GradingOpportunity,
verbose_name=_('Grading opportunity'), on_delete=models.CASCADE)
participation = models.ForeignKey(Participation,
verbose_name=_('Participation'), on_delete=models.CASCADE)
state = models.CharField(max_length=50,
choices=GRADE_STATE_CHANGE_CHOICES,
# Translators: something like 'status'.
attempt_id = models.CharField(max_length=50, null=True, blank=True,
# Translators: help text of "attempt_id" in GradeChange class
help_text=_("Grade changes are grouped by their 'attempt ID' "
"where later grades with the same attempt ID supersede earlier "
points = models.DecimalField(max_digits=10, decimal_places=2,
max_points = models.DecimalField(max_digits=10, decimal_places=2,
comment = models.TextField(blank=True, null=True,
due_time = models.DateTimeField(default=None, blank=True, null=True,
creator = models.ForeignKey(settings.AUTH_USER_MODEL, null=True,
verbose_name=_('Creator'), on_delete=models.CASCADE)
grade_time = models.DateTimeField(default=now, db_index=True,
flow_session = models.ForeignKey(FlowSession, null=True, blank=True,
verbose_name=_('Flow session'), on_delete=models.CASCADE)
verbose_name_plural = _("Grade changes")
ordering = ("opportunity", "participation", "grade_time")
def __unicode__(self):
ifaint
committed
# Translators: information for GradeChange
return _("%(participation)s %(state)s on %(opportunityname)s") % {
'participation': self.participation,
'state': self.state,
'opportunityname': self.opportunity.name}
if six.PY3:
__str__ = __unicode__
super(GradeChange, self).clean()
if self.opportunity.course != self.participation.course:
raise ValidationError(_("Participation and opportunity must live "
"in the same course"))
if self.max_points is not None and self.points is not None:
return 100*self.points/self.max_points
else:
return None
def get_state_desc(self):
return dict(GRADE_STATE_CHANGE_CHOICES).get(
self.state)
# }}}
# {{{ grade state machine
class GradeStateMachine(object):
def __init__(self):
self.opportunity = None
self.state = None
self._clear_grades()
self.due_time = None
self.last_report_time = None
# applies to *all* grade changes
self._last_grade_change_time = None
def _clear_grades(self):
self.state = None
self.last_grade_time = None
self.valid_percentages = []
def _consume_grade_change(self, gchange, set_is_superseded):
if self.opportunity is None:
self.opportunity = gchange.opportunity
self.due_time = self.opportunity.due_time
else:
assert self.opportunity.pk == gchange.opportunity.pk
# check that times are increasing
if self._last_grade_change_time is not None:
assert gchange.grade_time > self._last_grade_change_time
self._last_grade_change_time = gchange.grade_time
if gchange.state == grade_state_change_types.graded:
if self.state == grade_state_change_types.unavailable:
raise ValueError(
_("cannot accept grade once opportunity has been "
"marked 'unavailable'"))
if self.state == grade_state_change_types.exempt:
raise ValueError(
_("cannot accept grade once opportunity has been "
#if self.due_time is not None and gchange.grade_time > self.due_time:
#raise ValueError("cannot accept grade after due date")
self.state = gchange.state
gchange.attempt_id in self.attempt_id_to_gchange):
self.attempt_id_to_gchange[gchange.attempt_id] \
self.attempt_id_to_gchange[gchange.attempt_id] \
= gchange
else:
self.valid_percentages.append(gchange.percentage())
self.last_graded_time = gchange.grade_time
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
elif gchange.state == grade_state_change_types.unavailable:
self._clear_grades()
self.state = gchange.state
elif gchange.state == grade_state_change_types.do_over:
self._clear_grades()
elif gchange.state == grade_state_change_types.exempt:
self._clear_grades()
self.state = gchange.state
elif gchange.state == grade_state_change_types.report_sent:
self.last_report_time = gchange.grade_time
elif gchange.state == grade_state_change_types.extension:
self.due_time = gchange.due_time
elif gchange.state in [
grade_state_change_types.grading_started,
grade_state_change_types.retrieved,
]:
pass
else:
raise RuntimeError(
_("invalid grade change state '%s'") % gchange.state)
def consume(self, iterable, set_is_superseded=False):
for gchange in iterable:
gchange.is_superseded = False
self._consume_grade_change(gchange, set_is_superseded)
Andreas Klöckner
committed
valid_grade_changes = sorted(
(gchange
for gchange in self.attempt_id_to_gchange.values()
if gchange.percentage() is not None),
key=lambda gchange: gchange.grade_time)
self.valid_percentages.extend(
gchange.percentage()
Andreas Klöckner
committed
for gchange in valid_grade_changes)
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
return self
def percentage(self):
"""
:return: a percentage of achieved points, or *None*
"""
if self.opportunity is None or not self.valid_percentages:
return None
strategy = self.opportunity.aggregation_strategy
if strategy == grade_aggregation_strategy.max_grade:
return max(self.valid_percentages)
elif strategy == grade_aggregation_strategy.min_grade:
return min(self.valid_percentages)
elif strategy == grade_aggregation_strategy.avg_grade:
return sum(self.valid_percentages)/len(self.valid_percentages)
elif strategy == grade_aggregation_strategy.use_earliest:
return self.valid_percentages[0]
elif strategy == grade_aggregation_strategy.use_latest:
return self.valid_percentages[-1]
else:
raise ValueError(
_("invalid grade aggregation strategy '%s'") % strategy)
def stringify_state(self):
if self.state is None:
elif self.state == grade_state_change_types.exempt:
elif self.state == grade_state_change_types.graded:
if self.valid_percentages:
result = "%.1f%%" % self.percentage()
if len(self.valid_percentages) > 1:
result += " (/%d)" % len(self.valid_percentages)
return result
else:
def stringify_machine_readable_state(self):
if self.state is None:
return u"NONE"
elif self.state == grade_state_change_types.exempt:
return "EXEMPT"
elif self.state == grade_state_change_types.graded:
if self.valid_percentages:
return "%.3f" % self.percentage()
else:
return u"NONE"
else:
return u"OTHER_STATE"
def stringify_percentage(self):
if self.state == grade_state_change_types.graded:
if self.valid_percentages:
return "%.1f" % self.percentage()
else:
return u""
else:
return ""
# }}}
# {{{ flow <-> grading integration
def get_flow_grading_opportunity(course, flow_id, flow_desc, grading_rule):
from course.utils import FlowSessionGradingRule
assert isinstance(grading_rule, FlowSessionGradingRule)
# Translators: display the name of a flow
_("Flow: %(flow_desc_title)s")
gopp, created = GradingOpportunity.objects.get_or_create(
course=course,
identifier=grading_rule.grade_identifier,
name=default_name,
aggregation_strategy=grading_rule.grade_aggregation_strategy,
# update gopp.name when flow_desc.title changed
if not created:
if gopp.name != default_name:
gopp.name = default_name
gopp.save()
# {{{ XMPP log
class InstantMessage(models.Model):
participation = models.ForeignKey(Participation,
verbose_name=_('Participation'), on_delete=models.CASCADE)
verbose_name_plural = _("Instant messages")
ordering = ("participation__course", "time")
def __unicode__(self):
return "%s: %s" % (self.participation, self.text)
if six.PY3:
__str__ = __unicode__
# {{{ exam tickets
class Exam(models.Model):
course = models.ForeignKey(Course,
verbose_name=_('Course'), on_delete=models.CASCADE)
description = models.CharField(max_length=200,
verbose_name=_('Description'))
flow_id = models.CharField(max_length=200,
verbose_name=_('Flow ID'))
active = models.BooleanField(
default=True,
verbose_name=_('Currently active'))
no_exams_before = models.DateTimeField(
verbose_name=_('No exams before'))
no_exams_after = models.DateTimeField(
null=True, blank=True,
class Meta:
verbose_name = _("Exam")
verbose_name_plural = _("Exams")
ordering = ("course", "no_exams_before",)
def __unicode__(self):
return _("Exam %(description)s in %(course)s") % {
'description': self.description,
'course': self.course,
}
if six.PY3:
__str__ = __unicode__
class ExamTicket(models.Model):
exam = models.ForeignKey(Exam,
verbose_name=_('Exam'), on_delete=models.CASCADE)
participation = models.ForeignKey(Participation, db_index=True,
verbose_name=_('Participation'), on_delete=models.CASCADE)
creator = models.ForeignKey(settings.AUTH_USER_MODEL, null=True,
verbose_name=_('Creator'), on_delete=models.CASCADE)
creation_time = models.DateTimeField(default=now,
verbose_name=_('Creation time'))
usage_time = models.DateTimeField(
verbose_name=_('Date and time of first usage of ticket'),
state = models.CharField(max_length=50,
choices=EXAM_TICKET_STATE_CHOICES,
verbose_name=_('Exam ticket state'))
code = models.CharField(max_length=50, db_index=True, unique=True)
class Meta:
verbose_name = _("Exam ticket")
verbose_name_plural = _("Exam tickets")
ordering = ("exam__course", "-creation_time")
permissions = (
("can_issue_exam_tickets", _("Can issue exam tickets to student")),
)
def __unicode__(self):
return _("Exam ticket for %(participation)s in %(exam)s") % {
'participation': self.participation,
'exam': self.exam,
}
if six.PY3:
__str__ = __unicode__
def clean(self):
super(ExamTicket, self).clean()
try:
if self.exam.course != self.participation.course:
raise ValidationError(_("Participation and exam must live "
"in the same course"))
except ObjectDoesNotExist:
pass
# vim: foldmethod=marker