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

« back to all changes in this revision

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

  • Committer: David Coles
  • Date: 2010-02-17 07:19:52 UTC
  • Revision ID: coles.david@gmail.com-20100217071952-ycxxhx2lvdm7fiyh
docs: HTTPS example now uses HTTP for public mode. Isolates public mode cookies from IVLE cookies

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
31
30
 
32
31
from storm.locals import Desc, Store
33
32
import genshi
34
33
from genshi.filters import HTMLFormFiller
35
 
from genshi.template import Context
 
34
from genshi.template import Context, TemplateLoader
36
35
import formencode
37
36
import formencode.validators
38
37
 
39
 
from ivle.webapp.base.forms import (BaseFormView, URLNameValidator,
40
 
                                    DateTimeValidator)
 
38
from ivle.webapp.base.forms import BaseFormView
41
39
from ivle.webapp.base.plugins import ViewPlugin, MediaPlugin
42
40
from ivle.webapp.base.xhtml import XHTMLView
43
 
from ivle.webapp.base.text import TextView
44
41
from ivle.webapp.errors import BadRequest
45
42
from ivle.webapp import ApplicationRoot
46
43
 
49
46
from ivle import util
50
47
import ivle.date
51
48
 
 
49
from ivle.webapp.admin.projectservice import ProjectSetRESTView
 
50
from ivle.webapp.admin.offeringservice import OfferingRESTView
52
51
from ivle.webapp.admin.publishing import (root_to_subject, root_to_semester,
53
52
            subject_to_offering, offering_to_projectset, offering_to_project,
54
 
            offering_to_enrolment, subject_url, semester_url, offering_url,
55
 
            projectset_url, project_url, enrolment_url)
 
53
            subject_url, semester_url, offering_url, projectset_url,
 
54
            project_url)
56
55
from ivle.webapp.admin.breadcrumbs import (SubjectBreadcrumb,
57
 
            OfferingBreadcrumb, UserBreadcrumb, ProjectBreadcrumb,
58
 
            ProjectsBreadcrumb, EnrolmentBreadcrumb)
 
56
            OfferingBreadcrumb, UserBreadcrumb, ProjectBreadcrumb)
59
57
from ivle.webapp.core import Plugin as CorePlugin
60
58
from ivle.webapp.groups import GroupsView
61
59
from ivle.webapp.media import media_url
65
63
    '''The view of the list of subjects.'''
66
64
    template = 'templates/subjects.html'
67
65
    tab = 'subjects'
68
 
    breadcrumb_text = "Subjects"
69
66
 
70
67
    def authorize(self, req):
71
68
        return req.user is not None
75
72
        ctx['user'] = req.user
76
73
        ctx['semesters'] = []
77
74
 
78
 
        for semester in req.store.find(Semester).order_by(
79
 
            Desc(Semester.year), Desc(Semester.display_name)):
 
75
        for semester in req.store.find(Semester).order_by(Desc(Semester.year),
 
76
                                                     Desc(Semester.semester)):
80
77
            if req.user.admin:
81
78
                # For admins, show all subjects in the system
82
79
                offerings = list(semester.offerings.find())
98
95
    def populate(self, req, ctx):
99
96
        ctx['req'] = req
100
97
        ctx['mediapath'] = media_url(req, CorePlugin, 'images/')
101
 
        ctx['SubjectView'] = SubjectView
102
98
        ctx['SubjectEdit'] = SubjectEdit
103
99
        ctx['SemesterEdit'] = SemesterEdit
104
100
 
105
101
        ctx['subjects'] = req.store.find(Subject).order_by(Subject.name)
106
102
        ctx['semesters'] = req.store.find(Semester).order_by(
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.
 
103
            Semester.year, Semester.semester)
 
104
 
 
105
 
 
106
class SubjectShortNameUniquenessValidator(formencode.FancyValidator):
 
107
    """A FormEncode validator that checks that a subject name is unused.
112
108
 
113
109
    The subject referenced by state.existing_subject is permitted
114
110
    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.
118
111
    """
119
 
 
120
 
    def __init__(self, attribute, display):
121
 
        self.attribute = attribute
122
 
        self.display = display
 
112
    def __init__(self, matching=None):
 
113
        self.matching = matching
123
114
 
124
115
    def _to_python(self, value, state):
