diff --git a/course/__init__.py b/course/__init__.py
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..1dab58eb27c37a210fe8f076e8ff8680cf9ade32 100644
--- a/course/__init__.py
+++ b/course/__init__.py
@@ -0,0 +1 @@
+default_app_config = 'course.apps.CourseConfig'
\ No newline at end of file
diff --git a/course/apps.py b/course/apps.py
new file mode 100644
index 0000000000000000000000000000000000000000..233c608a914bf4d2262c62589a12b36ab1c19197
--- /dev/null
+++ b/course/apps.py
@@ -0,0 +1,8 @@
+from django.apps import AppConfig
+from django.utils.translation import ugettext_lazy as _
+
+
+class CourseConfig(AppConfig):
+ name = 'course'
+ # for translation of the name of "Course" app displayed in admin.
+ verbose_name = _("Course module")
\ No newline at end of file
diff --git a/course/auth.py b/course/auth.py
index a9a3872fe8d048b889519a41f77a3b11f1516518..18d7715749775907530ffb57600d750c015e3cb1 100644
--- a/course/auth.py
+++ b/course/auth.py
@@ -124,7 +124,8 @@ class ImpersonateForm(StyledForm):
key=lambda user: user.last_name.lower())
],
required=True,
- help_text="Select user to impersonate.")
+ help_text="Select user to impersonate.",
+ label="User")
self.helper.add_input(Submit("submit", "Impersonate",
css_class="col-lg-offset-2"))
@@ -269,7 +270,7 @@ def sign_in_by_user_pw(request):
class SignUpForm(StyledModelForm):
- username = forms.CharField(required=True, max_length=30)
+ username = forms.CharField(required=True, max_length=30, label="Username")
class Meta:
model = User
@@ -352,7 +353,7 @@ def sign_up(request):
class ResetPasswordForm(StyledForm):
- email = forms.EmailField(required=True)
+ email = forms.EmailField(required=True, label="Email")
def __init__(self, *args, **kwargs):
super(ResetPasswordForm, self).__init__(*args, **kwargs)
@@ -412,8 +413,10 @@ def reset_password(request):
class ResetPasswordStage2Form(StyledForm):
- password = forms.CharField(widget=forms.PasswordInput())
- password_repeat = forms.CharField(widget=forms.PasswordInput())
+ password = forms.CharField(widget=forms.PasswordInput(),
+ label="Password")
+ password_repeat = forms.CharField(widget=forms.PasswordInput(),
+ label="Password repeat")
def __init__(self, *args, **kwargs):
super(ResetPasswordStage2Form, self).__init__(*args, **kwargs)
@@ -484,7 +487,7 @@ def reset_password_stage2(request, user_id, sign_in_key):
# {{{ email sign-in flow
class SignInByEmailForm(StyledForm):
- email = forms.EmailField(required=True)
+ email = forms.EmailField(required=True, label="Email")
def __init__(self, *args, **kwargs):
super(SignInByEmailForm, self).__init__(*args, **kwargs)
diff --git a/course/calendar.py b/course/calendar.py
index c271e0890ed73912c6b78ae7d37a071e2ab0e183..01511d2c41476c64656dd51319fbf60f2bb84807 100644
--- a/course/calendar.py
+++ b/course/calendar.py
@@ -24,7 +24,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
"""
-
+from django.utils.translation import ugettext_lazy as _, pgettext_lazy
from django.contrib.auth.decorators import login_required
from course.utils import course_view, render_course_page
from django.core.exceptions import PermissionDenied, ObjectDoesNotExist
@@ -72,17 +72,23 @@ def check_events(pctx):
class RecurringEventForm(StyledForm):
kind = forms.CharField(required=True,
- help_text="Should be lower_case_with_underscores, no spaces allowed.")
+ help_text="Should be lower_case_with_underscores, no spaces allowed.",
+ label=pgettext_lazy("kind of event","Kind of event"))
time = forms.DateTimeField(
widget=DateTimePicker(
- options={"format": "YYYY-MM-DD HH:mm", "sideBySide": True}))
- duration_in_minutes = forms.FloatField(required=False)
+ options={"format": "YYYY-MM-DD HH:mm", "sideBySide": True}),
+ label=pgettext_lazy("starting time of event","Starting time"))
+ duration_in_minutes = forms.FloatField(required=False,
+ label="Duration in minutes")
interval = forms.ChoiceField(required=True,
choices=(
("weekly", "Weekly"),
- ))
- starting_ordinal = forms.IntegerField(required=False)
- count = forms.IntegerField(required=True)
+ ),
+ label=pgettext_lazy("interval of recurring events","Interval"))
+ starting_ordinal = forms.IntegerField(required=False,
+ label=pgettext_lazy("Starting ordinal of recurring events","Starting ordinal"))
+ count = forms.IntegerField(required=True,
+ label=pgettext_lazy("Count of recurring events","Count"))
def __init__(self, *args, **kwargs):
super(RecurringEventForm, self).__init__(*args, **kwargs)
@@ -190,8 +196,10 @@ def create_recurring_events(pctx):
class RenumberEventsForm(StyledForm):
kind = forms.CharField(required=True,
- help_text="Should be lower_case_with_underscores, no spaces allowed.")
- starting_ordinal = forms.IntegerField(required=True, initial=1)
+ help_text="Should be lower_case_with_underscores, no spaces allowed.",
+ label=pgettext_lazy("kind of event","Kind of event"))
+ starting_ordinal = forms.IntegerField(required=True, initial=1,
+ label=pgettext_lazy("Starting ordinal of recurring events","Starting ordinal"))
def __init__(self, *args, **kwargs):
super(RenumberEventsForm, self).__init__(*args, **kwargs)
diff --git a/course/enrollment.py b/course/enrollment.py
index 71ec14db285d5ce370d025461b87b9f9b805c42d..ba6846239bdb78c52ce0187bfc5de0b6cd7f371c 100644
--- a/course/enrollment.py
+++ b/course/enrollment.py
@@ -207,9 +207,11 @@ deny_enrollment.short_description = "Deny enrollment"
class BulkPreapprovalsForm(StyledForm):
role = forms.ChoiceField(
choices=PARTICIPATION_ROLE_CHOICES,
- initial=participation_role.student)
+ initial=participation_role.student,
+ label="Role")
emails = forms.CharField(required=True, widget=forms.Textarea,
- help_text="Enter fully qualified email addresses, one per line.")
+ help_text="Enter fully qualified email addresses, one per line.",
+ label="Emails")
def __init__(self, *args, **kwargs):
super(BulkPreapprovalsForm, self).__init__(*args, **kwargs)
diff --git a/course/flow.py b/course/flow.py
index 1491530aaaf3d46f787a215903374be70539edc3..187de82b2a950534186737cce5a62bc4c6f76831 100644
--- a/course/flow.py
+++ b/course/flow.py
@@ -1372,17 +1372,20 @@ class RegradeFlowForm(StyledForm):
self.fields["flow_id"] = forms.ChoiceField(
choices=[(fid, fid) for fid in flow_ids],
initial=participation_role.student,
- required=True)
+ required=True,
+ label="Flow ID")
self.fields["access_rules_tag"] = forms.CharField(
required=False,
help_text="If non-empty, limit the regrading to sessions started "
- "under this access rules tag.")
+ "under this access rules tag.",
+ label="Access rules tag")
self.fields["regraded_session_in_progress"] = forms.ChoiceField(
choices=(
("any", "Regrade in-progress and not-in-progress sessions"),
("yes", "Regrade in-progress sessions only"),
("no", "Regrade not-in-progress sessions only"),
- ))
+ ),
+ label="Regraded session in progress")
self.helper.add_input(
Submit("regrade", "Regrade", css_class="col-lg-offset-2"))
diff --git a/course/grades.py b/course/grades.py
index 6629115bf5d4cdbfbcd54d9be80427d747efa1a6..965b6587b272f7b1a767f5684450c2c071633c4e 100644
--- a/course/grades.py
+++ b/course/grades.py
@@ -336,12 +336,14 @@ class ModifySessionsForm(StyledForm):
self.fields["rule_tag"] = forms.ChoiceField(
choices=tuple(
(rule_tag, str(rule_tag))
- for rule_tag in session_rule_tags))
+ for rule_tag in session_rule_tags),
+ label="Rule tag")
self.fields["past_due_only"] = forms.BooleanField(
required=False,
initial=True,
help_text="Only act on in-progress sessions that are past "
- "their access rule's due date (applies to 'expire' and 'end')")
+ "their access rule's due date (applies to 'expire' and 'end')",
+ label="Past due only")
self.helper.add_input(
Submit("expire", "Expire sessions",
@@ -644,10 +646,12 @@ class ReopenSessionForm(StyledForm):
[(tag, tag) for tag in tags],
initial=(current_tag
if current_tag is not None
- else NONE_SESSION_TAG))
+ else NONE_SESSION_TAG),
+ label="Set access rules tag")
self.fields["comment"] = forms.CharField(
- widget=forms.Textarea, required=True)
+ widget=forms.Textarea, required=True,
+ label="Comment")
self.helper.add_input(
Submit(
@@ -932,31 +936,39 @@ class ImportGradesForm(StyledForm):
.order_by("identifier")),
help_text="Click to create "
"a new grading opportunity. Reload this form when done."
- % reverse("admin:course_gradingopportunity_add"))
+ % reverse("admin:course_gradingopportunity_add"),
+ label="Grading opportunity")
self.fields["attempt_id"] = forms.CharField(
initial="main",
- required=True)
- self.fields["file"] = forms.FileField()
+ required=True,
+ label="Attempt ID")
+ self.fields["file"] = forms.FileField(
+ label="File")
self.fields["format"] = forms.ChoiceField(
choices=(
("csvhead", "CSV with Header"),
("csv", "CSV"),
- ))
+ ),
+ label="Format")
self.fields["id_column"] = forms.IntegerField(
help_text="1-based column index for the Email or NetID "
"used to locate student record",
- min_value=1)
+ min_value=1,
+ label="User ID column")
self.fields["points_column"] = forms.IntegerField(
help_text="1-based column index for the (numerical) grade",
- min_value=1)
+ min_value=1,
+ label="Points column")
self.fields["feedback_column"] = forms.IntegerField(
help_text="1-based column index for further (textual) feedback",
- min_value=1, required=False)
+ min_value=1, required=False,
+ label="Feedback column")
self.fields["max_points"] = forms.DecimalField(
- initial=100)
+ initial=100,
+ label="Max points")
self.helper.add_input(
Submit("preview", "Preview",
diff --git a/course/im.py b/course/im.py
index e9ba5ce42d5edd03948192fd8d9018c49de86c6a..730899e2383950a80128f21293f9a5e66ff1b6d5 100644
--- a/course/im.py
+++ b/course/im.py
@@ -47,7 +47,8 @@ import threading
# {{{ instant message
class InstantMessageForm(forms.Form):
- message = forms.CharField(required=True, max_length=200)
+ message = forms.CharField(required=True, max_length=200,
+ label="message")
def __init__(self, *args, **kwargs):
self.helper = FormHelper()
@@ -163,10 +164,10 @@ def send_instant_message(pctx):
xmpp = get_xmpp_connection(pctx.course)
if xmpp.is_recipient_online():
- form_text = "Recipient is Online."
+ form_text = "Recipient is Online."
else:
- form_text = "Recipient is Offline."
- form_text = "
%s
" % form_text
+ form_text = "Recipient is Offline."
+ form_text = "%s
" % form_text
if request.method == "POST":
form = InstantMessageForm(request.POST, request.FILES)
diff --git a/course/models.py b/course/models.py
index 19f38beb7ae55102fe13874ed4508196770e0ebe..7075adc9eff83227442e790d6a960a709a8549ab 100644
--- a/course/models.py
+++ b/course/models.py
@@ -29,6 +29,13 @@ from django.contrib.auth.models import User
from django.utils.timezone import now
from django.core.urlresolvers import reverse
from django.core.exceptions import ValidationError, ObjectDoesNotExist
+from django.utils.translation import (
+ ugettext_lazy as _ ,
+ pgettext_lazy,
+ ugettext_noop,
+ string_concat,
+ ugettext,
+ )
from course.constants import ( # noqa
user_status, USER_STATUS_CHOICES,
@@ -53,11 +60,14 @@ class Facility(models.Model):
identifier = models.CharField(max_length=50, unique=True,
help_text="Format is lower-case-with-hyphens. "
- "Do not use spaces.")
- description = models.CharField(max_length=100)
+ "Do not use spaces.",
+ verbose_name = "Facility ID")
+ description = models.CharField(max_length=100,
+ verbose_name = "Facility description")
class Meta:
- verbose_name_plural = "facilities"
+ verbose_name = "Facility"
+ verbose_name_plural = "Facilities"
def __unicode__(self):
return self.identifier
@@ -70,12 +80,13 @@ class FacilityIPRange(models.Model):
ip_range = models.CharField(
max_length=200,
- verbose_name="IP Range")
+ verbose_name="IP range")
- description = models.CharField(max_length=100,)
+ description = models.CharField(max_length=100,
+ verbose_name = 'Ip range description')
class Meta:
- verbose_name = "Facility IP Range"
+ verbose_name = "Facility IP range"
def clean(self):
import ipaddr
@@ -102,12 +113,18 @@ def get_user_status(user):
class UserStatus(models.Model):
- user = models.OneToOneField(User, db_index=True, related_name="user_status")
+ user = models.OneToOneField(User, db_index=True, related_name="user_status",
+ verbose_name = 'User ID')
status = models.CharField(max_length=50,
- choices=USER_STATUS_CHOICES)
+ choices=USER_STATUS_CHOICES,
+ verbose_name = 'User status')
sign_in_key = models.CharField(max_length=50,
- null=True, unique=True, db_index=True, blank=True)
- key_time = models.DateTimeField(default=now)
+ help_text="NEED HELP TEXT",
+ null=True, unique=True, db_index=True, blank=True,
+ verbose_name = 'Sign in key')
+ key_time = models.DateTimeField(default=now,
+ help_text="NEED HELP TEXT",
+ verbose_name = 'Key time')
editor_mode = models.CharField(max_length=20,
choices=(
@@ -116,14 +133,17 @@ class UserStatus(models.Model):
("emacs", "Emacs"),
("vim", "Vim"),
),
- default="default")
+ default="default",
+ # Translators: the text editor used by participants
+ verbose_name = "Editor mode")
class Meta:
- verbose_name_plural = "user statuses"
+ verbose_name = "User status"
+ verbose_name_plural = "User statuses"
ordering = ("key_time",)
def __unicode__(self):
- return "User status for %s" % self.user
+ return "User status for %(user)s" % {'user':self.user}
# }}}
@@ -135,77 +155,98 @@ class Course(models.Model):
help_text="A course identifier. Alphanumeric with dashes, "
"no spaces. This is visible in URLs and determines the location "
"on your file system where the course's git repository lives.",
+ verbose_name = 'Course ID',
db_index=True)
hidden = models.BooleanField(
default=True,
- help_text="Is the course only accessible to course staff?")
+ help_text="Is the course only accessible to course staff?",
+ verbose_name = 'Hidden to student')
listed = models.BooleanField(
default=True,
- help_text="Should the course be listed on the main page?")
+ help_text="Should the course be listed on the main page?",
+ verbose_name = 'Listed on main page')
accepts_enrollment = models.BooleanField(
- default=True)
+ default=True,
+ verbose_name = 'Accepts enrollment')
valid = models.BooleanField(
default=True,
- help_text="Whether the course content has passed validation.")
+ help_text="Whether the course content has passed validation.",
+ verbose_name = 'Valid')
git_source = models.CharField(max_length=200, blank=True,
help_text="A Git URL from which to pull course updates. "
"If you're just starting out, enter "
"git://github.com/inducer/relate-sample "
- "to get some sample content.")
+ "to get some sample content.",
+ verbose_name = 'git source')
ssh_private_key = models.TextField(blank=True,
help_text="An SSH private key to use for Git authentication. "
- "Not needed for the sample URL above.")
+ "Not needed for the sample URL above.",
+ verbose_name = 'SSH private key')
course_file = models.CharField(max_length=200,
default="course.yml",
help_text="Name of a YAML file in the git repository that contains "
- "the root course descriptor.")
+ "the root course descriptor.",
+ verbose_name = 'Course file')
events_file = models.CharField(max_length=200,
default="events.yml",
help_text="Name of a YAML file in the git repository that contains "
- "calendar information.")
+ "calendar information.",
+ verbose_name = 'Events file')
enrollment_approval_required = models.BooleanField(
default=False,
help_text="If set, each enrolling student must be "
- "individually approved.")
+ "individually approved.",
+ verbose_name = 'Enrollment approval required')
enrollment_required_email_suffix = models.CharField(
max_length=200, blank=True, null=True,
help_text="Enrollee's email addresses must end in the "
- "specified suffix, such as '@illinois.edu'.")
+ "specified suffix, such as '@illinois.edu'.",
+ verbose_name = 'Enrollment required email suffix')
from_email = models.EmailField(
help_text="This email address will be used in the 'From' line "
- "of automated emails sent by RELATE.")
+ "of automated emails sent by RELATE."),
+ verbose_name = 'From email')
notify_email = models.EmailField(
help_text="This email address will receive "
- "notifications about the course.")
+ "notifications about the course.",
+ verbose_name = 'Notify email')
# {{{ XMPP
course_xmpp_id = models.CharField(max_length=200, blank=True, null=True,
help_text="(Required only if the instant message feature is desired.) "
"The Jabber/XMPP ID (JID) the course will use to sign in to an "
- "XMPP server.")
+ "XMPP server.",
+ verbose_name = 'Course xmpp ID')
course_xmpp_password = models.CharField(max_length=200, blank=True, null=True,
help_text="(Required only if the instant message feature is desired.) "
- "The password to go with the JID above.")
+ "The password to go with the JID above.",
+ verbose_name = 'Course xmpp password')
recipient_xmpp_id = models.CharField(max_length=200, blank=True, null=True,
help_text="(Required only if the instant message feature is desired.) "
- "The JID to which instant messages will be sent.")
+ "The JID to which instant messages will be sent.",
+ verbose_name = 'Recipient xmpp ID')
# }}}
active_git_commit_sha = models.CharField(max_length=200, null=False,
- blank=False)
+ blank=False,
+ verbose_name = 'Active git commit sha')
participants = models.ManyToManyField(User,
through='Participation')
+ class Meta:
+ verbose_name = "Course"
+ verbose_name_plural = "Courses"
+
def __unicode__(self):
return self.identifier
@@ -222,21 +263,29 @@ class Event(models.Model):
course content.
"""
- course = models.ForeignKey(Course)
+ course = models.ForeignKey(Course,
+ verbose_name = 'Course ID')
kind = models.CharField(max_length=50,
- help_text="Should be lower_case_with_underscores, no spaces allowed.")
- ordinal = models.IntegerField(blank=True, null=True)
-
- time = models.DateTimeField()
- end_time = models.DateTimeField(null=True, blank=True)
-
+ help_text="Should be lower_case_with_underscores, no spaces allowed.",
+ verbose_name = 'Kind of event')
+ ordinal = models.IntegerField(blank=True, null=True,
+ verbose_name = 'Ordinal of event')
+
+ time = models.DateTimeField(verbose_name = 'Start time')
+ end_time = models.DateTimeField(null=True, blank=True,
+ verbose_name = 'End time')
all_day = models.BooleanField(default=False,
+ # Translators: for when the due time is "All day", how the webpage of a event is displayed.
help_text="Only affects the rendering in the class calendar, "
- "in that a start time is not shown")
+ "in that a start time is not shown",
+ verbose_name = 'All day')
- shown_in_calendar = models.BooleanField(default=True)
+ shown_in_calendar = models.BooleanField(default=True,
+ verbose_name = 'Shown in calendar')
class Meta:
+ verbose_name = "Event"
+ verbose_name_plural = "Events"
ordering = ("course", "time")
unique_together = (("course", "kind", "ordinal"))
@@ -252,72 +301,97 @@ class Event(models.Model):
# {{{ participation
class ParticipationTag(models.Model):
- course = models.ForeignKey(Course)
-
+ course = models.ForeignKey(Course,
+ verbose_name = 'Course ID')
name = models.CharField(max_length=100, unique=True,
help_text="Format is lower-case-with-hyphens. "
- "Do not use spaces.")
+ "Do not use spaces.",
+ verbose_name = 'Name of ParticipationTag')
def clean(self):
import re
name_valid_re = re.compile(r"^\w+$")
if name_valid_re.match(self.name) is None:
+ # Translators: "Name" is the name of a ParticipationTag
raise ValidationError({"name": "Name contains invalid characters."})
def __unicode__(self):
return "%s (%s)" % (self.name, self.course)
class Meta:
+ verbose_name = "Participation tag"
+ verbose_name_plural = "Participation tags"
unique_together = (("course", "name"),)
ordering = ("course", "name")
class Participation(models.Model):
- user = models.ForeignKey(User)
- course = models.ForeignKey(Course, related_name="participations")
+ user = models.ForeignKey(User,
+ verbose_name = 'User ID')
+ course = models.ForeignKey(Course, related_name="participations",
+ verbose_name = 'Course ID')
- enroll_time = models.DateTimeField(default=now)
+ enroll_time = models.DateTimeField(default=now,
+ verbose_name = 'Enroll time')
role = models.CharField(max_length=50,
choices=PARTICIPATION_ROLE_CHOICES,
help_text="Instructors may update course content. "
"Teaching assistants may access and change grade data. "
"Observers may access analytics. "
- "Each role includes privileges from subsequent roles.")
+ "Each role includes privileges from subsequent roles.",
+ verbose_name = 'Participation role')
status = models.CharField(max_length=50,
- choices=PARTICIPATION_STATUS_CHOICES)
+ choices=PARTICIPATION_STATUS_CHOICES,
+ verbose_name = 'Participation status')
time_factor = models.DecimalField(
max_digits=10, decimal_places=2,
- default=1)
+ default=1,
+ help_text="NEED HELP TEXT",
+ verbose_name = 'Time factor')
preview_git_commit_sha = models.CharField(max_length=200, null=True,
- blank=True)
+ blank=True,
+ verbose_name = 'Preview git commit sha')
- tags = models.ManyToManyField(ParticipationTag, blank=True)
+ tags = models.ManyToManyField(ParticipationTag, blank=True,
+ help_text="NEED HELP TEXT",
+ verbose_name = 'Tags')
def __unicode__(self):
- return "%s in %s as %s" % (
- self.user, self.course, self.role)
+ # Translators: displayed format of Participation: some user in some course as some role
+ return "%(user)s in %(course)s as %(role)s" % {
+ 'user':self.user, 'course':self.course, 'role':dict(PARTICIPATION_ROLE_CHOICES).get(self.role).lower()}
class Meta:
+ verbose_name = "Participation"
+ verbose_name_plural = "Participations"
unique_together = (("user", "course"),)
ordering = ("course", "user")
class ParticipationPreapproval(models.Model):
- email = models.EmailField(max_length=254)
- course = models.ForeignKey(Course)
+ email = models.EmailField(max_length=254,
+ verbose_name = 'Email')
+ course = models.ForeignKey(Course,
+ verbose_name = 'Course ID')
role = models.CharField(max_length=50,
- choices=PARTICIPATION_ROLE_CHOICES)
+ choices=PARTICIPATION_ROLE_CHOICES,
+ verbose_name = 'Role')
- creator = models.ForeignKey(User, null=True)
- creation_time = models.DateTimeField(default=now, db_index=True)
+ creator = models.ForeignKey(User, null=True,
+ verbose_name = 'Creator')
+ creation_time = models.DateTimeField(default=now, db_index=True,
+ verbose_name = 'Creation time')
def __unicode__(self):
- return "%s in %s" % (self.email, self.course)
+ # Translators: somebody's email in some course in Particiaption Preapproval
+ return "%(email)s in %(course)s" % {"email": self.email, "course": self.course}
class Meta:
+ verbose_name = "Participation preapproval"
+ verbose_name_plural = "Participation preapprovals"
unique_together = (("course", "email"),)
ordering = ("course", "email")
@@ -325,34 +399,52 @@ class ParticipationPreapproval(models.Model):
class InstantFlowRequest(models.Model):
- course = models.ForeignKey(Course)
- flow_id = models.CharField(max_length=200)
- start_time = models.DateTimeField(default=now)
- end_time = models.DateTimeField()
- cancelled = models.BooleanField(default=False)
-
+ course = models.ForeignKey(Course,
+ verbose_name = 'Course ID')
+ flow_id = models.CharField(max_length=200,
+ verbose_name = 'Flow ID')
+ start_time = models.DateTimeField(default=now,
+ verbose_name = 'Start time')
+ end_time = models.DateTimeField(
+ verbose_name = 'End time')
+ cancelled = models.BooleanField(default=False,
+ verbose_name = 'Cancelled')
+
+ class Meta:
+ verbose_name = "Instant flow request"
+ verbose_name_plural = "Instant flow requests"
# {{{ flow session
class FlowSession(models.Model):
# This looks like it's redundant with 'participation', below--but it's not.
# 'participation' is nullable.
- course = models.ForeignKey(Course)
+ course = models.ForeignKey(Course,
+ verbose_name = 'Course ID')
participation = models.ForeignKey(Participation, null=True, blank=True,
- db_index=True)
- active_git_commit_sha = models.CharField(max_length=200)
- flow_id = models.CharField(max_length=200, db_index=True)
- start_time = models.DateTimeField(default=now)
- completion_time = models.DateTimeField(null=True, blank=True)
- page_count = models.IntegerField(null=True, blank=True)
-
- in_progress = models.BooleanField(default=None)
+ db_index=True,
+ verbose_name = 'Participation')
+ active_git_commit_sha = models.CharField(max_length=200,
+ verbose_name = 'Active git commit sha')
+ flow_id = models.CharField(max_length=200, db_index=True,
+ verbose_name = 'Flow ID')
+ start_time = models.DateTimeField(default=now,
+ verbose_name = 'Start time')
+ completion_time = models.DateTimeField(null=True, blank=True,
+ verbose_name = 'Completition time')
+ page_count = models.IntegerField(null=True, blank=True,
+ verbose_name = 'Page count')
+
+ in_progress = models.BooleanField(default=None,
+ verbose_name = 'In progress')
access_rules_tag = models.CharField(max_length=200, null=True,
- blank=True)
+ blank=True,
+ verbose_name = 'Access rules tag')
expiration_mode = models.CharField(max_length=20, null=True,
default=flow_session_expiration_mode.end,
- choices=FLOW_SESSION_EXPIRATION_MODE_CHOICES)
+ choices=FLOW_SESSION_EXPIRATION_MODE_CHOICES,
+ verbose_name = 'Expiration mode')
# Non-normal: These fields can be recomputed, albeit at great expense.
#
@@ -360,24 +452,29 @@ class FlowSession(models.Model):
# some flow sessions are not for credit and still have results.
points = models.DecimalField(max_digits=10, decimal_places=2,
- blank=True, null=True)
+ blank=True, null=True,
+ verbose_name = 'Points')
max_points = models.DecimalField(max_digits=10, decimal_places=2,
- blank=True, null=True)
- result_comment = models.TextField(blank=True, null=True)
+ blank=True, null=True,
+ verbose_name = 'Max point')
+ result_comment = models.TextField(blank=True, null=True,
+ verbose_name = 'Result comment')
class Meta:
+ verbose_name = "Flow session"
+ verbose_name_plural = "Flow sessions"
ordering = ("course", "-start_time")
def __unicode__(self):
if self.participation is None:
- return "anonymous session %d on '%s'" % (
- self.id,
- self.flow_id)
+ return "anonymous session %(session_id)d on '%(flow_id)s'" % {
+ 'session_id':self.id,
+ 'flow_id':self.flow_id}
else:
- return "%s's session %d on '%s'" % (
- self.participation.user,
- self.id,
- self.flow_id)
+ return "%(user)s's session %(session_id)d on '%(flow_id)s'" % {
+ 'user':self.participation.user,
+ 'session_id':self.id,
+ 'flow_id':self.flow_id}
def append_comment(self, s):
if s is None:
@@ -419,23 +516,32 @@ class FlowSession(models.Model):
# {{{ flow page data
class FlowPageData(models.Model):
- flow_session = models.ForeignKey(FlowSession, related_name="page_data")
- ordinal = models.IntegerField(null=True, blank=True)
+ flow_session = models.ForeignKey(FlowSession, related_name="page_data",
+ verbose_name = 'Flow session')
+ ordinal = models.IntegerField(null=True, blank=True,
+ verbose_name = 'Ordinal')
- group_id = models.CharField(max_length=200)
- page_id = models.CharField(max_length=200)
+ group_id = models.CharField(max_length=200,
+ verbose_name = 'Group ID')
+ page_id = models.CharField(max_length=200,
+ verbose_name = 'Page ID')
- data = JSONField(null=True, blank=True)
+ data = JSONField(null=True, blank=True,
+ # Show correct characters in admin for non ascii languages.
+ dump_kwargs={'ensure_ascii': False},
+ verbose_name = 'Data')
class Meta:
- verbose_name_plural = "flow page data"
+ verbose_name = "Flow page data"
+ verbose_name_plural = "Flow page data"
def __unicode__(self):
- return "Data for page '%s/%s' (ordinal %s) in %s" % (
- self.group_id,
- self.page_id,
- self.ordinal,
- self.flow_session)
+ # flow page data
+ return "Data for page '%(group_id)s/%(page_id)s' (ordinal %(ordinal)s) in %(flow_session)s" % {
+ 'group_id':self.group_id,
+ 'page_id':self.page_id,
+ 'ordinal':self.ordinal,
+ 'flow_session':self.flow_session}
# Django's templates are a little daft. No arithmetic--really?
def previous_ordinal(self):
@@ -453,15 +559,25 @@ class FlowPageVisit(models.Model):
# This is redundant (because the FlowSession is available through
# page_data), but it helps the admin site understand the link
# and provide editing.
- flow_session = models.ForeignKey(FlowSession, db_index=True)
-
- page_data = models.ForeignKey(FlowPageData, db_index=True)
- visit_time = models.DateTimeField(default=now, db_index=True)
- remote_address = models.GenericIPAddressField(null=True, blank=True)
-
- is_synthetic = models.BooleanField(default=False)
-
- answer = JSONField(null=True, blank=True)
+ flow_session = models.ForeignKey(FlowSession, db_index=True,
+ verbose_name = 'Flow session')
+
+ page_data = models.ForeignKey(FlowPageData, db_index=True,
+ verbose_name = 'Page data')
+ visit_time = models.DateTimeField(default=now, db_index=True,
+ verbose_name = 'Visit time')
+ remote_address = models.GenericIPAddressField(null=True, blank=True,
+ verbose_name = 'Remote address')
+
+ is_synthetic = models.BooleanField(default=False,
+ help_text="NEED HELP TEXT"
+ verbose_name = 'Is synthetic')
+
+ answer = JSONField(null=True, blank=True,
+ # Show correct characters in admin for non ascii languages.
+ dump_kwargs={'ensure_ascii': False},
+ # Translators: "Answer" is a Noun.
+ verbose_name = 'Answer')
# is_submitted_answer may seem redundant with answers being
# non-NULL, but it isn't. This supports saved (but as
@@ -471,21 +587,27 @@ class FlowPageVisit(models.Model):
# (Should coincide with 'answer is None')
# True means it's a final, submitted answer fit for grading.
# False means it's just a saved answer.
- is_submitted_answer = models.NullBooleanField()
+ is_submitted_answer = models.NullBooleanField(
+ # Translators: determine whether the answer is a final, submitted answer fit for grading.
+ verbose_name = 'Is submitted answer')
def __unicode__(self):
- result = "'%s/%s' in '%s' on %s" % (
- self.page_data.group_id,
- self.page_data.page_id,
- self.flow_session,
- self.visit_time)
+ # Translators: flow page visit
+ result = "'%(group_id)s/%(page_id)s' in '%(session)s' on %(time)s" % {
+ 'group_id':self.page_data.group_id,
+ 'page_id':self.page_data.page_id,
+ 'session':self.flow_session,
+ 'time':self.visit_time}
if self.answer is not None:
- result += " (with answer)"
+ # Translators: flow page visit: if the answer is provided by user then append the string.
+ result += unicode(" (with answer)")
return result
class Meta:
+ verbose_name = "Flow page visit"
+ verbose_name_plural = "Flow page visits"
# These must be distinguishable, to figure out what came later.
unique_together = (("page_data", "visit_time"),)
@@ -511,34 +633,49 @@ class FlowPageVisit(models.Model):
# {{{ flow page visit grade
class FlowPageVisitGrade(models.Model):
- visit = models.ForeignKey(FlowPageVisit, related_name="grades")
+ visit = models.ForeignKey(FlowPageVisit, related_name="grades",
+ verbose_name = 'Visit')
# NULL means 'autograded'
- grader = models.ForeignKey(User, null=True, blank=True)
- grade_time = models.DateTimeField(db_index=True, default=now)
+ grader = models.ForeignKey(User, null=True, blank=True,
+ verbose_name = 'Grader')
+ grade_time = models.DateTimeField(db_index=True, default=now,
+ verbose_name = 'Grade time')
graded_at_git_commit_sha = models.CharField(
- max_length=200, null=True, blank=True)
+ max_length=200, null=True, blank=True,
+ verbose_name = 'Graded at git commit sha')
- grade_data = JSONField(null=True, blank=True)
+ grade_data = JSONField(null=True, blank=True,
+ # Show correct characters in admin for non ascii languages.
+ dump_kwargs={'ensure_ascii': False},
+ verbose_name = 'Grade data')
# This data should be recomputable, but we'll cache it here,
# because it might be very expensive (container-launch expensive
# for code questions, for example) to recompute.
max_points = models.FloatField(null=True, blank=True,
+ # Translators: max point in grade
help_text="Point value of this question when receiving "
- "full credit.")
+ "full credit.",
+ verbose_name = 'Max points')
correctness = models.FloatField(null=True, blank=True,
+ # Translators: correctness in grade
help_text="Real number between zero and one (inclusively) "
- "indicating the degree of correctness of the answer.")
+ "indicating the degree of correctness of the answer.",
+ verbose_name = 'Correctness')
# This JSON object has fields corresponding to
# :class:`course.page.AnswerFeedback`, except for
# :attr:`course.page.AnswerFeedback.correctness`, which is stored
# separately for efficiency.
- feedback = JSONField(null=True, blank=True)
+ feedback = JSONField(null=True, blank=True,
+ # Show correct characters in admin for non ascii languages.
+ dump_kwargs={'ensure_ascii': False},
+ # Translators: "Feedback" stands for the feedback of answers.
+ verbose_name = 'Feedback')
def percentage(self):
if self.correctness is not None:
@@ -553,23 +690,31 @@ class FlowPageVisitGrade(models.Model):
return None
class Meta:
+ verbose_name = "Flow page visit grade"
+ verbose_name_plural = "Flow page visit grades"
# These must be distinguishable, to figure out what came later.
unique_together = (("visit", "grade_time"),)
ordering = ("visit", "grade_time")
def __unicode__(self):
- return "grade of %s: %s" % (
- self.visit, self.percentage())
+ # information on FlowPageVisitGrade class
+ return "grade of %(visit)s: %(percentage)s" % {
+ 'visit':self.visit, 'percentage':self.percentage()}
class FlowPageBulkFeedback(models.Model):
# We're only storing one of these per page, because
# they're 'bulk' (i.e. big, like plots or program output)
- page_data = models.OneToOneField(FlowPageData)
- grade = models.ForeignKey(FlowPageVisitGrade)
+ page_data = models.OneToOneField(FlowPageData,
+ verbose_name = 'Page data')
+ grade = models.ForeignKey(FlowPageVisitGrade,
+ verbose_name = 'Grade')
- bulk_feedback = JSONField(null=True, blank=True)
+ bulk_feedback = JSONField(null=True, blank=True,
+ # Show correct characters in admin for non ascii languages.
+ dump_kwargs={'ensure_ascii': False},
+ verbose_name = 'Bulk feedback')
def update_bulk_feedback(page_data, grade, bulk_feedback_json):
@@ -626,44 +771,60 @@ def validate_stipulations(stip):
class FlowAccessException(models.Model):
# deprecated
- participation = models.ForeignKey(Participation, db_index=True)
- flow_id = models.CharField(max_length=200, blank=False, null=False)
- expiration = models.DateTimeField(blank=True, null=True)
+ participation = models.ForeignKey(Participation, db_index=True,
+ verbose_name = 'Participation')
+ flow_id = models.CharField(max_length=200, blank=False, null=False,
+ verbose_name = 'Flow ID')
+ expiration = models.DateTimeField(blank=True, null=True,
+ verbose_name = 'Expiration')
stipulations = JSONField(blank=True, null=True,
+ # Translators: help text for stipulations in FlowAccessException (deprecated)
help_text="A dictionary of the same things that can be added "
"to a flow access rule, such as allowed_session_count or "
"credit_percent. If not specified here, values will default "
"to the stipulations in the course content.",
- validators=[validate_stipulations])
+ validators=[validate_stipulations],
+ dump_kwargs={'ensure_ascii': False},
+ verbose_name = 'Stipulations')
- creator = models.ForeignKey(User, null=True)
- creation_time = models.DateTimeField(default=now, db_index=True)
+ creator = models.ForeignKey(User, null=True,
+ verbose_name = 'Creator')
+ creation_time = models.DateTimeField(default=now, db_index=True,
+ verbose_name = 'Creation time')
is_sticky = models.BooleanField(
default=False,
+ # Translators: deprecated
help_text="Check if a flow started under this "
"exception rule set should stay "
- "under this rule set until it is expired.")
+ "under this rule set until it is expired.",
+ # Translators: deprecated
+ verbose_name = 'Is sticky')
- comment = models.TextField(blank=True, null=True)
+ comment = models.TextField(blank=True, null=True,
+ verbose_name = 'Comment')
def __unicode__(self):
- return "Access exception for '%s' to '%s' in '%s'" % (
- self.participation.user, self.flow_id,
- self.participation.course)
+ # Translators: flow access exception in admin (deprecated)
+ return "Access exception for '%(user)s' to '%(flow_id)s' in '%(course)s'" % {
+ 'user':self.participation.user, 'flow_id':self.flow_id,
+ 'course':self.participation.course}
class FlowAccessExceptionEntry(models.Model):
# deprecated
exception = models.ForeignKey(FlowAccessException,
- related_name="entries")
+ related_name="entries",
+ verbose_name = 'Exception')
permission = models.CharField(max_length=50,
- choices=FLOW_PERMISSION_CHOICES)
+ choices=FLOW_PERMISSION_CHOICES,
+ verbose_name = 'Permission')
class Meta:
- verbose_name_plural = "flow access exception entries"
+ # Translators: FlowAccessExceptionEntry (deprecated)
+ verbose_name_plural = "Flow access exception entries"
def __unicode__(self):
return self.permission
@@ -672,25 +833,35 @@ class FlowAccessExceptionEntry(models.Model):
class FlowRuleException(models.Model):
- flow_id = models.CharField(max_length=200, blank=False, null=False)
- participation = models.ForeignKey(Participation, db_index=True)
- expiration = models.DateTimeField(blank=True, null=True)
+ flow_id = models.CharField(max_length=200, blank=False, null=False,
+ verbose_name = 'Flow ID')
+ participation = models.ForeignKey(Participation, db_index=True,
+ verbose_name = 'Participation')
+ expiration = models.DateTimeField(blank=True, null=True,
+ verbose_name = 'Expiration')
- creator = models.ForeignKey(User, null=True)
- creation_time = models.DateTimeField(default=now, db_index=True)
+ creator = models.ForeignKey(User, null=True,
+ verbose_name = 'Creator')
+ creation_time = models.DateTimeField(default=now, db_index=True,
+ verbose_name = 'Creation time')
- comment = models.TextField(blank=True, null=True)
+ comment = models.TextField(blank=True, null=True,
+ verbose_name = 'Comment')
kind = models.CharField(max_length=50, blank=False, null=False,
- choices=FLOW_RULE_KIND_CHOICES)
- rule = YAMLField(blank=False, null=False)
- active = models.BooleanField(default=True)
+ choices=FLOW_RULE_KIND_CHOICES,
+ verbose_name = 'Kind')
+ rule = YAMLField(blank=False, null=False,
+ verbose_name = 'Rule')
+ active = models.BooleanField(default=True,
+ verbose_name = pgettext_lazy("is the flow rule exception activated?","Active"))
def __unicode__(self):
- return "%s exception for '%s' to '%s' in '%s'" % (
- self.kind,
- self.participation.user, self.flow_id,
- self.participation.course)
+ # Translators: For FlowRuleException
+ return "%(kind)s exception for '%(user)s' to '%(flow_id)s' in '%(course)s'" % {
+ 'kind':self.kind,
+ 'user':self.participation.user, 'flow_id':self.flow_id,
+ 'course':self.participation.course}
def clean(self):
if (self.kind == flow_rule_kind.grading
@@ -733,44 +904,65 @@ class FlowRuleException(models.Model):
elif self.kind == flow_rule_kind.grading:
validate_session_grading_rule(ctx, unicode(self), rule, tags)
else:
+ # 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))
+ class Meta:
+ verbose_name = "Flow rule exception"
+ verbose_name_plural = "Flow rule exceptions"
+
# }}}
# {{{ grading
class GradingOpportunity(models.Model):
- course = models.ForeignKey(Course)
+ course = models.ForeignKey(Course,
+ verbose_name = 'Course ID')
identifier = models.CharField(max_length=200, blank=False, null=False,
+ # Translators: format of identifier for GradingOpportunity
help_text="A symbolic name for this grade. "
- "lower_case_with_underscores, no spaces.")
+ "lower_case_with_underscores, no spaces.",
+ verbose_name = 'Grading opportunity ID')
name = models.CharField(max_length=200, blank=False, null=False,
- help_text="A human-readable identifier for the grade.")
+ # Translators: name for GradingOpportunity
+ help_text="A human-readable identifier for the grade.",
+ verbose_name = 'Grading opportunity name')
flow_id = models.CharField(max_length=200, blank=True, null=True,
help_text="Flow identifier that this grading opportunity "
- "is linked to, if any")
+ "is linked to, if any",
+ verbose_name = 'Flow ID')
aggregation_strategy = models.CharField(max_length=20,
- choices=GRADE_AGGREGATION_STRATEGY_CHOICES)
+ choices=GRADE_AGGREGATION_STRATEGY_CHOICES,
+ # Translators: strategy on how the grading of mutiple sessioins are aggregated.
+ verbose_name = 'Aggregation strategy')
- due_time = models.DateTimeField(default=None, blank=True, null=True)
- creation_time = models.DateTimeField(default=now)
+ due_time = models.DateTimeField(default=None, blank=True, null=True,
+ verbose_name = 'Due time')
+ creation_time = models.DateTimeField(default=now,
+ verbose_name = 'Creation time')
- shown_in_grade_book = models.BooleanField(default=True)
- shown_in_student_grade_book = models.BooleanField(default=True)
+ shown_in_grade_book = models.BooleanField(default=True,
+ verbose_name = 'Shown in grade book')
+ shown_in_student_grade_book = models.BooleanField(default=True,
+ verbose_name = 'Shown in student grade book')
class Meta:
- verbose_name_plural = "grading opportunities"
+ verbose_name = "Grading opportunity"
+ verbose_name_plural = "Grading opportunities"
ordering = ("course", "due_time", "identifier")
unique_together = (("course", "identifier"),)
def __unicode__(self):
- return "%s (%s) in %s" % (self.name, self.identifier, self.course)
+ # Translators: For GradingOpportunity
+ return "%(opportunit_name)s (%(opportunit_id)s) in %(course)s" % {
+ 'opportunit_name':self.name, 'opportunit_id':self.identifier, 'course':self.course}
class GradeChange(models.Model):
@@ -781,39 +973,57 @@ class GradeChange(models.Model):
identifier, where later grades with the same :attr:`attempt_id` supersede earlier
ones.
"""
- opportunity = models.ForeignKey(GradingOpportunity)
+ opportunity = models.ForeignKey(GradingOpportunity,
+ verbose_name = 'Grading opportunity')
- participation = models.ForeignKey(Participation)
+ participation = models.ForeignKey(Participation,
+ verbose_name = 'Participation')
state = models.CharField(max_length=50,
- choices=GRADE_STATE_CHANGE_CHOICES)
+ choices=GRADE_STATE_CHANGE_CHOICES,
+ # Translators: something like 'status'.
+ verbose_name = 'State')
attempt_id = models.CharField(max_length=50, null=True, blank=True,
default="main",
+ # 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 "
- "ones.")
+ "ones.",
+ verbose_name = 'Attempt ID')
points = models.DecimalField(max_digits=10, decimal_places=2,
- blank=True, null=True)
- max_points = models.DecimalField(max_digits=10, decimal_places=2)
+ blank=True, null=True,
+ verbose_name = 'Points')
+ max_points = models.DecimalField(max_digits=10, decimal_places=2,
+ verbose_name = 'Max points')
- comment = models.TextField(blank=True, null=True)
+ comment = models.TextField(blank=True, null=True,
+ verbose_name = 'Comment')
- due_time = models.DateTimeField(default=None, blank=True, null=True)
+ due_time = models.DateTimeField(default=None, blank=True, null=True,
+ verbose_name = 'Due time')
- creator = models.ForeignKey(User, null=True)
- grade_time = models.DateTimeField(default=now, db_index=True)
+ creator = models.ForeignKey(User, null=True,
+ verbose_name = 'Creator')
+ grade_time = models.DateTimeField(default=now, db_index=True,
+ verbose_name = 'Grade time')
flow_session = models.ForeignKey(FlowSession, null=True, blank=True,
- related_name="grade_changes")
+ related_name="grade_changes",
+ verbose_name = 'Flow session')
class Meta:
+ verbose_name = "Grade change"
+ verbose_name_plural = "Grade changes"
ordering = ("opportunity", "participation", "grade_time")
def __unicode__(self):
- return "%s %s on %s" % (self.participation, self.state,
- self.opportunity.name)
+ # 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):
if self.opportunity.course != self.participation.course:
@@ -997,7 +1207,8 @@ def get_flow_grading_opportunity(course, flow_id, flow_desc, grading_rule):
course=course,
identifier=grading_rule.grade_identifier,
defaults=dict(
- name="Flow: %s" % flow_desc.title,
+ # Translators: name of flow with prefix "Flow"
+ name="Flow: %(flow_desc_title)s" % {"flow_desc_title":flow_desc.title},
flow_id=flow_id,
aggregation_strategy=grading_rule.grade_aggregation_strategy,
))
@@ -1010,11 +1221,16 @@ def get_flow_grading_opportunity(course, flow_id, flow_desc, grading_rule):
# {{{ XMPP log
class InstantMessage(models.Model):
- participation = models.ForeignKey(Participation)
- text = models.CharField(max_length=200)
- time = models.DateTimeField(default=now)
+ participation = models.ForeignKey(Participation,
+ verbose_name = 'Participation')
+ text = models.CharField(max_length=200,
+ verbose_name = 'Text')
+ time = models.DateTimeField(default=now,
+ verbose_name = 'Time')
class Meta:
+ verbose_name = "Instant message"
+ verbose_name_plural = "Instant messages"
ordering = ("participation__course", "time")
def __unicode__(self):
diff --git a/course/page/base.py b/course/page/base.py
index 5ea8387d22363b1e6a8691c705458205f7d316f8..484176399e598f5bfcc65297a211a1197a71ac82 100644
--- a/course/page/base.py
+++ b/course/page/base.py
@@ -530,7 +530,8 @@ class HumanTextFeedbackForm(StyledForm):
self.fields["released"] = forms.BooleanField(
initial=True, required=False,
help_text="Whether the grade and feedback below are to be shown "
- "to student")
+ "to student",
+ label="Released")
self.fields["grade_percent"] = forms.FloatField(
min_value=0,
max_value=100 * MAX_EXTRA_CREDIT_FACTOR,
@@ -538,7 +539,8 @@ class HumanTextFeedbackForm(StyledForm):
required=False,
# avoid unfortunate scroll wheel accidents reported by graders
- widget=forms.TextInput)
+ widget=forms.TextInput,
+ label="Grade percent")
if point_value is not None:
self.fields["grade_points"] = forms.FloatField(
@@ -549,7 +551,8 @@ class HumanTextFeedbackForm(StyledForm):
required=False,
# avoid unfortunate scroll wheel accidents reported by graders
- widget=forms.TextInput)
+ widget=forms.TextInput,
+ label="Grade points")
self.fields["feedback_text"] = forms.CharField(
widget=forms.Textarea(),
@@ -557,16 +560,19 @@ class HumanTextFeedbackForm(StyledForm):
help_text=mark_safe("Feedback to be shown to student, using "
""
- "RELATE-flavored Markdown"))
+ "RELATE-flavored Markdown"),
+ label="Feedback text")
self.fields["notify"] = forms.BooleanField(
initial=False, required=False,
help_text="Checking this box and submitting the form "
"will notify the participant "
- "with a generic message containing the feedback text")
+ "with a generic message containing the feedback text",
+ label="Notify")
self.fields["notes"] = forms.CharField(
widget=forms.Textarea(),
help_text="Internal notes, not shown to student",
- required=False)
+ required=False,
+ label="Notes")
def clean(self):
grade_percent = self.cleaned_data.get("grade_percent")
@@ -666,9 +672,9 @@ class PageBaseWithHumanTextFeedback(PageBase):
from django.core.mail import send_mail
from django.conf import settings
- send_mail("[%s:%s] New notification"
- % (page_context.course.identifier,
- page_context.flow_session.flow_id),
+ send_mail("[%(identifier)s:%(flow_id)s] New notification"
+ % {'identifier':page_context.course.identifier,
+ 'flow_id':page_context.flow_session.flow_id},
message,
settings.ROBOT_EMAIL_FROM,
recipient_list=[
diff --git a/course/page/choice.py b/course/page/choice.py
index b6adef27c208a99c47d183dc1399144bb492adca..96a2b35ea44550cc3cd9e9ae23f1463ec3038604 100644
--- a/course/page/choice.py
+++ b/course/page/choice.py
@@ -40,6 +40,8 @@ class ChoiceAnswerForm(StyledForm):
super(ChoiceAnswerForm, self).__init__(*args, **kwargs)
self.fields["choice"] = field
+ # Translators: "choice" in Choice Answer Form in a choice question.
+ self.fields["choice"].label = "Choice"
# {{{ choice question
@@ -104,8 +106,8 @@ class ChoiceQuestion(PageBaseWithTitle, PageBaseWithValue):
try:
choice = str(choice)
except:
- raise ValidationError("%s, choice %d: unable to convert to string"
- % (location, choice_idx+1))
+ raise ValidationError("%(location)s, choice %(id)d: unable to convert to string"
+ % {'location':location, 'id':choice_idx+1})
if choice.startswith(self.CORRECT_TAG):
correct_choice_count += 1
@@ -115,8 +117,8 @@ class ChoiceQuestion(PageBaseWithTitle, PageBaseWithValue):
remove_prefix(self.CORRECT_TAG, choice))
if correct_choice_count < 1:
- raise ValidationError("%s: one or more correct answer(s) "
- "expected, %d found" % (location, correct_choice_count))
+ raise ValidationError("%(location)s: one or more correct answer(s) "
+ "expected, %(correct)d found" % {'location':location, 'correct':correct_choice_count})
def required_attrs(self):
return super(ChoiceQuestion, self).required_attrs() + (
diff --git a/course/page/code.py b/course/page/code.py
index 5888e6451ef878b5af3dd89c5b7f546925e28199..956c6e2b01c7e71e789541f03354013b10746d2b 100644
--- a/course/page/code.py
+++ b/course/page/code.py
@@ -54,7 +54,8 @@ class PythonCodeForm(StyledForm):
self.fields["answer"] = forms.CharField(required=True,
initial=initial_code,
help_text=cm_help_text,
- widget=cm_widget)
+ widget=cm_widget,
+ label="Answer")
def clean(self):
# FIXME Should try compilation
diff --git a/course/page/text.py b/course/page/text.py
index c2db93d8bada2a0b261519809142f1b7146ecc79..d83f386062870659ebdafc18a5cb5182dd042e76 100644
--- a/course/page/text.py
+++ b/course/page/text.py
@@ -91,7 +91,8 @@ class TextAnswerForm(StyledForm):
self.fields["answer"] = forms.CharField(
required=True,
widget=widget,
- help_text=help_text)
+ help_text=help_text,
+ label="Answer")
def clean(self):
cleaned_data = super(TextAnswerForm, self).clean()
diff --git a/course/page/upload.py b/course/page/upload.py
index e4aa0c10fe74d0b45171999e9bc2813be62ba54e..071b098e03d20af96f69a93d65876fb81789e3f4 100644
--- a/course/page/upload.py
+++ b/course/page/upload.py
@@ -39,7 +39,7 @@ from relate.utils import StyledForm
# {{{ upload question
class FileUploadForm(StyledForm):
- uploaded_file = forms.FileField(required=True)
+ uploaded_file = forms.FileField(required=True,label='Uploaded file')
def __init__(self, maximum_megabytes, mime_types, *args, **kwargs):
super(FileUploadForm, self).__init__(*args, **kwargs)
diff --git a/course/sandbox.py b/course/sandbox.py
index aabf38891e26e18e877565c78cd3a5b2d9c30f30..831f4eb198155717060db4ee6b0b91ae17d2728e 100644
--- a/course/sandbox.py
+++ b/course/sandbox.py
@@ -58,7 +58,8 @@ class SandboxForm(forms.Form):
widget=cm_widget,
help_text=mark_safe(
help_text + " Press Alt/Cmd+(Shift+)P to preview. "
- + cm_help_text))
+ + cm_help_text),
+ label="Content")
self.helper.add_input(
Submit(
diff --git a/course/versioning.py b/course/versioning.py
index b3b6fccd13d7dcd06ddf6ccc7f1db29406a5de7a..e2f6e02eb1d30af2d9ce4941a0b782de912703a3 100644
--- a/course/versioning.py
+++ b/course/versioning.py
@@ -31,6 +31,13 @@ import django.forms as forms
from django.contrib.auth.decorators import login_required
from django.core.exceptions import PermissionDenied, SuspiciousOperation
from django.core.urlresolvers import reverse
+from django.utils.translation import (
+ ugettext_lazy as _ ,
+ ugettext,
+ pgettext,
+ pgettext_lazy,
+ string_concat,
+ )
from django.db import transaction
@@ -335,7 +342,8 @@ def run_course_update_command(request, pctx, command, new_sha, may_update):
class GitUpdateForm(StyledForm):
- new_sha = forms.CharField(required=True)
+ new_sha = forms.CharField(required=True,
+ label=pgettext_lazy("new git SHA for revision of course contents","New git SHA"))
def __init__(self, may_update, previewing, *args, **kwargs):
super(GitUpdateForm, self).__init__(*args, **kwargs)
diff --git a/course/views.py b/course/views.py
index 93cae9604f46dc02aafefafcf52938177fdc0dcd..f3f2a565d4a39baa7faa4827dc2a69c4b60e4fe1 100644
--- a/course/views.py
+++ b/course/views.py
@@ -34,6 +34,17 @@ import django.views.decorators.http as http_dec
from django import http
from django.utils.safestring import mark_safe
from django.db import transaction
+from django.utils import six
+from django.utils.translation import (
+ ugettext_lazy as _ ,
+ ugettext,
+ string_concat,
+ pgettext,
+ pgettext_lazy,
+ )
+from django.utils.functional import lazy
+
+mark_safe_lazy = lazy(mark_safe, six.text_type)
from django.views.decorators.cache import cache_control
@@ -211,7 +222,8 @@ def get_repo_file(request, course_identifier, commit_sha, path):
class FakeTimeForm(StyledForm):
time = forms.DateTimeField(
widget=DateTimePicker(
- options={"format": "YYYY-MM-DD HH:mm", "sideBySide": True}))
+ options={"format": "YYYY-MM-DD HH:mm", "sideBySide": True}),
+ label='Time')
def __init__(self, *args, **kwargs):
super(FakeTimeForm, self).__init__(*args, **kwargs)
@@ -291,9 +303,11 @@ class InstantFlowRequestForm(StyledForm):
self.fields["flow_id"] = forms.ChoiceField(
choices=[(fid, fid) for fid in flow_ids],
- required=True)
+ required=True,
+ label="Flow ID")
self.fields["duration_in_minutes"] = forms.IntegerField(
- required=True, initial=20)
+ required=True, initial=20,
+ label="Duration in minutes")
self.helper.add_input(
Submit("add", "Add", css_class="col-lg-offset-2"))
@@ -365,7 +379,8 @@ class FlowTestForm(StyledForm):
self.fields["flow_id"] = forms.ChoiceField(
choices=[(fid, fid) for fid in flow_ids],
- required=True)
+ required=True,
+ label="Flow ID")
self.helper.add_input(
Submit("test", mark_safe("Go »"), css_class="col-lg-offset-2"))
@@ -423,10 +438,12 @@ class ExceptionStage1Form(StyledForm):
)
.order_by("user__last_name")),
required=True,
- help_text="Select participant for whom exception is to be granted.")
+ help_text="Select participant for whom exception is to be granted.",
+ label="Participant")
self.fields["flow_id"] = forms.ChoiceField(
choices=[(fid, fid) for fid in flow_ids],
- required=True)
+ required=True,
+ label="Flow ID")
self.helper.add_input(
Submit(
@@ -483,7 +500,8 @@ class CreateSessionForm(StyledForm):
choices=session_tag_choices,
initial=default_tag,
help_text="If you click 'Create session', this tag will be "
- "applied to the new session.")
+ "applied to the new session.",
+ label="Access rules tag for new session")
if create_session_is_override:
self.helper.add_input(
@@ -504,7 +522,8 @@ class ExceptionStage2Form(StyledForm):
(session.id, strify_session_for_exception(session))
for session in sessions),
help_text="The rules that currently apply to selected session "
- "will provide the default values for the rules on the next page.")
+ "will provide the default values for the rules on the next page.",
+ label="Session")
self.helper.add_input(Submit("next", mark_safe("Next »"),
css_class="col-lg-offset-2"))
@@ -622,6 +641,7 @@ class ExceptionStage3Form(StyledForm):
options={"format": "YYYY-MM-DD HH:mm", "sideBySide": True,
"showClear": True}),
required=False,
+ label=pgettext_lazy("time when access expires","Access expires"),
help_text="At the specified time, the special access granted below "
"will expire "
"and revert to being the same as for the rest of the class. "
@@ -644,7 +664,8 @@ class ExceptionStage3Form(StyledForm):
[(tag, tag) for tag in tags],
initial=(base_session_tag
if base_session_tag is not None
- else NONE_SESSION_TAG))
+ else NONE_SESSION_TAG),
+ label="Set access rules tag")
self.fields["restrict_to_same_tag"] = forms.BooleanField(
label="Exception only applies to sessions with the above tag",
required=False,
@@ -666,24 +687,28 @@ class ExceptionStage3Form(StyledForm):
self.fields["due_same_as_access_expiration"] = forms.BooleanField(
required=False, help_text="If set, the 'Due' field will be "
"disregarded.",
- initial=default_data.get("due_same_as_access_expiration") or False)
+ initial=default_data.get("due_same_as_access_expiration") or False,
+ label="Due same as access expiration")
self.fields["due"] = forms.DateTimeField(
widget=DateTimePicker(
options={"format": "YYYY-MM-DD HH:mm", "sideBySide": True}),
required=False,
- help_text="The due date shown to the student. Also, the "
+ help_text="The due time shown to the student. Also, the "
"time after which "
"any session under these rules is subject to expiration.",
- initial=default_data.get("due"))
+ initial=default_data.get("due"),
+ label="Due time")
self.fields["credit_percent"] = forms.IntegerField(required=False,
- initial=default_data.get("credit_percent"))
+ initial=default_data.get("credit_percent"),
+ label="Credit percent")
layout.append(Div("due_same_as_access_expiration", "due", "credit_percent",
css_class="well"))
self.fields["comment"] = forms.CharField(
widget=forms.Textarea, required=True,
- initial=default_data.get("comment"))
+ initial=default_data.get("comment"),
+ label="Comment")
layout.append("comment")
diff --git a/relate/settings.py b/relate/settings.py
index 614053c433588879e0763f80a129486c8d87554e..27e069311f5340db51eebd9e0e9da3cc7e77180c 100644
--- a/relate/settings.py
+++ b/relate/settings.py
@@ -48,6 +48,7 @@ INSTALLED_APPS = (
MIDDLEWARE_CLASSES = (
"django.contrib.sessions.middleware.SessionMiddleware",
+ "django.middleware.locale.LocaleMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
@@ -173,3 +174,7 @@ TEMPLATES = [
}
},
]
+
+LOCALE_PATHS = (
+ BASE_DIR+ '/locale',
+)