85
76
ctx['semesters'].append((semester, offerings))
88
class SubjectsManage(XHTMLView):
89
'''Subject management view.'''
90
template = 'templates/subjects-manage.html'
93
def authorize(self, req):
94
return req.user is not None and req.user.admin
96
def populate(self, req, ctx):
98
ctx['mediapath'] = media_url(req, CorePlugin, 'images/')
99
ctx['SubjectView'] = SubjectView
100
ctx['SubjectEdit'] = SubjectEdit
101
ctx['SemesterEdit'] = SemesterEdit
103
ctx['subjects'] = req.store.find(Subject).order_by(Subject.name)
104
ctx['semesters'] = req.store.find(Semester).order_by(
105
Semester.year, Semester.semester)
108
class SubjectShortNameUniquenessValidator(formencode.FancyValidator):
109
"""A FormEncode validator that checks that a subject name is unused.
111
The subject referenced by state.existing_subject is permitted
112
to hold that name. If any other object holds it, the input is rejected.
114
def __init__(self, matching=None):
115
self.matching = matching
117
def _to_python(self, value, state):
118
if (state.store.find(
119
Subject, short_name=value).one() not in
120
(None, state.existing_subject)):
121
raise formencode.Invalid(
122
'Short name already taken', value, state)
126
class SubjectSchema(formencode.Schema):
127
short_name = formencode.All(
128
SubjectShortNameUniquenessValidator(),
129
URLNameValidator(not_empty=True))
130
name = formencode.validators.UnicodeString(not_empty=True)
131
code = formencode.validators.UnicodeString(not_empty=True)
134
class SubjectFormView(BaseFormView):
135
"""An abstract form to add or edit a subject."""
138
def authorize(self, req):
139
return req.user is not None and req.user.admin
141
def populate_state(self, state):
142
state.existing_subject = None
146
return SubjectSchema()
149
class SubjectNew(SubjectFormView):
150
"""A form to create a subject."""
151
template = 'templates/subject-new.html'
153
def get_default_data(self, req):
156
def save_object(self, req, data):
157
new_subject = Subject()
158
new_subject.short_name = data['short_name']
159
new_subject.name = data['name']
160
new_subject.code = data['code']
162
req.store.add(new_subject)
166
class SubjectEdit(SubjectFormView):
167
"""A form to edit a subject."""
168
template = 'templates/subject-edit.html'
170
def populate_state(self, state):
171
state.existing_subject = self.context
173
def get_default_data(self, req):
175
'short_name': self.context.short_name,
176
'name': self.context.name,
177
'code': self.context.code,
180
def save_object(self, req, data):
181
self.context.short_name = data['short_name']
182
self.context.name = data['name']
183
self.context.code = data['code']
188
class SemesterUniquenessValidator(formencode.FancyValidator):
189
"""A FormEncode validator that checks that a semester is unique.
191
There cannot be more than one semester for the same year and semester.
193
def _to_python(self, value, state):
194
if (state.store.find(
195
Semester, year=value['year'], semester=value['semester']
196
).one() not in (None, state.existing_semester)):
197
raise formencode.Invalid(
198
'Semester already exists', value, state)
202
class SemesterSchema(formencode.Schema):
203
year = URLNameValidator()
204
semester = URLNameValidator()
205
state = formencode.All(
206
formencode.validators.OneOf(["past", "current", "future"]),
207
formencode.validators.UnicodeString())
208
chained_validators = [SemesterUniquenessValidator()]
211
class SemesterFormView(BaseFormView):
214
def authorize(self, req):
215
return req.user is not None and req.user.admin
219
return SemesterSchema()
221
def get_return_url(self, obj):
222
return '/subjects/+manage'
225
class SemesterNew(SemesterFormView):
226
"""A form to create a semester."""
227
template = 'templates/semester-new.html'
230
def populate_state(self, state):
231
state.existing_semester = None
233
def get_default_data(self, req):
236
def save_object(self, req, data):
237
new_semester = Semester()
238
new_semester.year = data['year']
239
new_semester.semester = data['semester']
240
new_semester.state = data['state']
242
req.store.add(new_semester)
246
class SemesterEdit(SemesterFormView):
247
"""A form to edit a semester."""
248
template = 'templates/semester-edit.html'
250
def populate_state(self, state):
251
state.existing_semester = self.context
253
def get_default_data(self, req):
255
'year': self.context.year,
256
'semester': self.context.semester,
257
'state': self.context.state,
260
def save_object(self, req, data):
261
self.context.year = data['year']
262
self.context.semester = data['semester']
263
self.context.state = data['state']
267
class SubjectView(XHTMLView):
268
'''The view of the list of offerings in a given subject.'''
269
template = 'templates/subject.html'
272
def authorize(self, req):
273
return req.user is not None
275
def populate(self, req, ctx):
276
ctx['context'] = self.context
278
ctx['user'] = req.user
279
ctx['offerings'] = list(self.context.offerings)
280
ctx['permissions'] = self.context.get_permissions(req.user,req.config)
281
ctx['SubjectEdit'] = SubjectEdit
282
ctx['SubjectOfferingNew'] = SubjectOfferingNew
285
class OfferingView(XHTMLView):
286
"""The home page of an offering."""
287
template = 'templates/offering.html'
291
def populate(self, req, ctx):
292
# Need the worksheet result styles.
293
self.plugin_styles[TutorialPlugin] = ['tutorial.css']
294
ctx['context'] = self.context
296
ctx['permissions'] = self.context.get_permissions(req.user,req.config)
297
ctx['format_submission_principal'] = util.format_submission_principal
298
ctx['format_datetime'] = ivle.date.make_date_nice
299
ctx['format_datetime_short'] = ivle.date.format_datetime_for_paragraph
300
ctx['OfferingEdit'] = OfferingEdit
301
ctx['OfferingCloneWorksheets'] = OfferingCloneWorksheets
302
ctx['GroupsView'] = GroupsView
303
ctx['EnrolmentsView'] = EnrolmentsView
304
ctx['Project'] = ivle.database.Project
306
# As we go, calculate the total score for this subject
307
# (Assessable worksheets only, mandatory problems only)
309
ctx['worksheets'], problems_total, problems_done = (
310
ivle.worksheet.utils.create_list_of_fake_worksheets_and_stats(
311
req.config, req.store, req.user, self.context,
312
as_of=self.context.worksheet_cutoff))
314
ctx['exercises_total'] = problems_total
315
ctx['exercises_done'] = problems_done
316
if problems_total > 0:
317
if problems_done >= problems_total:
318
ctx['worksheets_complete_class'] = "complete"
319
elif problems_done > 0:
320
ctx['worksheets_complete_class'] = "semicomplete"
322
ctx['worksheets_complete_class'] = "incomplete"
323
# Calculate the final percentage and mark for the subject
324
(ctx['exercises_pct'], ctx['worksheet_mark'],
325
ctx['worksheet_max_mark']) = (
326
ivle.worksheet.utils.calculate_mark(
327
problems_done, problems_total))
330
class SubjectValidator(formencode.FancyValidator):
331
"""A FormEncode validator that turns a subject name into a subject.
333
The state must have a 'store' attribute, which is the Storm store
336
def _to_python(self, value, state):
337
subject = state.store.find(Subject, short_name=value).one()
341
raise formencode.Invalid('Subject does not exist', value, state)
344
class SemesterValidator(formencode.FancyValidator):
345
"""A FormEncode validator that turns a string into a semester.
347
The string should be of the form 'year/semester', eg. '2009/1'.
349
The state must have a 'store' attribute, which is the Storm store
352
def _to_python(self, value, state):
354
year, semester = value.split('/')
356
year = semester = None
358
semester = state.store.find(
359
Semester, year=year, semester=semester).one()
363
raise formencode.Invalid('Semester does not exist', value, state)
366
class OfferingUniquenessValidator(formencode.FancyValidator):
367
"""A FormEncode validator that checks that an offering is unique.
369
There cannot be more than one offering in the same year and semester.
371
The offering referenced by state.existing_offering is permitted to
372
hold that year and semester tuple. If any other object holds it, the
375
def _to_python(self, value, state):
376
if (state.store.find(
377
Offering, subject=value['subject'],
378
semester=value['semester']).one() not in
379
(None, state.existing_offering)):
380
raise formencode.Invalid(
381
'Offering already exists', value, state)
385
class OfferingSchema(formencode.Schema):
386
description = formencode.validators.UnicodeString(
387
if_missing=None, not_empty=False)
388
url = formencode.validators.URL(if_missing=None, not_empty=False)
389
worksheet_cutoff = DateTimeValidator(if_missing=None, not_empty=False)
390
show_worksheet_marks = formencode.validators.StringBoolean(
394
class OfferingAdminSchema(OfferingSchema):
395
subject = formencode.All(
396
SubjectValidator(), formencode.validators.UnicodeString())
397
semester = formencode.All(
398
SemesterValidator(), formencode.validators.UnicodeString())
399
chained_validators = [OfferingUniquenessValidator()]
402
class OfferingEdit(BaseFormView):
403
"""A form to edit an offering's details."""
404
template = 'templates/offering-edit.html'
410
if self.req.user.admin:
411
return OfferingAdminSchema()
413
return OfferingSchema()
415
def populate(self, req, ctx):
416
super(OfferingEdit, self).populate(req, ctx)
417
ctx['subjects'] = req.store.find(Subject).order_by(Subject.name)
418
ctx['semesters'] = req.store.find(Semester).order_by(
419
Semester.year, Semester.semester)
420
ctx['force_subject'] = None
422
def populate_state(self, state):
423
state.existing_offering = self.context
425
def get_default_data(self, req):
427
'subject': self.context.subject.short_name,
428
'semester': self.context.semester.year + '/' +
429
self.context.semester.semester,
430
'url': self.context.url,
431
'description': self.context.description,
432
'worksheet_cutoff': self.context.worksheet_cutoff,
433
'show_worksheet_marks': self.context.show_worksheet_marks,
436
def save_object(self, req, data):
438
self.context.subject = data['subject']
439
self.context.semester = data['semester']
440
self.context.description = data['description']
441
self.context.url = unicode(data['url']) if data['url'] else None
442
self.context.worksheet_cutoff = data['worksheet_cutoff']
443
self.context.show_worksheet_marks = data['show_worksheet_marks']
447
class OfferingNew(BaseFormView):
448
"""A form to create an offering."""
449
template = 'templates/offering-new.html'
452
def authorize(self, req):
453
return req.user is not None and req.user.admin
457
return OfferingAdminSchema()
459
def populate(self, req, ctx):
460
super(OfferingNew, self).populate(req, ctx)
461
ctx['subjects'] = req.store.find(Subject).order_by(Subject.name)
462
ctx['semesters'] = req.store.find(Semester).order_by(
463
Semester.year, Semester.semester)
464
ctx['force_subject'] = None
466
def populate_state(self, state):
467
state.existing_offering = None
469
def get_default_data(self, req):
472
def save_object(self, req, data):
473
new_offering = Offering()
474
new_offering.subject = data['subject']
475
new_offering.semester = data['semester']
476
new_offering.description = data['description']
477
new_offering.url = unicode(data['url']) if data['url'] else None
478
new_offering.worksheet_cutoff = data['worksheet_cutoff']
479
new_offering.show_worksheet_marks = data['show_worksheet_marks']
481
req.store.add(new_offering)
484
class SubjectOfferingNew(OfferingNew):
485
"""A form to create an offering for a given subject."""
486
# Identical to OfferingNew, except it forces the subject to be the subject
488
def populate(self, req, ctx):
489
super(SubjectOfferingNew, self).populate(req, ctx)
490
ctx['force_subject'] = self.context
492
class OfferingCloneWorksheetsSchema(formencode.Schema):
493
subject = formencode.All(
494
SubjectValidator(), formencode.validators.UnicodeString())
495
semester = formencode.All(
496
SemesterValidator(), formencode.validators.UnicodeString())
499
class OfferingCloneWorksheets(BaseFormView):
500
"""A form to clone worksheets from one offering to another."""
501
template = 'templates/offering-clone-worksheets.html'
504
def authorize(self, req):
505
return req.user is not None and req.user.admin
509
return OfferingCloneWorksheetsSchema()
511
def populate(self, req, ctx):
512
super(OfferingCloneWorksheets, self).populate(req, ctx)
513
ctx['subjects'] = req.store.find(Subject).order_by(Subject.name)
514
ctx['semesters'] = req.store.find(Semester).order_by(
515
Semester.year, Semester.semester)
517
def get_default_data(self, req):
520
def save_object(self, req, data):
521
if self.context.worksheets.count() > 0:
523
"Cannot clone to target with existing worksheets.")
524
offering = req.store.find(
525
Offering, subject=data['subject'], semester=data['semester']).one()
527
raise BadRequest("No such offering.")
528
if offering.worksheets.count() == 0:
529
raise BadRequest("Source offering has no worksheets.")
531
self.context.clone_worksheets(offering)
535
79
class UserValidator(formencode.FancyValidator):
536
80
"""A FormEncode validator that turns a username into a user.
624
157
ctx['data'] = data or {}
625
158
ctx['offering'] = self.context
626
ctx['roles_auth'] = self.context.get_permissions(req.user, req.config)
627
159
ctx['errors'] = errors
628
# If all of the fields validated, set the global form error.
629
if isinstance(errors, basestring):
630
ctx['error_value'] = errors
633
class EnrolmentEditSchema(formencode.Schema):
634
role = formencode.All(formencode.validators.OneOf(
635
["lecturer", "tutor", "student"]),
636
RoleEnrolmentValidator(),
637
formencode.validators.UnicodeString())
640
class EnrolmentEdit(BaseFormView):
641
"""A form to alter an enrolment's role."""
642
template = 'templates/enrolment-edit.html'
646
def populate_state(self, state):
647
state.offering = self.context.offering
649
def get_default_data(self, req):
650
return {'role': self.context.role}
654
return EnrolmentEditSchema()
656
def save_object(self, req, data):
657
self.context.role = data['role']
659
def get_return_url(self, obj):
660
return self.req.publisher.generate(
661
self.context.offering, EnrolmentsView)
663
def populate(self, req, ctx):
664
super(EnrolmentEdit, self).populate(req, ctx)
665
ctx['offering_perms'] = self.context.offering.get_permissions(
666
req.user, req.config)
669
class EnrolmentDelete(XHTMLView):
670
"""A form to alter an enrolment's role."""
671
template = 'templates/enrolment-delete.html'
675
def populate(self, req, ctx):
676
# If POSTing, delete delete delete.
677
if req.method == 'POST':
678
self.context.delete()
680
req.throw_redirect(req.publisher.generate(
681
self.context.offering, EnrolmentsView))
683
ctx['enrolment'] = self.context
686
161
class OfferingProjectsView(XHTMLView):
687
162
"""View the projects for an offering."""
688
163
template = 'templates/offering_projects.html'
689
164
permission = 'edit'
691
breadcrumb_text = 'Projects'
693
167
def populate(self, req, ctx):
694
168
self.plugin_styles[Plugin] = ["project.css"]
169
self.plugin_scripts[Plugin] = ["project.js"]
696
171
ctx['offering'] = self.context
697
172
ctx['projectsets'] = []
173
ctx['OfferingRESTView'] = OfferingRESTView
699
175
#Open the projectset Fragment, and render it for inclusion
700
176
#into the ProjectSets page
177
#XXX: This could be a lot cleaner
178
loader = genshi.template.TemplateLoader(".", auto_reload=True)
701
180
set_fragment = os.path.join(os.path.dirname(__file__),
702
181
"templates/projectset_fragment.html")
703
182
project_fragment = os.path.join(os.path.dirname(__file__),
704
183
"templates/project_fragment.html")
707
self.context.project_sets.order_by(ivle.database.ProjectSet.id):
708
settmpl = self._loader.load(set_fragment)
185
for projectset in self.context.project_sets:
186
settmpl = loader.load(set_fragment)
709
187
setCtx = Context()
710
188
setCtx['req'] = req
711
189
setCtx['projectset'] = projectset
712
190
setCtx['projects'] = []
713
191
setCtx['GroupsView'] = GroupsView
714
setCtx['ProjectSetEdit'] = ProjectSetEdit
715
setCtx['ProjectNew'] = ProjectNew
192
setCtx['ProjectSetRESTView'] = ProjectSetRESTView
718
projectset.projects.order_by(ivle.database.Project.deadline):
719
projecttmpl = self._loader.load(project_fragment)
194
for project in projectset.projects:
195
projecttmpl = loader.load(project_fragment)
720
196
projectCtx = Context()
721
197
projectCtx['req'] = req
722
198
projectCtx['project'] = project
723
projectCtx['ProjectEdit'] = ProjectEdit
724
projectCtx['ProjectDelete'] = ProjectDelete
726
200
setCtx['projects'].append(
727
201
projecttmpl.generate(projectCtx))
756
230
def populate(self, req, ctx):
757
231
self.plugin_styles[Plugin] = ["project.css"]
760
ctx['permissions'] = self.context.get_permissions(req.user,req.config)
761
ctx['GroupsView'] = GroupsView
762
ctx['EnrolView'] = EnrolView
763
ctx['format_datetime'] = ivle.date.make_date_nice
764
233
ctx['format_datetime_short'] = ivle.date.format_datetime_for_paragraph
765
234
ctx['build_subversion_url'] = self.build_subversion_url
235
ctx['svn_addr'] = req.config['urls']['svn_addr']
766
236
ctx['project'] = self.context
767
237
ctx['user'] = req.user
768
ctx['ProjectEdit'] = ProjectEdit
769
ctx['ProjectDelete'] = ProjectDelete
771
class ProjectUniquenessValidator(formencode.FancyValidator):
772
"""A FormEncode validator that checks that a project short_name is unique
775
The project referenced by state.existing_project is permitted to
776
hold that short_name. If any other project holds it, the input is rejected.
778
def _to_python(self, value, state):
779
if (state.store.find(
781
Project.short_name == unicode(value),
782
Project.project_set_id == ProjectSet.id,
783
ProjectSet.offering == state.offering).one() not in
784
(None, state.existing_project)):
785
raise formencode.Invalid(
786
"A project with that URL name already exists in this offering."
790
class ProjectSchema(formencode.Schema):
791
name = formencode.validators.UnicodeString(not_empty=True)
792
short_name = formencode.All(
793
URLNameValidator(not_empty=True),
794
ProjectUniquenessValidator())
795
deadline = DateTimeValidator(not_empty=True)
796
url = formencode.validators.URL(if_missing=None, not_empty=False)
797
synopsis = formencode.validators.UnicodeString(not_empty=True)
799
class ProjectEdit(BaseFormView):
800
"""A form to edit a project."""
801
template = 'templates/project-edit.html'
807
return ProjectSchema()
809
def populate(self, req, ctx):
810
super(ProjectEdit, self).populate(req, ctx)
811
ctx['projectset'] = self.context.project_set
813
def populate_state(self, state):
814
state.offering = self.context.project_set.offering
815
state.existing_project = self.context
817
def get_default_data(self, req):
819
'name': self.context.name,
820
'short_name': self.context.short_name,
821
'deadline': self.context.deadline,
822
'url': self.context.url,
823
'synopsis': self.context.synopsis,
826
def save_object(self, req, data):
827
self.context.name = data['name']
828
self.context.short_name = data['short_name']
829
self.context.deadline = data['deadline']
830
self.context.url = unicode(data['url']) if data['url'] else None
831
self.context.synopsis = data['synopsis']
834
class ProjectNew(BaseFormView):
835
"""A form to create a new project."""
836
template = 'templates/project-new.html'
842
return ProjectSchema()
844
def populate(self, req, ctx):
845
super(ProjectNew, self).populate(req, ctx)
846
ctx['projectset'] = self.context
848
def populate_state(self, state):
849
state.offering = self.context.offering
850
state.existing_project = None
852
def get_default_data(self, req):
855
def save_object(self, req, data):
856
new_project = Project()
857
new_project.project_set = self.context
858
new_project.name = data['name']
859
new_project.short_name = data['short_name']
860
new_project.deadline = data['deadline']
861
new_project.url = unicode(data['url']) if data['url'] else None
862
new_project.synopsis = data['synopsis']
863
req.store.add(new_project)
866
class ProjectDelete(XHTMLView):
867
"""A form to delete a project."""
868
template = 'templates/project-delete.html'
872
def populate(self, req, ctx):
873
# If post, delete the project, or display a message explaining that
874
# the project cannot be deleted
875
if self.context.can_delete:
876
if req.method == 'POST':
877
self.context.delete()
878
self.template = 'templates/project-deleted.html'
881
self.template = 'templates/project-undeletable.html'
883
# If get and can delete, display a delete confirmation page
885
# Variables for the template
887
ctx['project'] = self.context
888
ctx['OfferingProjectsView'] = OfferingProjectsView
890
class ProjectSetSchema(formencode.Schema):
891
group_size = formencode.validators.Int(if_missing=None, not_empty=False)
893
class ProjectSetEdit(BaseFormView):
894
"""A form to edit a project set."""
895
template = 'templates/projectset-edit.html'
901
return ProjectSetSchema()
903
def populate(self, req, ctx):
904
super(ProjectSetEdit, self).populate(req, ctx)
906
def get_default_data(self, req):
908
'group_size': self.context.max_students_per_group,
911
def save_object(self, req, data):
912
self.context.max_students_per_group = data['group_size']
915
class ProjectSetNew(BaseFormView):
916
"""A form to create a new project set."""
917
template = 'templates/projectset-new.html'
920
breadcrumb_text = "Projects"
924
return ProjectSetSchema()
926
def populate(self, req, ctx):
927
super(ProjectSetNew, self).populate(req, ctx)
929
def get_default_data(self, req):
932
def save_object(self, req, data):
933
new_set = ProjectSet()
934
new_set.offering = self.context
935
new_set.max_students_per_group = data['group_size']
936
req.store.add(new_set)
239
class OfferingEnrolmentSet(object):
240
def __init__(self, offering):
241
self.offering = offering
939
243
class Plugin(ViewPlugin, MediaPlugin):
940
forward_routes = (root_to_subject, root_to_semester, subject_to_offering,
941
offering_to_project, offering_to_projectset,
942
offering_to_enrolment)
944
subject_url, semester_url, offering_url, projectset_url, project_url,
244
forward_routes = (root_to_subject, subject_to_offering,
245
offering_to_project, offering_to_projectset)
246
reverse_routes = (subject_url, offering_url, projectset_url, project_url)
947
248
views = [(ApplicationRoot, ('subjects', '+index'), SubjectsView),
948
(ApplicationRoot, ('subjects', '+manage'), SubjectsManage),
949
(ApplicationRoot, ('subjects', '+new'), SubjectNew),
950
(ApplicationRoot, ('subjects', '+new-offering'), OfferingNew),
951
(ApplicationRoot, ('+semesters', '+new'), SemesterNew),
952
(Subject, '+index', SubjectView),
953
(Subject, '+edit', SubjectEdit),
954
(Subject, '+new-offering', SubjectOfferingNew),
955
(Semester, '+edit', SemesterEdit),
956
(Offering, '+index', OfferingView),
957
(Offering, '+edit', OfferingEdit),
958
(Offering, '+clone-worksheets', OfferingCloneWorksheets),
959
249
(Offering, ('+enrolments', '+index'), EnrolmentsView),
960
250
(Offering, ('+enrolments', '+new'), EnrolView),
961
(Enrolment, '+edit', EnrolmentEdit),
962
(Enrolment, '+delete', EnrolmentDelete),
963
251
(Offering, ('+projects', '+index'), OfferingProjectsView),
964
(Offering, ('+projects', '+new-set'), ProjectSetNew),
965
(ProjectSet, '+edit', ProjectSetEdit),
966
(ProjectSet, '+new', ProjectNew),
967
252
(Project, '+index', ProjectView),
968
(Project, '+edit', ProjectEdit),
969
(Project, '+delete', ProjectDelete),
254
(Offering, ('+projectsets', '+new'), OfferingRESTView, 'api'),
255
(ProjectSet, ('+projects', '+new'), ProjectSetRESTView, 'api'),
256
(Project, '+index', ProjectRESTView, 'api'),
972
259
breadcrumbs = {Subject: SubjectBreadcrumb,
973
260
Offering: OfferingBreadcrumb,
974
261
User: UserBreadcrumb,
975
262
Project: ProjectBreadcrumb,
976
Enrolment: EnrolmentBreadcrumb,