~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-28 11:23:51 UTC
  • Revision ID: grantw@unimelb.edu.au-20100728112351-ubw2pri5bw2wwprw
Fix HTML5 media handlers function call. It was renamed in the CodeMirror branch.

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
 
44
from ivle.webapp.errors import BadRequest
41
45
from ivle.webapp import ApplicationRoot
42
46
 
43
47
from ivle.database import Subject, Semester, Offering, Enrolment, User,\
45
49
from ivle import util
46
50
import ivle.date
47
51
 
48
 
from ivle.webapp.admin.projectservice import ProjectSetRESTView
49
 
from ivle.webapp.admin.offeringservice import OfferingRESTView
50
52
from ivle.webapp.admin.publishing import (root_to_subject, root_to_semester,
51
53
            subject_to_offering, offering_to_projectset, offering_to_project,
52
 
            subject_url, semester_url, offering_url, projectset_url,
53
 
            project_url)
 
54
            offering_to_enrolment, subject_url, semester_url, offering_url,
 
55
            projectset_url, project_url, enrolment_url)
54
56
from ivle.webapp.admin.breadcrumbs import (SubjectBreadcrumb,
55
 
            OfferingBreadcrumb, UserBreadcrumb, ProjectBreadcrumb)
 
57
            OfferingBreadcrumb, UserBreadcrumb, ProjectBreadcrumb,
 
58
            ProjectsBreadcrumb, EnrolmentBreadcrumb)
56
59
from ivle.webapp.core import Plugin as CorePlugin
57
60
from ivle.webapp.groups import GroupsView
58
61
from ivle.webapp.media import media_url
62
65
    '''The view of the list of subjects.'''
63
66
    template = 'templates/subjects.html'
64
67
    tab = 'subjects'
 
68
    breadcrumb_text = "Subjects"
65
69
 
66
70
    def authorize(self, req):
67
71
        return req.user is not None
70
74
        ctx['req'] = req
71
75
        ctx['user'] = req.user
72
76
        ctx['semesters'] = []
73
 
        ctx['mediapath'] = media_url(req, CorePlugin, 'images/')
74
 
        ctx['SubjectEdit'] = SubjectEdit
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())
84
86
            if len(offerings):
85
87
                ctx['semesters'].append((semester, offerings))
86
88
 
87
 
        # Admins get a separate list of subjects so they can add/edit.
88
 
        if req.user.admin:
89
 
            ctx['subjects'] = req.store.find(Subject).order_by(Subject.name)
90
 
 
91
 
 
92
 
class SubjectShortNameUniquenessValidator(formencode.FancyValidator):
93
 
    """A FormEncode validator that checks that a subject name is unused.
 
89
 
 
90
class SubjectsManage(XHTMLView):
 
91
    '''Subject management view.'''
 
92
    template = 'templates/subjects-manage.html'
 
93
    tab = 'subjects'
 
94
 
 
95
    def authorize(self, req):
 
96
        return req.user is not None and req.user.admin
 
97
 
 
98
    def populate(self, req, ctx):
 
99
        ctx['req'] = req
 
100
        ctx['mediapath'] = media_url(req, CorePlugin, 'images/')
 
101
        ctx['SubjectView'] = SubjectView
 
102
        ctx['SubjectEdit'] = SubjectEdit
 
103
        ctx['SemesterEdit'] = SemesterEdit
 
104
 
 
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)
 
108
 
 
109
 
 
110
class SubjectUniquenessValidator(formencode.FancyValidator):
 
111
    """A FormEncode validator that checks that a subject attribute is unique.
94
112
 
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.
 
115
 
 
116
    :param attribute: the name of the attribute to check.
 
117
    :param display: a string to identify the field in case of error.
97
118
    """
98
 
    def __init__(self, matching=None):
99
 
        self.matching = matching
 
119
 
 
120
    def __init__(self, attribute, display):
 
121
        self.attribute = attribute
 
122
        self.display = display
100
123
 
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)
107
129
        return value
108
130
 
109
131
 
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)
116
140
 
117
141
 
118
142
class SubjectFormView(BaseFormView):
129
153
    def validator(self):
130
154
        return SubjectSchema()
131
155
 
132
 
    def get_return_url(self, obj):
133
 
        return '/subjects'
134
 
 
135
156
 
136
157
class SubjectNew(SubjectFormView):
137
158
    """A form to create a subject."""
179
200
    """
180
201
    def _to_python(self, value, state):
181
202
        if (state.store.find(
182
 
                Semester, year=value['year'], semester=value['semester']
 
203
                Semester, year=value['year'], url_name=value['url_name']
183
204
                ).one() not in (None, state.existing_semester)):
184
205
            raise formencode.Invalid(
185
206
                'Semester already exists', value, state)
187
208
 
188
209
 
189
210
class SemesterSchema(formencode.Schema):
190
 
    year = formencode.validators.UnicodeString()
