471
396
new_offering.semester = data['semester']
472
397
new_offering.description = data['description']
473
398
new_offering.url = unicode(data['url']) if data['url'] else None
474
new_offering.show_worksheet_marks = data['show_worksheet_marks']
476
400
req.store.add(new_offering)
477
401
return new_offering
479
class SubjectOfferingNew(OfferingNew):
480
"""A form to create an offering for a given subject."""
481
# Identical to OfferingNew, except it forces the subject to be the subject
483
def populate(self, req, ctx):
484
super(SubjectOfferingNew, self).populate(req, ctx)
485
ctx['force_subject'] = self.context
487
class OfferingCloneWorksheetsSchema(formencode.Schema):
488
subject = formencode.All(
489
SubjectValidator(), formencode.validators.UnicodeString())
490
semester = formencode.All(
491
SemesterValidator(), formencode.validators.UnicodeString())
494
class OfferingCloneWorksheets(BaseFormView):
495
"""A form to clone worksheets from one offering to another."""
496
template = 'templates/offering-clone-worksheets.html'
499
def authorize(self, req):
500
return req.user is not None and req.user.admin
504
return OfferingCloneWorksheetsSchema()
506
def populate(self, req, ctx):
507
super(OfferingCloneWorksheets, self).populate(req, ctx)
508
ctx['subjects'] = req.store.find(Subject).order_by(Subject.name)
509
ctx['semesters'] = req.store.find(Semester).order_by(
510
Semester.year, Semester.semester)
512
def get_default_data(self, req):
515
def save_object(self, req, data):
516
if self.context.worksheets.count() > 0:
518
"Cannot clone to target with existing worksheets.")
519
offering = req.store.find(
520
Offering, subject=data['subject'], semester=data['semester']).one()
522
raise BadRequest("No such offering.")
523
if offering.worksheets.count() == 0:
524
raise BadRequest("Source offering has no worksheets.")
526
self.context.clone_worksheets(offering)
530
404
class UserValidator(formencode.FancyValidator):
531
405
"""A FormEncode validator that turns a username into a user.
620
485
ctx['offering'] = self.context
621
486
ctx['roles_auth'] = self.context.get_permissions(req.user, req.config)
622
487
ctx['errors'] = errors
623
# If all of the fields validated, set the global form error.
624
if isinstance(errors, basestring):
625
ctx['error_value'] = errors
628
class EnrolmentEditSchema(formencode.Schema):
629
role = formencode.All(formencode.validators.OneOf(
630
["lecturer", "tutor", "student"]),
631
RoleEnrolmentValidator(),
632
formencode.validators.UnicodeString())
635
class EnrolmentEdit(BaseFormView):
636
"""A form to alter an enrolment's role."""
637
template = 'templates/enrolment-edit.html'
641
def populate_state(self, state):
642
state.offering = self.context.offering
644
def get_default_data(self, req):
645
return {'role': self.context.role}
649
return EnrolmentEditSchema()
651
def save_object(self, req, data):
652
self.context.role = data['role']
654
def get_return_url(self, obj):
655
return self.req.publisher.generate(
656
self.context.offering, EnrolmentsView)
658
def populate(self, req, ctx):
659
super(EnrolmentEdit, self).populate(req, ctx)
660
ctx['offering_perms'] = self.context.offering.get_permissions(
661
req.user, req.config)
664
class EnrolmentDelete(XHTMLView):
665
"""A form to alter an enrolment's role."""
666
template = 'templates/enrolment-delete.html'
670
def populate(self, req, ctx):
671
# If POSTing, delete delete delete.
672
if req.method == 'POST':
673
self.context.delete()
675
req.throw_redirect(req.publisher.generate(
676
self.context.offering, EnrolmentsView))
678
ctx['enrolment'] = self.context
681
489
class OfferingProjectsView(XHTMLView):
682
490
"""View the projects for an offering."""
683
491
template = 'templates/offering_projects.html'
684
492
permission = 'edit'
686
breadcrumb_text = 'Projects'
688
495
def populate(self, req, ctx):
689
496
self.plugin_styles[Plugin] = ["project.css"]
497
self.plugin_scripts[Plugin] = ["project.js"]
691
499
ctx['offering'] = self.context
692
500
ctx['projectsets'] = []
501
ctx['OfferingRESTView'] = OfferingRESTView
694
503
#Open the projectset Fragment, and render it for inclusion
695
504
#into the ProjectSets page
505
#XXX: This could be a lot cleaner
506
loader = genshi.template.TemplateLoader(".", auto_reload=True)
696
508
set_fragment = os.path.join(os.path.dirname(__file__),
697
509
"templates/projectset_fragment.html")
698
510
project_fragment = os.path.join(os.path.dirname(__file__),
699
511
"templates/project_fragment.html")
702
self.context.project_sets.order_by(ivle.database.ProjectSet.id):
703
settmpl = self._loader.load(set_fragment)
513
for projectset in self.context.project_sets:
514
settmpl = loader.load(set_fragment)
704
515
setCtx = Context()
705
516
setCtx['req'] = req
706
517
setCtx['projectset'] = projectset
707
518
setCtx['projects'] = []
708
519
setCtx['GroupsView'] = GroupsView
709
setCtx['ProjectSetEdit'] = ProjectSetEdit
710
setCtx['ProjectNew'] = ProjectNew
520
setCtx['ProjectSetRESTView'] = ProjectSetRESTView
713
projectset.projects.order_by(ivle.database.Project.deadline):
714
projecttmpl = self._loader.load(project_fragment)
522
for project in projectset.projects:
523
projecttmpl = loader.load(project_fragment)
715
524
projectCtx = Context()
716
525
projectCtx['req'] = req
717
526
projectCtx['project'] = project
718
projectCtx['ProjectEdit'] = ProjectEdit
719
projectCtx['ProjectDelete'] = ProjectDelete
721
528
setCtx['projects'].append(
722
529
projecttmpl.generate(projectCtx))
752
559
self.plugin_styles[Plugin] = ["project.css"]
755
ctx['permissions'] = self.context.get_permissions(req.user,req.config)
756
562
ctx['GroupsView'] = GroupsView
757
563
ctx['EnrolView'] = EnrolView
758
ctx['format_datetime'] = ivle.date.make_date_nice
759
564
ctx['format_datetime_short'] = ivle.date.format_datetime_for_paragraph
760
565
ctx['build_subversion_url'] = self.build_subversion_url
761
566
ctx['svn_addr'] = req.config['urls']['svn_addr']
762
567
ctx['project'] = self.context
763
568
ctx['user'] = req.user
764
ctx['ProjectEdit'] = ProjectEdit
765
ctx['ProjectDelete'] = ProjectDelete
767
class ProjectUniquenessValidator(formencode.FancyValidator):
768
"""A FormEncode validator that checks that a project short_name is unique
771
The project referenced by state.existing_project is permitted to
772
hold that short_name. If any other project holds it, the input is rejected.
774
def _to_python(self, value, state):
775
if (state.store.find(
777
Project.short_name == unicode(value),
778
Project.project_set_id == ProjectSet.id,
779
ProjectSet.offering == state.offering).one() not in
780
(None, state.existing_project)):
781
raise formencode.Invalid(
782
"A project with that URL name already exists in this offering."
786
class ProjectSchema(formencode.Schema):
787
name = formencode.validators.UnicodeString(not_empty=True)
788
short_name = formencode.All(
789
URLNameValidator(not_empty=True),
790
ProjectUniquenessValidator())
791
deadline = DateTimeValidator(not_empty=True)
792
url = formencode.validators.URL(if_missing=None, not_empty=False)
793
synopsis = formencode.validators.UnicodeString(not_empty=True)
795
class ProjectEdit(BaseFormView):
796
"""A form to edit a project."""
797
template = 'templates/project-edit.html'
803
return ProjectSchema()
805
def populate(self, req, ctx):
806
super(ProjectEdit, self).populate(req, ctx)
807
ctx['projectset'] = self.context.project_set
809
def populate_state(self, state):
810
state.offering = self.context.project_set.offering
811
state.existing_project = self.context
813
def get_default_data(self, req):
815
'name': self.context.name,
816
'short_name': self.context.short_name,
817
'deadline': self.context.deadline,
818
'url': self.context.url,
819
'synopsis': self.context.synopsis,
822
def save_object(self, req, data):
823
self.context.name = data['name']
824
self.context.short_name = data['short_name']
825
self.context.deadline = data['deadline']
826
self.context.url = unicode(data['url']) if data['url'] else None
827
self.context.synopsis = data['synopsis']
830
class ProjectNew(BaseFormView):
831
"""A form to create a new project."""
832
template = 'templates/project-new.html'
838
return ProjectSchema()
840
def populate(self, req, ctx):
841
super(ProjectNew, self).populate(req, ctx)
842
ctx['projectset'] = self.context
844
def populate_state(self, state):
845
state.offering = self.context.offering
846
state.existing_project = None
848
def get_default_data(self, req):
851
def save_object(self, req, data):
852
new_project = Project()
853
new_project.project_set = self.context
854
new_project.name = data['name']
855
new_project.short_name = data['short_name']
856
new_project.deadline = data['deadline']
857
new_project.url = unicode(data['url']) if data['url'] else None
858
new_project.synopsis = data['synopsis']
859
req.store.add(new_project)
862
class ProjectDelete(XHTMLView):
863
"""A form to delete a project."""
864
template = 'templates/project-delete.html'
868
def populate(self, req, ctx):
869
# If post, delete the project, or display a message explaining that
870
# the project cannot be deleted
871
if self.context.can_delete:
872
if req.method == 'POST':
873
self.context.delete()
874
self.template = 'templates/project-deleted.html'
877
self.template = 'templates/project-undeletable.html'
879
# If get and can delete, display a delete confirmation page
881
# Variables for the template
883
ctx['project'] = self.context
884
ctx['OfferingProjectsView'] = OfferingProjectsView
886
class ProjectSetSchema(formencode.Schema):
887
group_size = formencode.validators.Int(if_missing=None, not_empty=False)
889
class ProjectSetEdit(BaseFormView):
890
"""A form to edit a project set."""
891
template = 'templates/projectset-edit.html'
897
return ProjectSetSchema()
899
def populate(self, req, ctx):
900
super(ProjectSetEdit, self).populate(req, ctx)
902
def get_default_data(self, req):
904
'group_size': self.context.max_students_per_group,
907
def save_object(self, req, data):
908
self.context.max_students_per_group = data['group_size']
911
class ProjectSetNew(BaseFormView):
912
"""A form to create a new project set."""
913
template = 'templates/projectset-new.html'
916
breadcrumb_text = "Projects"
920
return ProjectSetSchema()
922
def populate(self, req, ctx):
923
super(ProjectSetNew, self).populate(req, ctx)
925
def get_default_data(self, req):
928
def save_object(self, req, data):
929
new_set = ProjectSet()
930
new_set.offering = self.context
931
new_set.max_students_per_group = data['group_size']
932
req.store.add(new_set)
935
570
class Plugin(ViewPlugin, MediaPlugin):
936
forward_routes = (root_to_subject, root_to_semester, subject_to_offering,
937
offering_to_project, offering_to_projectset,
938
offering_to_enrolment)
940
subject_url, semester_url, offering_url, projectset_url, project_url,
571
forward_routes = (root_to_subject, subject_to_offering,
572
offering_to_project, offering_to_projectset)
573
reverse_routes = (subject_url, offering_url, projectset_url, project_url)
943
575
views = [(ApplicationRoot, ('subjects', '+index'), SubjectsView),
944
(ApplicationRoot, ('subjects', '+manage'), SubjectsManage),
945
576
(ApplicationRoot, ('subjects', '+new'), SubjectNew),
946
577
(ApplicationRoot, ('subjects', '+new-offering'), OfferingNew),
947
(ApplicationRoot, ('+semesters', '+new'), SemesterNew),
948
(Subject, '+index', SubjectView),
578
(ApplicationRoot, ('subjects', '+new-semester'), SemesterNew),
949
579
(Subject, '+edit', SubjectEdit),
950
(Subject, '+new-offering', SubjectOfferingNew),
951
(Semester, '+edit', SemesterEdit),
952
580
(Offering, '+index', OfferingView),
953
581
(Offering, '+edit', OfferingEdit),
954
(Offering, '+clone-worksheets', OfferingCloneWorksheets),
955
582
(Offering, ('+enrolments', '+index'), EnrolmentsView),
956
583
(Offering, ('+enrolments', '+new'), EnrolView),
957
(Enrolment, '+edit', EnrolmentEdit),
958
(Enrolment, '+delete', EnrolmentDelete),
959
584
(Offering, ('+projects', '+index'), OfferingProjectsView),
960
(Offering, ('+projects', '+new-set'), ProjectSetNew),
961
(ProjectSet, '+edit', ProjectSetEdit),
962
(ProjectSet, '+new', ProjectNew),
963
585
(Project, '+index', ProjectView),
964
(Project, '+edit', ProjectEdit),
965
(Project, '+delete', ProjectDelete),
587
(Offering, ('+projectsets', '+new'), OfferingRESTView, 'api'),
588
(ProjectSet, ('+projects', '+new'), ProjectSetRESTView, 'api'),
968
591
breadcrumbs = {Subject: SubjectBreadcrumb,
969
592
Offering: OfferingBreadcrumb,
970
593
User: UserBreadcrumb,
971
594
Project: ProjectBreadcrumb,
972
Enrolment: EnrolmentBreadcrumb,