~azzar1/unity/add-show-desktop-key

« back to all changes in this revision

Viewing changes to ivle/webapp/admin/subject.py

  • Committer: William Grant
  • Date: 2010-07-29 11:34:44 UTC
  • Revision ID: grantw@unimelb.edu.au-20100729113444-ht6s5kdcixvfubhr
Merge video and audio handlers, and reword them.

Show diffs side-by-side

added added

removed removed

Lines of Context:
27
27
import urllib
28
28
import urlparse
29
29
import cgi
 
30
import datetime
30
31
 
31
32
from storm.locals import Desc, Store
32
33
import genshi
33
34
from genshi.filters import HTMLFormFiller
34
 
from genshi.template import Context, TemplateLoader
 
35
from genshi.template import Context
35
36
import formencode
36
37
import formencode.validators
37
38
 
38
 
from ivle.webapp.base.forms import BaseFormView
 
39
from ivle.webapp.base.forms import (BaseFormView, URLNameValidator,
 
40
                                    DateTimeValidator)
39
41
from ivle.webapp.base.plugins import ViewPlugin, MediaPlugin
40
42
from ivle.webapp.base.xhtml import XHTMLView
 
43
from ivle.webapp.base.text import TextView
41
44
from ivle.webapp.errors import BadRequest
42
45
from ivle.webapp import ApplicationRoot
43
46
 
46
49
from ivle import util
47
50
import ivle.date
48
51
 
49
 
from ivle.webapp.admin.projectservice import ProjectSetRESTView
50
 
from ivle.webapp.admin.offeringservice import OfferingRESTView
51
52
from ivle.webapp.admin.publishing import (root_to_subject, root_to_semester,
52
53
            subject_to_offering, offering_to_projectset, offering_to_project,
53
54
            offering_to_enrolment, subject_url, semester_url, offering_url,
54
55
            projectset_url, project_url, enrolment_url)
55
56
from ivle.webapp.admin.breadcrumbs import (SubjectBreadcrumb,
56
57
            OfferingBreadcrumb, UserBreadcrumb, ProjectBreadcrumb,
57
 
            EnrolmentBreadcrumb)
 
58
            ProjectsBreadcrumb, EnrolmentBreadcrumb)
58
59
from ivle.webapp.core import Plugin as CorePlugin
59
60
from ivle.webapp.groups import GroupsView
60
61
from ivle.webapp.media import media_url
64
65
    '''The view of the list of subjects.'''
65
66
    template = 'templates/subjects.html'
66
67
    tab = 'subjects'
 
68
    breadcrumb_text = "Subjects"
67
69
 
68
70
    def authorize(self, req):
69
71
        return req.user is not None
73
75
        ctx['user'] = req.user
74
76
        ctx['semesters'] = []
75
77
 
76
 
        for semester in req.store.find(Semester).order_by(Desc(Semester.year),
77
 
                                                     Desc(Semester.semester)):
 
78
        for semester in req.store.find(Semester).order_by(
 
79
            Desc(Semester.year), Desc(Semester.display_name)):
78
80
            if req.user.admin:
79
81
                # For admins, show all subjects in the system
80
82
                offerings = list(semester.offerings.find())
96
98
    def populate(self, req, ctx):
97
99
        ctx['req'] = req
98
100
        ctx['mediapath'] = media_url(req, CorePlugin, 'images/')
 
101
        ctx['SubjectView'] = SubjectView
99
102
        ctx['SubjectEdit'] = SubjectEdit
100
103
        ctx['SemesterEdit'] = SemesterEdit
101
104
 
102
105
        ctx['subjects'] = req.store.find(Subject).order_by(Subject.name)
103
106
        ctx['semesters'] = req.store.find(Semester).order_by(
104
 
            Semester.year, Semester.semester)
105
 
 
106
 
 
107
 
class SubjectShortNameUniquenessValidator(formencode.FancyValidator):
108
 
    """A FormEncode validator that checks that a subject name is unused.
 
107
            Semester.year, Semester.display_name)
 
108
 
 
109
 
 
110
class SubjectUniquenessValidator(formencode.FancyValidator):
 
111
    """A FormEncode validator that checks that a subject attribute is unique.
109
112
 
110
113
    The subject referenced by state.existing_subject is permitted
111
114
    to hold that name. If any other object holds it, the input is rejected.
 
115
 
 
116
    :param attribute: the name of the attribute to check.
 
117
    :param display: a string to identify the field in case of error.