125
 
        if (state.store.find(Subject, **{self.attribute: value}).one() not in
 
116
        if (state.store.find(
 
117
                Subject, short_name=value).one() not in
126
118
                (None, state.existing_subject)):
127
119
            raise formencode.Invalid(
128
 
                '%s already taken' % self.display, value, state)
 
120
                'Short name already taken', value, state)
129
121
        return value
130
122
 
131
123
 
132
124
class SubjectSchema(formencode.Schema):
133
125
    short_name = formencode.All(
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'),
 
126
        SubjectShortNameUniquenessValidator(),
139
127
        formencode.validators.UnicodeString(not_empty=True))
 
128
    name = formencode.validators.UnicodeString(not_empty=True)
 
129
    code = formencode.validators.UnicodeString(not_empty=True)
140
130
 
141
131
 
142
132
class SubjectFormView(BaseFormView):
153
143
    def validator(self):
154
144
        return SubjectSchema()
155
145
 
 
146
    def get_return_url(self, obj):
 
147
        return '/subjects'
 
148
 
156
149
 
157
150
class SubjectNew(SubjectFormView):
158
151
    """A form to create a subject."""
200
193
    """
201
194
    def _to_python(self, value, state):
202
195
        if (state.store.find(
203
 
                Semester, year=value['year'], url_name=value['url_name']
 
196
                Semester, year=value['year'], semester=value['semester']
204
197
                ).one() not in (None, state.existing_semester)):
205
198
            raise formencode.Invalid(
206
199
                'Semester already exists', value, state)
208
201
 
209
202
 
210
203
class SemesterSchema(formencode.Schema):
211
 
    year = URLNameValidator()
212
 
    code = formencode.validators.UnicodeString()
213
 
    url_name = URLNameValidator()
214
 
    display_name = formencode.validators.UnicodeString()
 
204
    year = formencode.validators.UnicodeString()
 
205
    semester = formencode.validators.UnicodeString()
215
206
    state = formencode.All(
216
207
        formencode.validators.OneOf(["past", "current", "future"]),
217
208
        formencode.validators.UnicodeString())
246
237
    def save_object(self, req, data):
247
238
        new_semester = Semester()
248
239
        new_semester.year = data['year']
249
 
        new_semester.code = data['code']
250
 
        new_semester.url_name = data['url_name']
251
 
        new_semester.display_name = data['display_name']
 
240
        new_semester.semester = data['semester']
252
241
        new_semester.state = data['state']
253
242
 
254
243
        req.store.add(new_semester)
265
254
    def get_default_data(self, req):
266
255
        return {
267
256
            'year': self.context.year,
268
 
            'code': self.context.code,
269
 
            'url_name': self.context.url_name,
270
 
            'display_name': self.context.display_name,
 
257
            'semester': self.context.semester,
271
258
            'state': self.context.state,
272
259
            }
273
260
 
274
261
    def save_object(self, req, data):
275
262
        self.context.year = data['year']
276
 
        self.context.code = data['code']
277
 
        self.context.url_name = data['url_name']
278
 
        self.context.display_name = data['display_name']
 
263
        self.context.semester = data['semester']
279
264
        self.context.state = data['state']
280
265
 
281
266
        return self.context
282
267
 
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
 
 
300
268
 
301
269
class OfferingView(XHTMLView):
302
270
    """The home page of an offering."""
317
285
        ctx['OfferingCloneWorksheets'] = OfferingCloneWorksheets
318
286
        ctx['GroupsView'] = GroupsView
319
287
        ctx['EnrolmentsView'] = EnrolmentsView
320
 
        ctx['Project'] = ivle.database.Project
321
288
 
322
289
        # As we go, calculate the total score for this subject
323
290
        # (Assessable worksheets only, mandatory problems only)
324
291
 
325
292
        ctx['worksheets'], problems_total, problems_done = (
326
293
            ivle.worksheet.utils.create_list_of_fake_worksheets_and_stats(
327
 
                req.config, req.store, req.user, self.context,
328
 
                as_of=self.context.worksheet_cutoff))
 
294
                req.store, req.user, self.context))
329
295
 
330
296
        ctx['exercises_total'] = problems_total
331
297
        ctx['exercises_done'] = problems_done
372
338
            year = semester = None
373
339
 
374
340
        semester = state.store.find(
375
 
            Semester, year=year, url_name=semester).one()
 
341
            Semester, year=year, semester=semester).one()
376
342
        if semester:
377
343
            return semester
378
344
        else:
402
368
    description = formencode.validators.UnicodeString(
403
369
        if_missing=None, not_empty=False)
404
370
    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)
408
371
 
409
372
 
410
373
class OfferingAdminSchema(OfferingSchema):
432
395
        super(OfferingEdit, self).populate(req, ctx)
433
396
        ctx['subjects'] = req.store.find(Subject).order_by(Subject.name)
434
397
        ctx['semesters'] = req.store.find(Semester).order_by(
435
 
            Semester.year, Semester.display_name)
436
 
        ctx['force_subject'] = None
 
398
            Semester.year, Semester.semester)
437
399
 
438
400
    def populate_state(self, state):
439
401
        state.existing_offering = self.context
442
404
        return {
443
405
            'subject': self.context.subject.short_name,
444
406
            'semester': self.context.semester.year + '/' +
445
 
                        self.context.semester.url_name,
 
407
                        self.context.semester.semester,
446
408
            'url': self.context.url,
447
409
            'description': self.context.description,
448
 
            'worksheet_cutoff': self.context.worksheet_cutoff,
449
 
            'show_worksheet_marks': self.context.show_worksheet_marks,
450
410
            }
451
411
 
452
412
    def save_object(self, req, data):
455
415
            self.context.semester = data['semester']
456
416
        self.context.description = data['description']
457
417
        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']
460
418
        return self.context
461
419
 
462
420
 
476
434
        super(OfferingNew, self).populate(req, ctx)
477
435
        ctx['subjects'] = req.store.find(Subject).order_by(Subject.name)
478
436
        ctx['semesters'] = req.store.find(Semester).order_by(
479
 
            Semester.year, Semester.display_name)
480
 
        ctx['force_subject'] = None
 
437
            Semester.year, Semester.semester)
481
438
 
482
439
    def populate_state(self, state):
483
440
        state.existing_offering = None
491
448
        new_offering.semester = data['semester']
492
449
        new_offering.description = data['description']
493
450
        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']
496
451
 
497
452
        req.store.add(new_offering)
498
453
        return new_offering
499
454
 
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
507
455
 
508
456
class OfferingCloneWorksheetsSchema(formencode.Schema):
509
457
    subject = formencode.All(
528
476
        super(OfferingCloneWorksheets, self).populate(req, ctx)
529
477
        ctx['subjects'] = req.store.find(Subject).order_by(Subject.name)
530
478
        ctx['semesters'] = req.store.find(Semester).order_by(
531
 
            Semester.year, Semester.display_name)
 
479
            Semester.year, Semester.semester)
532
480
 
533
481
    def get_default_data(self, req):
534
482
        return {}
599
547
    template = 'templates/enrolments.html'
600
548
    tab = 'subjects'
601
549
    permission = 'edit'
602
 
    breadcrumb_text = 'Enrolments'
603
550
 
604
551
    def populate(self, req, ctx):
605
552
        ctx['req'] = req
606
553
        ctx['offering'] = self.context
607
 
        ctx['mediapath'] = media_url(req, CorePlugin, 'images/')
608
 
        ctx['offering_perms'] = self.context.get_permissions(
609
 
            req.user, req.config)
610
554
        ctx['EnrolView'] = EnrolView
611
 
        ctx['EnrolmentEdit'] = EnrolmentEdit
612
 
        ctx['EnrolmentDelete'] = EnrolmentDelete
613
555
 
614
556
 
615
557
class EnrolView(XHTMLView):
641
583
        ctx['offering'] = self.context
642
584
        ctx['roles_auth'] = self.context.get_permissions(req.user, req.config)
643
585
        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
647
 
 
648
 
 
649
 
class EnrolmentEditSchema(formencode.Schema):
650
 
    role = formencode.All(formencode.validators.OneOf(
651
 
                                ["lecturer", "tutor", "student"]),
652
 
                          RoleEnrolmentValidator(),
653
 
                          formencode.validators.UnicodeString())
654
 
 
655
 
 
656
 
class EnrolmentEdit(BaseFormView):
657
 
    """A form to alter an enrolment's role."""
