693
655
def populate(self, req, ctx):
694
656
self.plugin_styles[Plugin] = ["project.css"]
657
self.plugin_scripts[Plugin] = ["project.js"]
696
659
ctx['offering'] = self.context
697
660
ctx['projectsets'] = []
661
ctx['OfferingRESTView'] = OfferingRESTView
699
663
#Open the projectset Fragment, and render it for inclusion
700
664
#into the ProjectSets page
665
#XXX: This could be a lot cleaner
666
loader = genshi.template.TemplateLoader(".", auto_reload=True)
701
668
set_fragment = os.path.join(os.path.dirname(__file__),
702
669
"templates/projectset_fragment.html")
703
670
project_fragment = os.path.join(os.path.dirname(__file__),
704
671
"templates/project_fragment.html")
707
self.context.project_sets.order_by(ivle.database.ProjectSet.id):
708
settmpl = self._loader.load(set_fragment)
673
for projectset in self.context.project_sets:
674
settmpl = loader.load(set_fragment)
709
675
setCtx = Context()
710
676
setCtx['req'] = req
711
677
setCtx['projectset'] = projectset
712
678
setCtx['projects'] = []
713
679
setCtx['GroupsView'] = GroupsView
714
setCtx['ProjectSetEdit'] = ProjectSetEdit
715
setCtx['ProjectNew'] = ProjectNew
680
setCtx['ProjectSetRESTView'] = ProjectSetRESTView
718
projectset.projects.order_by(ivle.database.Project.deadline):
719
projecttmpl = self._loader.load(project_fragment)
682
for project in projectset.projects:
683
projecttmpl = loader.load(project_fragment)
720
684
projectCtx = Context()
721
685
projectCtx['req'] = req
722
686
projectCtx['project'] = project
723
projectCtx['ProjectEdit'] = ProjectEdit
724
projectCtx['ProjectDelete'] = ProjectDelete
726
688
setCtx['projects'].append(
727
689
projecttmpl.generate(projectCtx))
735
697
permission = "view_project_submissions"
700
def build_subversion_url(self, svnroot, submission):
701
princ = submission.assessed.principal
703
if isinstance(princ, User):
704
path = 'users/%s' % princ.login
706
path = 'groups/%s_%s_%s_%s' % (
707
princ.project_set.offering.subject.short_name,
708
princ.project_set.offering.semester.year,
709
princ.project_set.offering.semester.semester,
712
return urlparse.urljoin(
714
os.path.join(path, submission.path[1:] if
715
submission.path.startswith(os.sep) else
738
718
def populate(self, req, ctx):
739
719
self.plugin_styles[Plugin] = ["project.css"]
742
ctx['permissions'] = self.context.get_permissions(req.user,req.config)
743
722
ctx['GroupsView'] = GroupsView
744
723
ctx['EnrolView'] = EnrolView
745
ctx['format_datetime'] = ivle.date.make_date_nice
746
724
ctx['format_datetime_short'] = ivle.date.format_datetime_for_paragraph
725
ctx['build_subversion_url'] = self.build_subversion_url
726
ctx['svn_addr'] = req.config['urls']['svn_addr']
747
727
ctx['project'] = self.context
748
728
ctx['user'] = req.user
749
ctx['ProjectEdit'] = ProjectEdit
750
ctx['ProjectDelete'] = ProjectDelete
752
class ProjectUniquenessValidator(formencode.FancyValidator):
753
"""A FormEncode validator that checks that a project short_name is unique
756
The project referenced by state.existing_project is permitted to
757
hold that short_name. If any other project holds it, the input is rejected.
759
def _to_python(self, value, state):
760
if (state.store.find(
762
Project.short_name == unicode(value),
763
Project.project_set_id == ProjectSet.id,
764
ProjectSet.offering == state.offering).one() not in
765
(None, state.existing_project)):
766
raise formencode.Invalid(
767
"A project with that URL name already exists in this offering."
771
class ProjectSchema(formencode.Schema):
772
name = formencode.validators.UnicodeString(not_empty=True)
773
short_name = formencode.All(
774
URLNameValidator(not_empty=True),
775
ProjectUniquenessValidator())
776
deadline = DateTimeValidator(not_empty=True)
777
url = formencode.validators.URL(if_missing=None, not_empty=False)
778
synopsis = formencode.validators.UnicodeString(not_empty=True)
780
class ProjectEdit(BaseFormView):
781
"""A form to edit a project."""
782
template = 'templates/project-edit.html'
788
return ProjectSchema()
790
def populate(self, req, ctx):
791
super(ProjectEdit, self).populate(req, ctx)
792
ctx['projectset'] = self.context.project_set
794
def populate_state(self, state):
795
state.offering = self.context.project_set.offering
796
state.existing_project = self.context
798
def get_default_data(self, req):
800
'name': self.context.name,
801
'short_name': self.context.short_name,
802
'deadline': self.context.deadline,
803
'url': self.context.url,
804
'synopsis': self.context.synopsis,
807
def save_object(self, req, data):
808
self.context.name = data['name']
809
self.context.short_name = data['short_name']
810
self.context.deadline = data['deadline']
811
self.context.url = unicode(data['url']) if data['url'] else None
812
self.context.synopsis = data['synopsis']
815
class ProjectNew(BaseFormView):
816
"""A form to create a new project."""
817
template = 'templates/project-new.html'
823
return ProjectSchema()
825
def populate(self, req, ctx):
826
super(ProjectNew, self).populate(req, ctx)
827
ctx['projectset'] = self.context
829
def populate_state(self, state):
830
state.offering = self.context.offering
831
state.existing_project = None
833
def get_default_data(self, req):
836
def save_object(self, req, data):
837
new_project = Project()
838
new_project.project_set = self.context
839
new_project.name = data['name']
840
new_project.short_name = data['short_name']
841
new_project.deadline = data['deadline']
842
new_project.url = unicode(data['url']) if data['url'] else None
843
new_project.synopsis = data['synopsis']
844
req.store.add(new_project)
847
class ProjectDelete(XHTMLView):
848
"""A form to delete a project."""
849
template = 'templates/project-delete.html'
853
def populate(self, req, ctx):
854
# If post, delete the project, or display a message explaining that
855
# the project cannot be deleted
856
if self.context.can_delete:
857
if req.method == 'POST':
858
self.context.delete()
859
self.template = 'templates/project-deleted.html'
862
self.template = 'templates/project-undeletable.html'
864
# If get and can delete, display a delete confirmation page
866
# Variables for the template
868
ctx['project'] = self.context
869
ctx['OfferingProjectsView'] = OfferingProjectsView
871
class ProjectSetSchema(formencode.Schema):
872
group_size = formencode.validators.Int(if_missing=None, not_empty=False)
874
class ProjectSetEdit(BaseFormView):
875
"""A form to edit a project set."""
876
template = 'templates/projectset-edit.html'
882
return ProjectSetSchema()
884
def populate(self, req, ctx):
885
super(ProjectSetEdit, self).populate(req, ctx)
887
def get_default_data(self, req):
889
'group_size': self.context.max_students_per_group,
892
def save_object(self, req, data):
893
self.context.max_students_per_group = data['group_size']
896
class ProjectSetNew(BaseFormView):
897
"""A form to create a new project set."""
898
template = 'templates/projectset-new.html'
901
breadcrumb_text = "Projects"
905
return ProjectSetSchema()
907
def populate(self, req, ctx):
908
super(ProjectSetNew, self).populate(req, ctx)
910
def get_default_data(self, req):
913
def save_object(self, req, data):
914
new_set = ProjectSet()
915
new_set.offering = self.context
916
new_set.max_students_per_group = data['group_size']
917
req.store.add(new_set)
920
730
class Plugin(ViewPlugin, MediaPlugin):
921
731
forward_routes = (root_to_subject, root_to_semester, subject_to_offering,
942
750
(Enrolment, '+edit', EnrolmentEdit),
943
751
(Enrolment, '+delete', EnrolmentDelete),
944
752
(Offering, ('+projects', '+index'), OfferingProjectsView),
945
(Offering, ('+projects', '+new-set'), ProjectSetNew),
946
(ProjectSet, '+edit', ProjectSetEdit),
947
(ProjectSet, '+new', ProjectNew),
948
753
(Project, '+index', ProjectView),
949
(Project, '+edit', ProjectEdit),
950
(Project, '+delete', ProjectDelete),
755
(Offering, ('+projectsets', '+new'), OfferingRESTView, 'api'),
756
(ProjectSet, ('+projects', '+new'), ProjectSetRESTView, 'api'),
953
759
breadcrumbs = {Subject: SubjectBreadcrumb,