85
87
ctx['semesters'].append((semester, offerings))
87
# Admins get a separate list of subjects so they can add/edit.
89
ctx['subjects'] = req.store.find(Subject).order_by(Subject.name)
92
class SubjectShortNameUniquenessValidator(formencode.FancyValidator):
93
"""A FormEncode validator that checks that a subject name is unused.
90
class SubjectsManage(XHTMLView):
91
'''Subject management view.'''
92
template = 'templates/subjects-manage.html'
95
def authorize(self, req):
96
return req.user is not None and req.user.admin
98
def populate(self, req, ctx):
100
ctx['mediapath'] = media_url(req, CorePlugin, 'images/')
101
ctx['SubjectView'] = SubjectView
102
ctx['SubjectEdit'] = SubjectEdit
103
ctx['SemesterEdit'] = SemesterEdit
105
ctx['subjects'] = req.store.find(Subject).order_by(Subject.name)
106
ctx['semesters'] = req.store.find(Semester).order_by(
107
Semester.year, Semester.display_name)
110
class SubjectUniquenessValidator(formencode.FancyValidator):
111
"""A FormEncode validator that checks that a subject attribute is unique.
95
113
The subject referenced by state.existing_subject is permitted
96
114
to hold that name. If any other object holds it, the input is rejected.
116
:param attribute: the name of the attribute to check.
117
:param display: a string to identify the field in case of error.
98
def __init__(self, matching=None):
99
self.matching = matching
120
def __init__(self, attribute, display):
121
self.attribute = attribute
122
self.display = display
101
124
def _to_python(self, value, state):
102
if (state.store.find(
103
Subject, short_name=value).one() not in
125
if (state.store.find(Subject, **{self.attribute: value}).one() not in
104
126
(None, state.existing_subject)):
105
127
raise formencode.Invalid(
106
'Short name already taken', value, state)
128
'%s already taken' % self.display, value, state)
110
132
class SubjectSchema(formencode.Schema):
111
133
short_name = formencode.All(
112
SubjectShortNameUniquenessValidator(),
134
SubjectUniquenessValidator('short_name', 'URL name'),
135
URLNameValidator(not_empty=True))
136
name = formencode.validators.UnicodeString(not_empty=True)
137
code = formencode.All(
138
SubjectUniquenessValidator('code', 'Subject code'),
113
139
formencode.validators.UnicodeString(not_empty=True))
114
name = formencode.validators.UnicodeString(not_empty=True)
115
code = formencode.validators.UnicodeString(not_empty=True)
118
142
class SubjectFormView(BaseFormView):
430
491
new_offering.semester = data['semester']
431
492
new_offering.description = data['description']
432
493
new_offering.url = unicode(data['url']) if data['url'] else None
494
new_offering.worksheet_cutoff = data['worksheet_cutoff']
495
new_offering.show_worksheet_marks = data['show_worksheet_marks']
434
497
req.store.add(new_offering)
435
498
return new_offering
500
class SubjectOfferingNew(OfferingNew):
501
"""A form to create an offering for a given subject."""
502
# Identical to OfferingNew, except it forces the subject to be the subject
504
def populate(self, req, ctx):
505
super(SubjectOfferingNew, self).populate(req, ctx)
506
ctx['force_subject'] = self.context
508
class OfferingCloneWorksheetsSchema(formencode.Schema):
509
subject = formencode.All(
510
SubjectValidator(), formencode.validators.UnicodeString())
511
semester = formencode.All(
512
SemesterValidator(), formencode.validators.UnicodeString())
515
class OfferingCloneWorksheets(BaseFormView):
516
"""A form to clone worksheets from one offering to another."""
517
template = 'templates/offering-clone-worksheets.html'
520
def authorize(self, req):
521
return req.user is not None and req.user.admin
525
return OfferingCloneWorksheetsSchema()
527
def populate(self, req, ctx):
528
super(OfferingCloneWorksheets, self).populate(req, ctx)
529
ctx['subjects'] = req.store.find(Subject).order_by(Subject.name)
530
ctx['semesters'] = req.store.find(Semester).order_by(
531
Semester.year, Semester.display_name)
533
def get_default_data(self, req):
536
def save_object(self, req, data):
537
if self.context.worksheets.count() > 0:
539
"Cannot clone to target with existing worksheets.")
540
offering = req.store.find(
541
Offering, subject=data['subject'], semester=data['semester']).one()
543
raise BadRequest("No such offering.")
544
if offering.worksheets.count() == 0:
545
raise BadRequest("Source offering has no worksheets.")
547
self.context.clone_worksheets(offering)
438
551
class UserValidator(formencode.FancyValidator):
439
552
"""A FormEncode validator that turns a username into a user.
519
641
ctx['offering'] = self.context
520
642
ctx['roles_auth'] = self.context.get_permissions(req.user, req.config)
521
643
ctx['errors'] = errors
644
# If all of the fields validated, set the global form error.
645
if isinstance(errors, basestring):
646
ctx['error_value'] = errors
649
class EnrolmentEditSchema(formencode.Schema):
650
role = formencode.All(formencode.validators.OneOf(
651
["lecturer", "tutor", "student"]),
652
RoleEnrolmentValidator(),
653
formencode.validators.UnicodeString())
656
class EnrolmentEdit(BaseFormView):
657
"""A form to alter an enrolment's role."""
658
template = 'templates/enrolment-edit.html'
662
def populate_state(self, state):
663
state.offering = self.context.offering
665
def get_default_data(self, req):
666
return {'role': self.context.role}
670
return EnrolmentEditSchema()
672
def save_object(self, req, data):
673
self.context.role = data['role']
675
def get_return_url(self, obj):
676
return self.req.publisher.generate(
677
self.context.offering, EnrolmentsView)
679
def populate(self, req, ctx):
680
super(EnrolmentEdit, self).populate(req, ctx)
681
ctx['offering_perms'] = self.context.offering.get_permissions(
682
req.user, req.config)
685
class EnrolmentDelete(XHTMLView):
686
"""A form to alter an enrolment's role."""
687
template = 'templates/enrolment-delete.html'
691
def populate(self, req, ctx):
692
# If POSTing, delete delete delete.
693
if req.method == 'POST':
694
self.context.delete()
696
req.throw_redirect(req.publisher.generate(
697
self.context.offering, EnrolmentsView))
699
ctx['enrolment'] = self.context
523
702
class OfferingProjectsView(XHTMLView):
524
703
"""View the projects for an offering."""
525
704
template = 'templates/offering_projects.html'
526
705
permission = 'edit'
707
breadcrumb_text = 'Projects'
529
709
def populate(self, req, ctx):
530
710
self.plugin_styles[Plugin] = ["project.css"]
531
self.plugin_scripts[Plugin] = ["project.js"]
533
712
ctx['offering'] = self.context
534
713
ctx['projectsets'] = []
535
ctx['OfferingRESTView'] = OfferingRESTView
537
715
#Open the projectset Fragment, and render it for inclusion
538
716
#into the ProjectSets page
539
#XXX: This could be a lot cleaner
540
loader = genshi.template.TemplateLoader(".", auto_reload=True)
542
717
set_fragment = os.path.join(os.path.dirname(__file__),
543
718
"templates/projectset_fragment.html")
544
719
project_fragment = os.path.join(os.path.dirname(__file__),
545
720
"templates/project_fragment.html")
547
for projectset in self.context.project_sets:
548
settmpl = loader.load(set_fragment)
723
self.context.project_sets.order_by(ivle.database.ProjectSet.id):
724
settmpl = self._loader.load(set_fragment)
549
725
setCtx = Context()
550
726
setCtx['req'] = req
551
727
setCtx['projectset'] = projectset
552
728
setCtx['projects'] = []
553
729
setCtx['GroupsView'] = GroupsView
554
setCtx['ProjectSetRESTView'] = ProjectSetRESTView
730
setCtx['ProjectSetEdit'] = ProjectSetEdit
731
setCtx['ProjectNew'] = ProjectNew
556
for project in projectset.projects:
557
projecttmpl = loader.load(project_fragment)
734
projectset.projects.order_by(ivle.database.Project.deadline):
735
projecttmpl = self._loader.load(project_fragment)
558
736
projectCtx = Context()
559
737
projectCtx['req'] = req
560
738
projectCtx['project'] = project
739
projectCtx['ProjectEdit'] = ProjectEdit
740
projectCtx['ProjectDelete'] = ProjectDelete
562
742
setCtx['projects'].append(
563
743
projecttmpl.generate(projectCtx))
571
751
permission = "view_project_submissions"
574
def build_subversion_url(self, svnroot, submission):
575
princ = submission.assessed.principal
577
if isinstance(princ, User):
578
path = 'users/%s' % princ.login
580
path = 'groups/%s_%s_%s_%s' % (
581
princ.project_set.offering.subject.short_name,
582
princ.project_set.offering.semester.year,
583
princ.project_set.offering.semester.semester,
586
return urlparse.urljoin(
588
os.path.join(path, submission.path[1:] if
589
submission.path.startswith(os.sep) else
592
754
def populate(self, req, ctx):
593
755
self.plugin_styles[Plugin] = ["project.css"]
758
ctx['permissions'] = self.context.get_permissions(req.user,req.config)
596
759
ctx['GroupsView'] = GroupsView
597
760
ctx['EnrolView'] = EnrolView
598
ctx['format_datetime_short'] = ivle.date.format_datetime_for_paragraph
599
ctx['build_subversion_url'] = self.build_subversion_url
600
ctx['svn_addr'] = req.config['urls']['svn_addr']
601
ctx['project'] = self.context
602
ctx['user'] = req.user
761
ctx['format_datetime'] = ivle.date.make_date_nice
762
ctx['format_datetime_short'] = ivle.date.format_datetime_for_paragraph
763
ctx['project'] = self.context
764
ctx['user'] = req.user
765
ctx['ProjectEdit'] = ProjectEdit
766
ctx['ProjectDelete'] = ProjectDelete
767
ctx['ProjectExport'] = ProjectBashExportView
769
class ProjectBashExportView(TextView):
770
"""Produce a Bash script for exporting projects"""
771
template = "templates/project-export.sh"
772
content_type = "text/x-sh"
773
permission = "view_project_submissions"
775
def populate(self, req, ctx):
777
ctx['permissions'] = self.context.get_permissions(req.user,req.config)
778
ctx['format_datetime'] = ivle.date.make_date_nice
779
ctx['format_datetime_short'] = ivle.date.format_datetime_for_paragraph
780
ctx['project'] = self.context
781
ctx['user'] = req.user
782
ctx['now'] = datetime.datetime.now()
783
ctx['format_datetime'] = ivle.date.make_date_nice
784
ctx['format_datetime_short'] = ivle.date.format_datetime_for_paragraph
786
class ProjectUniquenessValidator(formencode.FancyValidator):
787
"""A FormEncode validator that checks that a project short_name is unique
790
The project referenced by state.existing_project is permitted to
791
hold that short_name. If any other project holds it, the input is rejected.
793
def _to_python(self, value, state):
794
if (state.store.find(
796
Project.short_name == unicode(value),
797
Project.project_set_id == ProjectSet.id,
798
ProjectSet.offering == state.offering).one() not in
799
(None, state.existing_project)):
800
raise formencode.Invalid(
801
"A project with that URL name already exists in this offering."
805
class ProjectSchema(formencode.Schema):
806
name = formencode.validators.UnicodeString(not_empty=True)
807
short_name = formencode.All(
808
URLNameValidator(not_empty=True),
809
ProjectUniquenessValidator())
810
deadline = DateTimeValidator(not_empty=True)
811
url = formencode.validators.URL(if_missing=None, not_empty=False)
812
synopsis = formencode.validators.UnicodeString(not_empty=True)
814
class ProjectEdit(BaseFormView):
815
"""A form to edit a project."""
816
template = 'templates/project-edit.html'
822
return ProjectSchema()
824
def populate(self, req, ctx):
825
super(ProjectEdit, self).populate(req, ctx)
826
ctx['projectset'] = self.context.project_set
828
def populate_state(self, state):
829
state.offering = self.context.project_set.offering
830
state.existing_project = self.context
832
def get_default_data(self, req):
834
'name': self.context.name,
835
'short_name': self.context.short_name,
836
'deadline': self.context.deadline,
837
'url': self.context.url,
838
'synopsis': self.context.synopsis,
841
def save_object(self, req, data):
842
self.context.name = data['name']
843
self.context.short_name = data['short_name']
844
self.context.deadline = data['deadline']
845
self.context.url = unicode(data['url']) if data['url'] else None
846
self.context.synopsis = data['synopsis']
849
class ProjectNew(BaseFormView):
850
"""A form to create a new project."""
851
template = 'templates/project-new.html'
857
return ProjectSchema()
859
def populate(self, req, ctx):
860
super(ProjectNew, self).populate(req, ctx)
861
ctx['projectset'] = self.context
863
def populate_state(self, state):
864
state.offering = self.context.offering
865
state.existing_project = None
867
def get_default_data(self, req):
870
def save_object(self, req, data):
871
new_project = Project()
872
new_project.project_set = self.context
873
new_project.name = data['name']
874
new_project.short_name = data['short_name']
875
new_project.deadline = data['deadline']
876
new_project.url = unicode(data['url']) if data['url'] else None
877
new_project.synopsis = data['synopsis']
878
req.store.add(new_project)
881
class ProjectDelete(XHTMLView):
882
"""A form to delete a project."""
883
template = 'templates/project-delete.html'
887
def populate(self, req, ctx):
888
# If post, delete the project, or display a message explaining that
889
# the project cannot be deleted
890
if self.context.can_delete:
891
if req.method == 'POST':
892
self.context.delete()
893
self.template = 'templates/project-deleted.html'
896
self.template = 'templates/project-undeletable.html'
898
# If get and can delete, display a delete confirmation page
900
# Variables for the template
902
ctx['project'] = self.context
903
ctx['OfferingProjectsView'] = OfferingProjectsView
905
class ProjectSetSchema(formencode.Schema):
906
group_size = formencode.validators.Int(if_missing=None, not_empty=False)
908
class ProjectSetEdit(BaseFormView):
909
"""A form to edit a project set."""
910
template = 'templates/projectset-edit.html'
916
return ProjectSetSchema()
918
def populate(self, req, ctx):
919
super(ProjectSetEdit, self).populate(req, ctx)
921
def get_default_data(self, req):
923
'group_size': self.context.max_students_per_group,
926
def save_object(self, req, data):
927
self.context.max_students_per_group = data['group_size']
930
class ProjectSetNew(BaseFormView):
931
"""A form to create a new project set."""
932
template = 'templates/projectset-new.html'
935
breadcrumb_text = "Projects"
939
return ProjectSetSchema()
941
def populate(self, req, ctx):
942
super(ProjectSetNew, self).populate(req, ctx)
944
def get_default_data(self, req):
947
def save_object(self, req, data):
948
new_set = ProjectSet()
949
new_set.offering = self.context
950
new_set.max_students_per_group = data['group_size']
951
req.store.add(new_set)
604
954
class Plugin(ViewPlugin, MediaPlugin):
605
955
forward_routes = (root_to_subject, root_to_semester, subject_to_offering,
606
offering_to_project, offering_to_projectset)
956
offering_to_project, offering_to_projectset,
957
offering_to_enrolment)
607
958
reverse_routes = (
608
subject_url, semester_url, offering_url, projectset_url, project_url)
959
subject_url, semester_url, offering_url, projectset_url, project_url,
610
962
views = [(ApplicationRoot, ('subjects', '+index'), SubjectsView),
963
(ApplicationRoot, ('subjects', '+manage'), SubjectsManage),
611
964
(ApplicationRoot, ('subjects', '+new'), SubjectNew),
612
965
(ApplicationRoot, ('subjects', '+new-offering'), OfferingNew),
613
966
(ApplicationRoot, ('+semesters', '+new'), SemesterNew),
967
(Subject, '+index', SubjectView),
614
968
(Subject, '+edit', SubjectEdit),
969
(Subject, '+new-offering', SubjectOfferingNew),
615
970
(Semester, '+edit', SemesterEdit),
616
971
(Offering, '+index', OfferingView),
617
972
(Offering, '+edit', OfferingEdit),
973
(Offering, '+clone-worksheets', OfferingCloneWorksheets),
618
974
(Offering, ('+enrolments', '+index'), EnrolmentsView),
619
975
(Offering, ('+enrolments', '+new'), EnrolView),
976
(Enrolment, '+edit', EnrolmentEdit),
977
(Enrolment, '+delete', EnrolmentDelete),
620
978
(Offering, ('+projects', '+index'), OfferingProjectsView),
979
(Offering, ('+projects', '+new-set'), ProjectSetNew),
980
(ProjectSet, '+edit', ProjectSetEdit),
981
(ProjectSet, '+new', ProjectNew),
621
982
(Project, '+index', ProjectView),
623
(Offering, ('+projectsets', '+new'), OfferingRESTView, 'api'),
624
(ProjectSet, ('+projects', '+new'), ProjectSetRESTView, 'api'),
983
(Project, '+edit', ProjectEdit),
984
(Project, '+delete', ProjectDelete),
985
(Project, ('+export', 'project-export.sh'),
986
ProjectBashExportView),
627
989
breadcrumbs = {Subject: SubjectBreadcrumb,
628
990
Offering: OfferingBreadcrumb,
629
991
User: UserBreadcrumb,
630
992
Project: ProjectBreadcrumb,
993
Enrolment: EnrolmentBreadcrumb,