191
 
    semester = formencode.validators.UnicodeString()
 
211
    year = URLNameValidator()
 
212
    code = formencode.validators.UnicodeString()
 
213
    url_name = URLNameValidator()
 
214
    display_name = formencode.validators.UnicodeString()
192
215
    state = formencode.All(
193
216
        formencode.validators.OneOf(["past", "current", "future"]),
194
217
        formencode.validators.UnicodeString())
223
246
    def save_object(self, req, data):
224
247
        new_semester = Semester()
225
248
        new_semester.year = data['year']
226
 
        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']
227
252
        new_semester.state = data['state']
228
253
 
229
254
        req.store.add(new_semester)
240
265
    def get_default_data(self, req):
241
266
        return {
242
267
            'year': self.context.year,
243
 
            'semester': self.context.semester,
 
268
            'code': self.context.code,
 
269
            'url_name': self.context.url_name,
 
270
            'display_name': self.context.display_name,
244
271
            'state': self.context.state,
245
272
            }
246
273
 
247
274
    def save_object(self, req, data):
248
275
        self.context.year = data['year']
249
 
        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']
250
279
        self.context.state = data['state']
251
280
 
252
281
        return self.context
253
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
 
254
300
 
255
301
class OfferingView(XHTMLView):
256
302
    """The home page of an offering."""
268
314
        ctx['format_datetime'] = ivle.date.make_date_nice
269
315
        ctx['format_datetime_short'] = ivle.date.format_datetime_for_paragraph
270
316
        ctx['OfferingEdit'] = OfferingEdit
 
317
        ctx['OfferingCloneWorksheets'] = OfferingCloneWorksheets
271
318
        ctx['GroupsView'] = GroupsView
 
319
        ctx['EnrolmentsView'] = EnrolmentsView
 
320
        ctx['Project'] = ivle.database.Project
272
321
 
273
322
        # As we go, calculate the total score for this subject
274
323
        # (Assessable worksheets only, mandatory problems only)
275
324
 
276
325
        ctx['worksheets'], problems_total, problems_done = (
277
326
            ivle.worksheet.utils.create_list_of_fake_worksheets_and_stats(
278
 
                req.store, req.user, self.context))
 
327
                req.config, req.store, req.user, self.context,
 
328
                as_of=self.context.worksheet_cutoff))
279
329
 
280
330
        ctx['exercises_total'] = problems_total
281
331
        ctx['exercises_done'] = problems_done
322
372
            year = semester = None
323
373
 
324
374
        semester = state.store.find(
325
 
            Semester, year=year, semester=semester).one()
 
375
            Semester, year=year, url_name=semester).one()
326
376
        if semester:
327
377
            return semester
328
378
        else:
352
402
    description = formencode.validators.UnicodeString(
353
403
        if_missing=None, not_empty=False)
354
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)
355
408
 
356
409
 
357
410
class OfferingAdminSchema(OfferingSchema):
377
430
 
378
431
    def populate(self, req, ctx):
379
432
        super(OfferingEdit, self).populate(req, ctx)
380
 
        ctx['subjects'] = req.store.find(Subject)
381
 
        ctx['semesters'] = req.store.find(Semester)
 
433
        ctx['subjects'] = req.store.find(Subject).order_by(Subject.name)
 
434
        ctx['semesters'] = req.store.find(Semester).order_by(
 
435
            Semester.year, Semester.display_name)
 
436
        ctx['force_subject'] = None
382
437
 
383
438
    def populate_state(self, state):
384
439
        state.existing_offering = self.context
387
442
        return {
388
443
            'subject': self.context.subject.short_name,
389
444
            'semester': self.context.semester.year + '/' +
390
 
                        self.context.semester.semester,
 
445
                        self.context.semester.url_name,
391
446
            'url': self.context.url,
392
447
            'description': self.context.description,
 
448
            'worksheet_cutoff': self.context.worksheet_cutoff,
 
449
            'show_worksheet_marks': self.context.show_worksheet_marks,
393
450
            }
394
451
 
395
452
    def save_object(self, req, data):
398
455
            self.context.semester = data['semester']
399
456
        self.context.description = data['description']
400
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']
401
460
        return self.context
402
461
 
403
462
 
415
474
 
416
475
    def populate(self, req, ctx):
417
476
        super(OfferingNew, self).populate(req, ctx)
418
 
        ctx['subjects'] = req.store.find(Subject)
419
 
        ctx['semesters'] = req.store.find(Semester)
 
477
        ctx['subjects'] = req.store.find(Subject).order_by(Subject.name)
 
478
        ctx['semesters'] = req.store.find(Semester).order_by(
 
479
            Semester.year, Semester.display_name)
 
480
        ctx['force_subject'] = None
420
481
 
421
482
    def populate_state(self, state):
422
483
        state.existing_offering = None
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']
433
496
 
434
497
        req.store.add(new_offering)
435
498
        return new_offering
436
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
 