112
118
    """
113
 
    def __init__(self, matching=None):
114
 
        self.matching = matching
 
119
 
 
120
    def __init__(self, attribute, display):
 
121
        self.attribute = attribute
 
122
        self.display = display
115
123
 
116
124
    def _to_python(self, value, state):
117
 
        if (state.store.find(
118
 
                Subject, short_name=value).one() not in
 
125
        if (state.store.find(Subject, **{self.attribute: value}).one() not in
119
126
                (None, state.existing_subject)):
120
127
            raise formencode.Invalid(
121
 
                'Short name already taken', value, state)
 
128
                '%s already taken' % self.display, value, state)
122
129
        return value
123
130
 
124
131
 
125
132
class SubjectSchema(formencode.Schema):
126
133
    short_name = formencode.All(
127
 
        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'),
128
139
        formencode.validators.UnicodeString(not_empty=True))
129
 
    name = formencode.validators.UnicodeString(not_empty=True)
130
 
    code = formencode.validators.UnicodeString(not_empty=True)
131
140
 
132
141
 
133
142
class SubjectFormView(BaseFormView):
144
153
    def validator(self):
145
154
        return SubjectSchema()
146
155
 
147
 
    def get_return_url(self, obj):
148
 
        return '/subjects'
149
 
 
150
156
 
151
157
class SubjectNew(SubjectFormView):
152
158
    """A form to create a subject."""
194
200
    """
195
201
    def _to_python(self, value, state):
196
202
        if (state.store.find(
197
 
                Semester, year=value['year'], semester=value['semester']
 
203
                Semester, year=value['year'], url_name=value['url_name']
198
204
                ).one() not in (None, state.existing_semester)):
199
205
            raise formencode.Invalid(
200
206
                'Semester already exists', value, state)
202
208
 
203
209
 
204
210
class SemesterSchema(formencode.Schema):
205
 
    year = formencode.validators.UnicodeString()
206
 
    semester = formencode.validators.UnicodeString()
 
211
    year = URLNameValidator()
 
212
    code = formencode.validators.UnicodeString()
 
213
    url_name = URLNameValidator()
 
214
    display_name = formencode.validators.UnicodeString()
207
215
    state = formencode.All(
208
216
        formencode.validators.OneOf(["past", "current", "future"]),
209
217
        formencode.validators.UnicodeString())
238
246
    def save_object(self, req, data):
239
247
        new_semester = Semester()
240
248
        new_semester.year = data['year']
241
 
        new_semester.semester = data['semester']
 
249
        new_semester.code = data['code']
 
250
        new_semester.url_name = data['url_name']
 
251
        new_semester.display_name = data['display_name']
242
252
        new_semester.state = data['state']
243
253
 
244
254
        req.store.add(new_semester)
255
265
    def get_default_data(self, req):
256
266
        return {
257
267
            'year': self.context.year,
258
 
            'semester': self.context.semester,
 
268
            'code': self.context.code,
 
269
            'url_name': self.context.url_name,
 
270
            'display_name': self.context.display_name,
259
271
            'state': self.context.state,
260
272
            }
261
273
 
262
274
    def save_object(self, req, data):
263
275
        self.context.year = data['year']
264
 
        self.context.semester = data['semester']
 
276
        self.context.code = data['code']
 
277
        self.context.url_name = data['url_name']
 
278
        self.context.display_name = data['display_name']
265
279
        self.context.state = data['state']
266
280
 
267
281
        return self.context
268
282
 
 
283
class SubjectView(XHTMLView):
 
284
    '''The view of the list of offerings in a given subject.'''
 
285
    template = 'templates/subject.html'
 
286
    tab = 'subjects'
 
287
 
 
288
    def authorize(self, req):
 
289
        return req.user is not None
 
290
 
 
291
    def populate(self, req, ctx):
 
292
        ctx['context'] = self.context
 
293
        ctx['req'] = req
 
294
        ctx['user'] = req.user
 
295
        ctx['offerings'] = list(self.context.offerings)
 
296
        ctx['permissions'] = self.context.get_permissions(req.user,req.config)
 
297
        ctx['SubjectEdit'] = SubjectEdit
 
298
        ctx['SubjectOfferingNew'] = SubjectOfferingNew
 
299
 
269
300
 
270
301
class OfferingView(XHTMLView):
271
302
    """The home page of an offering."""
286
317
        ctx['OfferingCloneWorksheets'] = OfferingCloneWorksheets
287
318
        ctx['GroupsView'] = GroupsView
288
319
        ctx['EnrolmentsView'] = EnrolmentsView
 
320
        ctx['Project'] = ivle.database.Project
289
321
 
290
322
        # As we go, calculate the total score for this subject
291
323
        # (Assessable worksheets only, mandatory problems only)
292
324
 