658
 
    template = 'templates/enrolment-edit.html'
659
 
    tab = 'subjects'
660
 
    permission = 'edit'
661
 
 
662
 
    def populate_state(self, state):
663
 
        state.offering = self.context.offering
664
 
 
665
 
    def get_default_data(self, req):
666
 
        return {'role': self.context.role}
667
 
 
668
 
    @property
669
 
    def validator(self):
670
 
        return EnrolmentEditSchema()
671
 
 
672
 
    def save_object(self, req, data):
673
 
        self.context.role = data['role']
674
 
 
675
 
    def get_return_url(self, obj):
676
 
        return self.req.publisher.generate(
677
 
            self.context.offering, EnrolmentsView)
678
 
 
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)
683
 
 
684
 
 
685
 
class EnrolmentDelete(XHTMLView):
686
 
    """A form to alter an enrolment's role."""
687
 
    template = 'templates/enrolment-delete.html'
688
 
    tab = 'subjects'
689
 
    permission = 'edit'
690
 
 
691
 
    def populate(self, req, ctx):
692
 
        # If POSTing, delete delete delete.
693
 
        if req.method == 'POST':
694
 
            self.context.delete()
695
 
            req.store.commit()
696
 
            req.throw_redirect(req.publisher.generate(
697
 
                self.context.offering, EnrolmentsView))