507
 
 
508
class OfferingCloneWorksheetsSchema(formencode.Schema):
 
509
    subject = formencode.All(
 
510
        SubjectValidator(), formencode.validators.UnicodeString())
 
511
    semester = formencode.All(
 
512
        SemesterValidator(), formencode.validators.UnicodeString())
 
513
 
 
514
 
 
515
class OfferingCloneWorksheets(BaseFormView):
 
516
    """A form to clone worksheets from one offering to another."""
 
517
    template = 'templates/offering-clone-worksheets.html'
 
518
    tab = 'subjects'
 
519
 
 
520
    def authorize(self, req):
 
521
        return req.user is not None and req.user.admin
 
522
 
 
523
    @property
 
524
    def validator(self):
 
525
        return OfferingCloneWorksheetsSchema()
 
526
 
 
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)
 
532
 
 
533
    def get_default_data(self, req):
 
534
        return {}
 
535
 
 
536
    def save_object(self, req, data):
 
537
        if self.context.worksheets.count() > 0:
 
538
            raise BadRequest(
 
539
                "Cannot clone to target with existing worksheets.")
 
540
        offering = req.store.find(
 
541
            Offering, subject=data['subject'], semester=data['semester']).one()
 
542
        if offering is None:
 
543
            raise BadRequest("No such offering.")
 
544
        if offering.worksheets.count() == 0:
 
545
            raise BadRequest("Source offering has no worksheets.")
 
546
 
 
547
        self.context.clone_worksheets(offering)
 
548
        return self.context
 
549
 
437
550
 
438
551
class UserValidator(formencode.FancyValidator):
439
552
    """A FormEncode validator that turns a username into a user.
486
599
    template = 'templates/enrolments.html'
487
600
    tab = 'subjects'
488
601
    permission = 'edit'
 
602
    breadcrumb_text = 'Enrolments'
489
603
 
490
604
    def populate(self, req, ctx):
 
605
        ctx['req'] = req
491
606
        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
        ctx['EnrolView'] = EnrolView
 
611
        ctx['EnrolmentEdit'] = EnrolmentEdit
 
612
        ctx['EnrolmentDelete'] = EnrolmentDelete
 
613
 
492
614
 
493
615
class EnrolView(XHTMLView):
494
616
    """A form to enrol a user in an offering."""
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
 
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
 
522
701
 
523
702
class OfferingProjectsView(XHTMLView):
524
703
    """View the projects for an offering."""
525
704
    template = 'templates/offering_projects.html'
526
705
    permission = 'edit'
527
706
    tab = 'subjects'
 
707
    breadcrumb_text = 'Projects'
528
708
 
529
709
    def populate(self, req, ctx):
530
710
        self.plugin_styles[Plugin] = ["project.css"]
531
 
        self.plugin_scripts[Plugin] = ["project.js"]
532
711
        ctx['req'] = req
533
712
        ctx['offering'] = self.context
534
713
        ctx['projectsets'] = []
535
 
        ctx['OfferingRESTView'] = OfferingRESTView
536
714
 
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)
541
 
 
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")
546
721
 
547
 
        for projectset in self.context.project_sets:
548
 
            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)
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
555
732
 
556
 
            for project in projectset.projects:
557
 
                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)
558
736
                projectCtx = Context()
559
737
                projectCtx['req'] = req
560
738
                projectCtx['project'] = project
 
739
                projectCtx['ProjectEdit'] = ProjectEdit
 
740
                projectCtx['ProjectDelete'] = ProjectDelete
561
741
 
562
742
                setCtx['projects'].append(
563
743
                        projecttmpl.generate(projectCtx))
571
751
    permission = "view_project_submissions"
572
752
    tab = 'subjects'
573
753
 
574
 
    def build_subversion_url(self, svnroot, submission):
575
 
        princ = submission.assessed.principal
576
 
 
577
 
        if isinstance(princ, User):
578
 
            path = 'users/%s' % princ.login
579
 
        else:
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,
584
 
                    princ.name
585
 
                    )
586
 
        return urlparse.urljoin(
587
 
                    svnroot,
588
 
                    os.path.join(path, submission.path[1:] if
589
 
                                       submission.path.startswith(os.sep) else
590
 
                                       submission.path))
591
 
 
592
754
    def populate(self, req, ctx):
593
755
        self.plugin_styles[Plugin] = ["project.css"]
594
756
 
595
757
        ctx['req'] = req
 
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
 
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
603
953
 
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,
 
960
        enrolment_url)
609
961
 
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),
622
 
 
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),
625
987
             ]
626
988
 
627
989
    breadcrumbs = {Subject: SubjectBreadcrumb,
628
990
                   Offering: OfferingBreadcrumb,
629
991
                   User: UserBreadcrumb,
630
992
                   Project: ProjectBreadcrumb,
 
993
                   Enrolment: EnrolmentBreadcrumb,
631
994
                   }
632
995
 
633
996
    tabs = [