293
325
        ctx['worksheets'], problems_total, problems_done = (
294
326
            ivle.worksheet.utils.create_list_of_fake_worksheets_and_stats(
295
 
                req.store, req.user, self.context))
 
327
                req.config, req.store, req.user, self.context,
 
328
                as_of=self.context.worksheet_cutoff))
296
329
 
297
330
        ctx['exercises_total'] = problems_total
298
331
        ctx['exercises_done'] = problems_done
339
372
            year = semester = None
340
373
 
341
374
        semester = state.store.find(
342
 
            Semester, year=year, semester=semester).one()
 
375
            Semester, year=year, url_name=semester).one()
343
376
        if semester:
344
377
            return semester
345
378
        else:
369
402
    description = formencode.validators.UnicodeString(
370
403
        if_missing=None, not_empty=False)
371
404
    url = formencode.validators.URL(if_missing=None, not_empty=False)
 
405
    worksheet_cutoff = DateTimeValidator(if_missing=None, not_empty=False)
 
406
    show_worksheet_marks = formencode.validators.StringBoolean(
 
407
        if_missing=False)
372
408
 
373
409
 
374
410
class OfferingAdminSchema(OfferingSchema):
396
432
        super(OfferingEdit, self).populate(req, ctx)
397
433
        ctx['subjects'] = req.store.find(Subject).order_by(Subject.name)
398
434
        ctx['semesters'] = req.store.find(Semester).order_by(
399
 
            Semester.year, Semester.semester)
 
435
            Semester.year, Semester.display_name)
 
436
        ctx['force_subject'] = None
400
437
 
401
438
    def populate_state(self, state):
402
439
        state.existing_offering = self.context
405
442
        return {
406
443
            'subject': self.context.subject.short_name,
407
444
            'semester': self.context.semester.year + '/' +
408
 
                        self.context.semester.semester,
 
445
                        self.context.semester.url_name,
409
446
            'url': self.context.url,
410
447
            'description': self.context.description,
 
448
            'worksheet_cutoff': self.context.worksheet_cutoff,
 
449
            'show_worksheet_marks': self.context.show_worksheet_marks,
411
450
            }
412
451
 
413
452
    def save_object(self, req, data):
416
455
            self.context.semester = data['semester']
417
456
        self.context.description = data['description']
418
457
        self.context.url = unicode(data['url']) if data['url'] else None
 
458
        self.context.worksheet_cutoff = data['worksheet_cutoff']
 
459
        self.context.show_worksheet_marks = data['show_worksheet_marks']
419
460
        return self.context
420
461
 
421
462
 
435
476
        super(OfferingNew, self).populate(req, ctx)
436
477
        ctx['subjects'] = req.store.find(Subject).order_by(Subject.name)
437
478
        ctx['semesters'] = req.store.find(Semester).order_by(
438
 
            Semester.year, Semester.semester)
 
479
            Semester.year, Semester.display_name)
 
480
        ctx['force_subject'] = None
439
481
 
440
482
    def populate_state(self, state):
441
483
        state.existing_offering = None
449
491
        new_offering.semester = data['semester']
450
492
        new_offering.description = data['description']
451
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']
452
496
 
453
497
        req.store.add(new_offering)
454
498
        return new_offering
455
499
 
 
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
 
503
    # in context
 
504
    def populate(self, req, ctx):
 
505
        super(SubjectOfferingNew, self).populate(req, ctx)
 
506
        ctx['force_subject'] = self.context
456
507
 
457
508
class OfferingCloneWorksheetsSchema(formencode.Schema):
458
509
    subject = formencode.All(
477
528
        super(OfferingCloneWorksheets, self).populate(req, ctx)
478
529
        ctx['subjects'] = req.store.find(Subject).order_by(Subject.name)
479
530
        ctx['semesters'] = req.store.find(Semester).order_by(
480
 
            Semester.year, Semester.semester)
 
531
            Semester.year, Semester.display_name)
481
532
 
482
533
    def get_default_data(self, req):
483
534
        return {}
590
641
        ctx['offering'] = self.context
591
642
        ctx['roles_auth'] = self.context.get_permissions(req.user, req.config)
592
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
593
647
 
594
648
 
595
649
class EnrolmentEditSchema(formencode.Schema):
654
708
 
655
709
    def populate(self, req, ctx):
656
710
        self.plugin_styles[Plugin] = ["project.css"]
657
 
        self.plugin_scripts[Plugin] = ["project.js"]
658
711
        ctx['req'] = req
659
712
        ctx['offering'] = self.context
660
713
        ctx['projectsets'] = []
661
 
        ctx['OfferingRESTView'] = OfferingRESTView