698
 
 
699
 
        ctx['enrolment'] = self.context
700
 
 
701
586
 
702
587
class OfferingProjectsView(XHTMLView):
703
588
    """View the projects for an offering."""
704
589
    template = 'templates/offering_projects.html'
705
590
    permission = 'edit'
706
591
    tab = 'subjects'
707
 
    breadcrumb_text = 'Projects'
708
592
 
709
593
    def populate(self, req, ctx):
710
594
        self.plugin_styles[Plugin] = ["project.css"]
 
595
        self.plugin_scripts[Plugin] = ["project.js"]
711
596
        ctx['req'] = req
712
597
        ctx['offering'] = self.context
713
598
        ctx['projectsets'] = []
 
599
        ctx['OfferingRESTView'] = OfferingRESTView
714
600
 
715
601
        #Open the projectset Fragment, and render it for inclusion
716
602
        #into the ProjectSets page
 
603
        #XXX: This could be a lot cleaner
 
604
        loader = genshi.template.TemplateLoader(".", auto_reload=True)
 
605
 
717
606
        set_fragment = os.path.join(os.path.dirname(__file__),
718
607
                "templates/projectset_fragment.html")
719
608
        project_fragment = os.path.join(os.path.dirname(__file__),
720
609
                "templates/project_fragment.html")
721
610
 
722
 
        for projectset in \
723
 
            self.context.project_sets.order_by(ivle.database.ProjectSet.id):
724
 
            settmpl = self._loader.load(set_fragment)
 
611
        for projectset in self.context.project_sets:
 
612
            settmpl = loader.load(set_fragment)
725
613
            setCtx = Context()
726
614
            setCtx['req'] = req
727
615
            setCtx['projectset'] = projectset
728
616
            setCtx['projects'] = []
729
617
            setCtx['GroupsView'] = GroupsView
730
 
            setCtx['ProjectSetEdit'] = ProjectSetEdit
731
 
            setCtx['ProjectNew'] = ProjectNew
 
618
            setCtx['ProjectSetRESTView'] = ProjectSetRESTView
732
619
 
733
 
            for project in \
734
 
                projectset.projects.order_by(ivle.database.Project.deadline):
735
 
                projecttmpl = self._loader.load(project_fragment)
 
620
            for project in projectset.projects:
 
621
                projecttmpl = loader.load(project_fragment)
736
622
                projectCtx = Context()
737
623
                projectCtx['req'] = req
738
624
                projectCtx['project'] = project
739
 
                projectCtx['ProjectEdit'] = ProjectEdit
740
 
                projectCtx['ProjectDelete'] = ProjectDelete
741
625
 
742
626
                setCtx['projects'].append(
743
627
                        projecttmpl.generate(projectCtx))
751
635
    permission = "view_project_submissions"
752
636
    tab = 'subjects'
753
637
 
 
638
    def build_subversion_url(self, svnroot, submission):
 
639
        princ = submission.assessed.principal
 
640
 
 
641
        if isinstance(princ, User):
 
