Newer
Older
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})
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,
participation = models.ForeignKey(Participation,
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,
grade_time = models.DateTimeField(default=now, db_index=True,
flow_session = models.ForeignKey(FlowSession, null=True, blank=True,
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}
def clean(self):
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
# }}}
# {{{ 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
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
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)
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
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)
gopp, created = GradingOpportunity.objects.get_or_create(
course=course,
identifier=grading_rule.grade_identifier,
# Translators: display the name of a flow
_("Flow: %(flow_desc_title)s")
% {"flow_desc_title": flow_desc.title}),
aggregation_strategy=grading_rule.grade_aggregation_strategy,
# {{{ XMPP log
class InstantMessage(models.Model):
participation = models.ForeignKey(Participation,
verbose_name_plural = _("Instant messages")
ordering = ("participation__course", "time")
def __unicode__(self):
return "%s: %s" % (self.participation, self.text)
# }}}
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
# {{{ exam tickets
class Exam(models.Model):
course = models.ForeignKey(Course,
verbose_name=_('Course'))
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,
verbose_name=_('Creation time'))
class Meta:
verbose_name = _("Exam")
verbose_name_plural = _("Exams")
ordering = ("course", "no_exams_before",)
class ExamTicket(models.Model):
exam = models.ForeignKey(Exam,
verbose_name=_('Exam'))
participation = models.ForeignKey(Participation, db_index=True,
verbose_name=_('Participation'))
creator = models.ForeignKey(settings.AUTH_USER_MODEL, null=True,
verbose_name=_('Creator'))
creation_time = models.DateTimeField(default=now,
verbose_name=_('Creation time'))
usage_time = models.DateTimeField(
verbose_name=_('Usage time'))
usage_flow_session = models.ForeignKey(FlowSession,
verbose_name=_('Flow session for which ticket was used'))
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", "exam", "usage_time")
# }}}
# vim: foldmethod=marker