662
714
 
663
715
        #Open the projectset Fragment, and render it for inclusion
664
716
        #into the ProjectSets page
665
 
        #XXX: This could be a lot cleaner
666
 
        loader = genshi.template.TemplateLoader(".", auto_reload=True)
667
 
 
668
717
        set_fragment = os.path.join(os.path.dirname(__file__),
669
718
                "templates/projectset_fragment.html")
670
719
        project_fragment = os.path.join(os.path.dirname(__file__),
671
720
                "templates/project_fragment.html")
672
721
 
673
 
        for projectset in self.context.project_sets:
674
 
            settmpl = loader.load(set_fragment)
 
722
        for projectset in \
 
723
            self.context.project_sets.order_by(ivle.database.ProjectSet.id):
 
724
            settmpl = self._loader.load(set_fragment)
675
725
            setCtx = Context()
676
726
            setCtx['req'] = req
677
727
            setCtx['projectset'] = projectset
678
728
            setCtx['projects'] = []
679
729
            setCtx['GroupsView'] = GroupsView
680
 
            setCtx['ProjectSetRESTView'] = ProjectSetRESTView
 
730
            setCtx['ProjectSetEdit'] = ProjectSetEdit
 
731
            setCtx['ProjectNew'] = ProjectNew
681
732
 
682
 
            for project in projectset.projects:
683
 
                projecttmpl = loader.load(project_fragment)
 
733
            for project in \
 
734
                projectset.projects.order_by(ivle.database.Project.deadline):
 
735
                projecttmpl = self._loader.load(project_fragment)
684
736
                projectCtx = Context()
685
737
                projectCtx['req'] = req
686
738
                projectCtx['project'] = project
 
739
                projectCtx['ProjectEdit'] = ProjectEdit
 
740
                projectCtx['ProjectDelete'] = ProjectDelete
687
741
 
688
742
                setCtx['projects'].append(
689
743
                        projecttmpl.generate(projectCtx))
697
751
    permission = "view_project_submissions"
698
752
    tab = 'subjects'
699
753
 
700
 
    def build_subversion_url(self, svnroot, submission):
701
 
        princ = submission.assessed.principal
702
 
 
703
 
        if isinstance(princ, User):
704
 
            path = 'users/%s' % princ.login
705
 
        else:
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,
710
 
                    princ.name
711
 
                    )
712
 
        return urlparse.urljoin(
713
 
                    svnroot,
714
 
                    os.path.join(path, submission.path[1:] if
715
 
                                       submission.path.startswith(os.sep) else
716
 
                                       submission.path))
717
 
 
718
754
    def populate(self, req, ctx):
719
755
        self.plugin_styles[Plugin] = ["project.css"]
720
756
 
721
757
        ctx['req'] = req
 
758
        ctx['permissions'] = self.context.get_permissions(req.user,req.config)
722
759
        ctx['GroupsView'] = GroupsView
723
760
        ctx['EnrolView'] = EnrolView
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']
727
 
        ctx['project'] = self.context
728
 
        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
 
768
 
 
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"
 
774
 
 
775
    def populate(self, req, ctx):
 
776
        ctx['req'] = req
 
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
 
785
 
 
786
class ProjectUniquenessValidator(formencode.FancyValidator):
 
787
    """A FormEncode validator that checks that a project short_name is unique
 
788
    in a given offering.
 
789
 
 
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.
 
792
    """
 
793
    def _to_python(self, value, state):
 
794
        if (state.store.find(
 
795
            Project,
 
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."
 
802
                , value, state)
 
803
        return value
 
804
 
 
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)
 
813
 
 
814
class ProjectEdit(BaseFormView):
 
815
    """A form to edit a project."""
 
816
    template = 'templates/project-edit.html'
 
817
    tab = 'subjects'
 
818
    permission = 'edit'
 
819
 
 
820
    @property
 
821
    def validator(self):
 
822
        return ProjectSchema()
 
823
 
 
824
    def populate(self, req, ctx):
 
825
        super(ProjectEdit, self).populate(req, ctx)
 
826
        ctx['projectset'] = self.context.project_set
 
827
 
 
828
    def populate_state(self, state):
 
829
        state.offering = self.context.project_set.offering
 
830
        state.existing_project = self.context
 
831
 
 
832
    def get_default_data(self, req):
 
833
        return {
 
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,
 
839
            }
 
840
 
 
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']
 
847
        return self.context
 
848
 
 
849
class ProjectNew(BaseFormView):
 
850
    """A form to create a new project."""
 
851
    template = 'templates/project-new.html'
 
852
    tab = 'subjects'
 
853
    permission = 'edit'
 
