Newer
Older
_("%s, notify_on_submit: item %d is not a string")
for attr in ["max_points", "max_points_enforced_cap", "bonus_points"]:
if hasattr(flow_desc, attr):
vctx.add_warning(location,
_("Attribute '%s' is deprecated as part of a flow. "
"Specify it as part of a grading rule instead.")
% attr)
def validate_calendar_desc_struct(vctx, location, events_desc):
location,
events_desc,
required_attrs=[
],
allowed_attrs=[
("event_kinds", Struct),
("events", Struct),
]
)
if hasattr(events_desc, "event_kinds"):
for event_kind_name in events_desc.event_kinds._field_names:
event_kind = getattr(events_desc.event_kinds, event_kind_name)
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
validate_struct(
vctx,
"%s, event kind '%s'" % (location, event_kind_name),
event_kind,
required_attrs=[
],
allowed_attrs=[
("color", str),
("title", str),
]
)
if hasattr(events_desc, "events"):
for event_name in events_desc.events._field_names:
event_desc = getattr(events_desc.events, event_name)
validate_struct(
vctx,
"%s, event '%s'" % (location, event_name),
event_desc,
required_attrs=[
],
allowed_attrs=[
("color", str),
("title", str),
("description", "markup"),
("show_description_from", datespec_types),
("show_description_until", datespec_types),
if hasattr(event_desc, "show_description_from"):
vctx.encounter_datespec(location, event_desc.show_description_from)
if hasattr(event_desc, "show_description_until"):
vctx.encounter_datespec(location, event_desc.show_description_until)
def get_yaml_from_repo_safely(repo, full_name, commit_sha):
from course.content import get_yaml_from_repo
return get_yaml_from_repo(
repo=repo, full_name=full_name, commit_sha=commit_sha,
cached=False)
from traceback import print_exc
print_exc()
raise ValidationError(
"%(fullname)s: %(err_type)s: %(err_str)s" % {
'fullname': full_name,
def check_attributes_yml(vctx, repo, path, tree, access_kinds):
Andreas Klöckner
committed
"""
This function reads the .attributes.yml file and checks
that each item for each header is a string
Example::
# this validates
unenrolled:
- test1.pdf
student:
- test2.pdf
# this does not validate
unenrolled:
- test1.pdf
student:
- test2.pdf
- 42
Andreas Klöckner
committed
"""
from course.content import get_true_repo_and_path
repo, path = get_true_repo_and_path(repo, path)
dummy, attr_blob_sha = tree[b".attributes.yml"]
except KeyError:
# no .attributes.yml here
pass
else:
from relate.utils import dict_to_struct
from yaml import load as load_yaml
att_yml = dict_to_struct(load_yaml(repo[attr_blob_sha].data))
loc = path + "/" + ".attributes.yml"
Andreas Klöckner
committed
validate_struct(vctx, loc, att_yml,
required_attrs=[],
allowed_attrs=[(role, list) for role in access_kinds])
Andreas Klöckner
committed
if hasattr(att_yml, "public"):
vctx.add_warning(loc,
_("Access class 'public' is deprecated. Use 'unenrolled' "
"instead."))
if hasattr(att_yml, "public") and hasattr(att_yml, "unenrolled"):
raise ValidationError(
_("%s: access classes 'public' and 'unenrolled' may not "
"exist simultaneously.")
% (loc))
Andreas Klöckner
committed
for i, l in enumerate(getattr(att_yml, access_kind)):
Andreas Klöckner
committed
"%s: entry %d in '%s' is not a string"
% (loc, i+1, access_kind))
import stat
for entry in tree.items():
if stat.S_ISDIR(entry.mode):
dummy, blob_sha = tree[entry.path]
check_attributes_yml(
vctx, repo,
path+"/"+entry.path.decode("utf-8"), subtree,
access_kinds)
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
# {{{ check whether flow grade identifiers were changed in sketchy ways
def check_grade_identifier_link(
vctx, location, course, flow_id, flow_grade_identifier):
from course.models import GradingOpportunity
for bad_gopp in (
GradingOpportunity.objects
.filter(
course=course,
identifier=flow_grade_identifier)
.exclude(flow_id=flow_id)):
# 0 or 1 trips through this loop because of uniqueness
raise ValidationError(
_(
"{location}: existing grading opportunity with identifier "
"'{grade_identifier}' refers to flow '{other_flow_id}', however "
"flow code in this flow ('{new_flow_id}') specifies the same "
"grade identifier. "
"(Have you renamed the flow? If so, edit the grading "
"opportunity to match.)")
.format(
location=location,
grade_identifier=flow_grade_identifier,
other_flow_id=bad_gopp.flow_id,
new_flow_id=flow_id,
new_grade_identifier=flow_grade_identifier))
# }}}
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
# {{{ check whether page types were changed
def check_for_page_type_changes(vctx, location, course, flow_id, flow_desc):
from course.content import normalize_flow_desc
n_flow_desc = normalize_flow_desc(flow_desc)
from course.models import FlowPageData
for grp in n_flow_desc.groups:
for page_desc in grp.pages:
fpd_with_mismatched_page_types = list(
FlowPageData.objects
.filter(
flow_session__course=course,
flow_session__flow_id=flow_id,
group_id=grp.id,
page_id=page_desc.id)
.exclude(page_type=None)
.exclude(page_type=page_desc.type)
[0:1])
if fpd_with_mismatched_page_types:
mismatched_fpd, = fpd_with_mismatched_page_types
raise ValidationError(
_("%(loc)s, group '%(group)s', page '%(page)s': "
"page type ('%(type_new)s') differs from "
"type used in database ('%(type_old)s')")
% {"loc": location, "group": grp.id,
"page": page_desc.id,
"type_new": page_desc.type,
"type_old": mismatched_fpd.page_type})
# }}}
def validate_course_content(repo, course_file, events_file,
Andreas Klöckner
committed
validate_sha, course=None):
repo=repo,
commit_sha=validate_sha,
Andreas Klöckner
committed
course=course)
course_desc = get_yaml_from_repo_safely(repo, course_file,
commit_sha=validate_sha)
validate_staticpage_desc(vctx, course_file, course_desc)
try:
from course.content import get_yaml_from_repo
events_desc = get_yaml_from_repo(repo, events_file,
commit_sha=validate_sha, cached=False)
Andreas Klöckner
committed
if events_file != "events.yml":
vctx.add_warning(
Andreas Klöckner
committed
_("Events file"),
_("Your course repository does not have an events "
"file named '%s'.")
% events_file)
else:
# That's OK--no calendar info.
pass
validate_calendar_desc_struct(vctx, events_file, events_desc)
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
if vctx.course is not None:
from course.models import (
ParticipationPermission,
ParticipationRolePermission)
access_kinds = frozenset(
ParticipationPermission.objects
.filter(
participation__course=vctx.course,
permission=pperm.access_files_for,
)
.values_list("argument", flat=True)) | frozenset(
ParticipationRolePermission.objects
.filter(
course=vctx.course,
permission=pperm.access_files_for,
)
.values_list("argument", flat=True))
else:
access_kinds = ["public", "in_exam", "student", "ta",
"unenrolled", "instructor"]
vctx, repo, "",
get_repo_blob(repo, "", validate_sha),
access_kinds)
try:
flows_tree = get_repo_blob(repo, "media", validate_sha)
except ObjectDoesNotExist:
# That's great--no media directory.
pass
else:
vctx.add_warning(
'media/', _(
"Your course repository has a 'media/' directory. "
"Linking to media files using 'media:' is discouraged. "
"Use the 'repo:' and 'repocur:' linkng schemes instead."))
try:
flows_tree = get_repo_blob(repo, "flows", validate_sha)
except ObjectDoesNotExist:
# That's OK--no flows yet.
pass
else:
used_grade_identifiers = set()
for entry in flows_tree.items():
entry_path = entry.path.decode("utf-8")
if not entry_path.endswith(".yml"):
from course.constants import FLOW_ID_REGEX
match = re.match("^"+FLOW_ID_REGEX+"$", flow_id)
if match is None:
raise ValidationError(
string_concat("%s: ",
_("invalid flow name. "
"Flow names may only contain (roman) "
"letters, numbers, "
flow_desc = get_yaml_from_repo_safely(repo, location,
validate_flow_desc(vctx, location, flow_desc)
# {{{ check grade_identifier
Andreas Klöckner
committed
flow_grade_identifier = None
Andreas Klöckner
committed
flow_grade_identifier = getattr(
flow_desc.rules, "grade_identifier", None)
if (
flow_grade_identifier is not None
and
set([flow_grade_identifier]) & used_grade_identifiers):
Andreas Klöckner
committed
string_concat("%s: ",
_("flow uses the same grade_identifier "
Andreas Klöckner
committed
"as another flow"))
% location)
used_grade_identifiers.add(flow_grade_identifier)
if (course is not None
and flow_grade_identifier is not None):
check_grade_identifier_link(
vctx, location, course, flow_id, flow_grade_identifier)
Andreas Klöckner
committed
# }}}
if course is not None:
check_for_page_type_changes(
vctx, location, course, flow_id, flow_desc)
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
1387
1388
1389
1390
1391
1392
1393
1394
# }}}
# {{{ static pages
try:
pages_tree = get_repo_blob(repo, "staticpages", validate_sha)
except ObjectDoesNotExist:
# That's OK--no flows yet.
pass
else:
for entry in pages_tree.items():
entry_path = entry.path.decode("utf-8")
if not entry_path.endswith(".yml"):
continue
from course.constants import STATICPAGE_PATH_REGEX
page_name = entry_path[:-4]
match = re.match("^"+STATICPAGE_PATH_REGEX+"$", page_name)
if match is None:
raise ValidationError(
string_concat("%s: ",
_(
"invalid page name. "
"Page names may only contain "
"alphanumeric characters (any language) "
"and hyphens."
))
% entry_path)
location = "staticpages/%s" % entry_path
page_desc = get_yaml_from_repo_safely(repo, location,
commit_sha=validate_sha)
validate_staticpage_desc(vctx, location, page_desc)
# }}}
Andreas Klöckner
committed
# {{{ validation script support
class FileSystemFakeRepo(object):
def __init__(self, root):
self.root = root
assert isinstance(self.root, six.binary_type)
def controldir(self):
return self.root
def __getitem__(self, sha):
return sha
def __str__(self):
return "<FAKEREPO:%s>" % self.root
def decode(self):
return self
@property
def tree(self):
return FileSystemFakeRepoTree(self.root)
class FileSystemFakeRepoTreeEntry(object):
class FileSystemFakeRepoTree(object):
def __init__(self, root):
self.root = root
assert isinstance(self.root, six.binary_type)
if not name:
raise KeyError("<empty filename>")
from os.path import join, isdir, exists
name = join(self.root, name)
if not exists(name):
# returns mode, "sha"
if isdir(name):
return None, FileSystemFakeRepoTree(name)
else:
return None, FileSystemFakeRepoFile(name)
def items(self):
import os
return [
FileSystemFakeRepoTreeEntry(
path=n,
mode=os.stat(os.path.join(self.root, n)).st_mode)
for n in os.listdir(self.root)]
class FileSystemFakeRepoFile(object):
def __init__(self, name):
self.name = name
@property
def data(self):
with open(self.name, "rb") as inf:
return inf.read()
def validate_course_on_filesystem(
root, course_file, events_file):
fake_repo = FileSystemFakeRepo(root.encode("utf-8"))
warnings = validate_course_content(
course_file, events_file,
Andreas Klöckner
committed
validate_sha=fake_repo, course=None)
print(_("WARNINGS: "))
for w in warnings:
print("***", w.location, w.text)