642
            path = 'users/%s' % princ.login
 
643
        else:
 
644
            path = 'groups/%s_%s_%s_%s' % (
 
645
                    princ.project_set.offering.subject.short_name,
 
646
                    princ.project_set.offering.semester.year,
 
647
                    princ.project_set.offering.semester.semester,
 
648
                    princ.name
 
649
                    )
 
650
        return urlparse.urljoin(
 
651
                    svnroot,
 
652
                    os.path.join(path, submission.path[1:] if
 
653
                                       submission.path.startswith(os.sep) else
 
654
                                       submission.path))
 
655
 
754
656
    def populate(self, req, ctx):
755
657
        self.plugin_styles[Plugin] = ["project.css"]
756
658
 
757
659
        ctx['req'] = req
758
 
        ctx['permissions'] = self.context.get_permissions(req.user,req.config)
759
660
        ctx['GroupsView'] = GroupsView
760
661
        ctx['EnrolView'] = EnrolView
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
 
662
        ctx['format_datetime_short'] = ivle.date.format_datetime_for_paragraph
 
663
        ctx['build_subversion_url'] = self.build_subversion_url
 
664
        ctx['svn_addr'] = req.config['urls']['svn_addr']
 
665
        ctx['project'] = self.context
 
666
        ctx['user'] = req.user
953
667
 
954
668
class Plugin(ViewPlugin, MediaPlugin):
955
669
    forward_routes = (root_to_subject, root_to_semester, subject_to_offering,
956
 
                      offering_to_project, offering_to_projectset,
957
 
                      offering_to_enrolment)
 
670
                      offering_to_project, offering_to_projectset)
958
671
    reverse_routes = (
959
 
        subject_url, semester_url, offering_url, projectset_url, project_url,
960
 
        enrolment_url)
 
672
        subject_url, semester_url, offering_url, projectset_url, project_url)
961
673
 
962
674
    views = [(ApplicationRoot, ('subjects', '+index'), SubjectsView),
963
675
             (ApplicationRoot, ('subjects', '+manage'), SubjectsManage),
964
676
             (ApplicationRoot, ('subjects', '+new'), SubjectNew),
965
677
             (ApplicationRoot, ('subjects', '+new-offering'), OfferingNew),
966
678
             (ApplicationRoot, ('+semesters', '+new'), SemesterNew),
967
 
             (Subject, '+index', SubjectView),
968
679
             (Subject, '+edit', SubjectEdit),
969
 
             (Subject, '+new-offering', SubjectOfferingNew),
970
680
             (Semester, '+edit', SemesterEdit),
971
681
             (Offering, '+index', OfferingView),
972
682
             (Offering, '+edit', OfferingEdit),
973
683
             (Offering, '+clone-worksheets', OfferingCloneWorksheets),
974
684
             (Offering, ('+enrolments', '+index'), EnrolmentsView),
975
685
             (Offering, ('+enrolments', '+new'), EnrolView),
976
 
             (Enrolment, '+edit', EnrolmentEdit),
977
 
             (Enrolment, '+delete', EnrolmentDelete),
978
686
             (Offering, ('+projects', '+index'), OfferingProjectsView),
979
 
             (Offering, ('+projects', '+new-set'), ProjectSetNew),
980
 
             (ProjectSet, '+edit', ProjectSetEdit),
981
 
             (ProjectSet, '+new', ProjectNew),
982
687
             (Project, '+index', ProjectView),
983
 
             (Project, '+edit', ProjectEdit),
984
 
             (Project, '+delete', ProjectDelete),
985
 
             (Project, ('+export', 'project-export.sh'),
986
 
                ProjectBashExportView),
 
688
 
 
689
             (Offering, ('+projectsets', '+new'), OfferingRESTView, 'api'),
 
690
             (ProjectSet, ('+projects', '+new'), ProjectSetRESTView, 'api'),
987
691
             ]
988
692
 
989
693
    breadcrumbs = {Subject: SubjectBreadcrumb,
990
694
                   Offering: OfferingBreadcrumb,
991
695
                   User: UserBreadcrumb,
992
696
                   Project: ProjectBreadcrumb,
993
 
                   Enrolment: EnrolmentBreadcrumb,
994
697
                   }
995
698
 
996
699
    tabs = [