854
 
 
855
    @property
 
856
    def validator(self):
 
857
        return ProjectSchema()
 
858
 
 
859
    def populate(self, req, ctx):
 
860
        super(ProjectNew, self).populate(req, ctx)
 
861
        ctx['projectset'] = self.context
 
862
 
 
863
    def populate_state(self, state):
 
864
        state.offering = self.context.offering
 
865
        state.existing_project = None
 
866
 
 
867
    def get_default_data(self, req):
 
868
        return {}
 
869
 
 
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)
 
879
        return new_project
 
880
 
 
881
class ProjectDelete(XHTMLView):
 
882
    """A form to delete a project."""
 
883
    template = 'templates/project-delete.html'
 
884
    tab = 'subjects'
 
885
    permission = 'edit'
 
886
 
 
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'
 
894
        else:
 
895
            # Can't delete
 
896
            self.template = 'templates/project-undeletable.html'
 
897
 
 
898
        # If get and can delete, display a delete confirmation page
 
899
 
 
900
        # Variables for the template
 
901
        ctx['req'] = req
 
902
        ctx['project'] = self.context
 
903
        ctx['OfferingProjectsView'] = OfferingProjectsView
 
904
 
 
905
class ProjectSetSchema(formencode.Schema):
 
906
    group_size = formencode.validators.Int(if_missing=None, not_empty=False)
 
907
 
 
908
class ProjectSetEdit(BaseFormView):
 
909
    """A form to edit a project set."""
 
910
    template = 'templates/projectset-edit.html'
 
911
    tab = 'subjects'
 
912
    permission = 'edit'
 
913
 
 
914
    @property
 
915
    def validator(self):
 
916
        return ProjectSetSchema()
 
917
 
 
918
    def populate(self, req, ctx):
 
919
        super(ProjectSetEdit, self).populate(req, ctx)
 
920
 
 
921
    def get_default_data(self, req):
 
922
        return {
 
923
            'group_size': self.context.max_students_per_group,
 
924
            }
 
925
 
 
926
    def save_object(self, req, data):
 
927
        self.context.max_students_per_group = data['group_size']
 
928
        return self.context
 
929
 
 
930
class ProjectSetNew(BaseFormView):
 
931
    """A form to create a new project set."""
 
932
    template = 'templates/projectset-new.html'
 
933
    tab = 'subjects'
 
934
    permission = 'edit'
 
935
    breadcrumb_text = "Projects"
 
936
 
 
937
    @property
 
938
    def validator(self):
 
939
        return ProjectSetSchema()
 
940
 
 
941
    def populate(self, req, ctx):
 
942
        super(ProjectSetNew, self).populate(req, ctx)
 
943
 
 
944
    def get_default_data(self, req):
 
945
        return {}
 
946
 
 
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)
 
952
        return new_set
729
953
 
730
954
class Plugin(ViewPlugin, MediaPlugin):
731
955
    forward_routes = (root_to_subject, root_to_semester, subject_to_offering,
740
964
             (ApplicationRoot, ('subjects', '+new'), SubjectNew),
741
965
             (ApplicationRoot, ('subjects', '+new-offering'), OfferingNew),
742
966
             (ApplicationRoot, ('+semesters', '+new'), SemesterNew),
 
967
             (Subject, '+index', SubjectView),
743
968
             (Subject, '+edit', SubjectEdit),
 
969
             (Subject, '+new-offering', SubjectOfferingNew),
744
970
             (Semester, '+edit', SemesterEdit),
745
971
             (Offering, '+index', OfferingView),
746
972
             (Offering, '+edit', OfferingEdit),
750
976
             (Enrolment, '+edit', EnrolmentEdit),
751
977
             (Enrolment, '+delete', EnrolmentDelete),
752
978
             (Offering, ('+projects', '+index'), OfferingProjectsView),
 
979
             (Offering, ('+projects', '+new-set'), ProjectSetNew),
 
980
             (ProjectSet, '+edit', ProjectSetEdit),
 
981
             (ProjectSet, '+new', ProjectNew),
753
982
             (Project, '+index', ProjectView),
754
 
 
755
 
             (Offering, ('+projectsets', '+new'), OfferingRESTView, 'api'),
756
 
             (ProjectSet, ('+projects', '+new'), ProjectSetRESTView, 'api'),
 
983
             (Project, '+edit', ProjectEdit),
 
984
             (Project, '+delete', ProjectDelete),
 
985
             (Project, ('+export', 'project-export.sh'),
 
986
                ProjectBashExportView),
757
987
             ]
758
988
 
759
989
    breadcrumbs = {Subject: SubjectBreadcrumb,