~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 05:06:15 UTC
  • Revision ID: grantw@unimelb.edu.au-20100728050615-uwbxn9frla3pdw8m
Encode content_type when downloading files. cjson made us write bad code.

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, URLNameValidator
 
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
74
75
        ctx['user'] = req.user
75
76
        ctx['semesters'] = []
76
77
 
77
 
        for semester in req.store.find(Semester).order_by(Desc(Semester.year),
78
 
                                                     Desc(Semester.semester)):
 
78
        for semester in req.store.find(Semester).order_by(
 
79
            Desc(Semester.year), Desc(Semester.display_name)):
79
80
            if req.user.admin:
80
81
                # For admins, show all subjects in the system
81
82
                offerings = list(semester.offerings.find())
103
104
 
104
105
        ctx['subjects'] = req.store.find(Subject).order_by(Subject.name)
105
106
        ctx['semesters'] = req.store.find(Semester).order_by(
106
 
            Semester.year, Semester.semester)
107
 
 
108
 
 
109
 
class SubjectShortNameUniquenessValidator(formencode.FancyValidator):
110
 
    """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.
111
112
 
112
113
    The subject referenced by state.existing_subject is permitted
113
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.
114
118
    """
115
 
    def __init__(self, matching=None):
116
 
        self.matching = matching
 
119
 
 
120
    def __init__(self, attribute, display):
 
121
        self.attribute = attribute
 
122
        self.display = display
117
123
 
118
124
    def _to_python(self, value, state):
119
 
        if (state.store.find(
120
 
                Subject, short_name=value).one() not in
 
125
        if (state.store.find(Subject, **{self.attribute: value}).one() not in
121
126
                (None, state.existing_subject)):
122
127
            raise formencode.Invalid(
123
 
                'Short name already taken', value, state)
 
128
                '%s already taken' % self.display, value, state)
124
129
        return value
125
130
 
126
131
 
127
132
class SubjectSchema(formencode.Schema):
128
133
    short_name = formencode.All(
129
 
        SubjectShortNameUniquenessValidator(),
 
134
        SubjectUniquenessValidator('short_name', 'URL name'),
130
135
        URLNameValidator(not_empty=True))
131
136
    name = formencode.validators.UnicodeString(not_empty=True)
132
 
    code = formencode.validators.UnicodeString(not_empty=True)
 
137
    code = formencode.All(
 
138
        SubjectUniquenessValidator('code', 'Subject code'),
 
139
        formencode.validators.UnicodeString(not_empty=True))
133
140
 
134
141
 
135
142
class SubjectFormView(BaseFormView):
193
200
    """
194
201
    def _to_python(self, value, state):
195
202
        if (state.store.find(
196
 
                Semester, year=value['year'], semester=value['semester']
 
203
                Semester, year=value['year'], url_name=value['url_name']
197
204
                ).one() not in (None, state.existing_semester)):
198
205
            raise formencode.Invalid(
199
206
                'Semester already exists', value, state)
202
209
 
203
210
class SemesterSchema(formencode.Schema):
204
211
    year = URLNameValidator()
205
 
    semester = URLNameValidator()
 
212
    code = formencode.validators.UnicodeString()
 
213
    url_name = URLNameValidator()
 
214
    display_name = formencode.validators.UnicodeString()
206
215
    state = formencode.All(
207
216
        formencode.validators.OneOf(["past", "current", "future"]),
208
217
        formencode.validators.UnicodeString())
237
246
    def save_object(self, req, data):
238
247
        new_semester = Semester()
239
248
        new_semester.year = data['year']
240
 
        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']
241
252
        new_semester.state = data['state']
242
253
 
243
254
        req.store.add(new_semester)
254
265
    def get_default_data(self, req):
255
266
        return {
256
267
            'year': self.context.year,
257
 
            'semester': self.context.semester,
 
268
            'code': self.context.code,
 
269
            'url_name': self.context.url_name,
 
270
            'display_name': self.context.display_name,
258
271
            'state': self.context.state,
259
272
            }
260
273
 
261
274
    def save_object(self, req, data):
262
275
        self.context.year = data['year']
263
 
        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']
264
279
        self.context.state = data['state']
265
280
 
266
281
        return self.context
302
317
        ctx['OfferingCloneWorksheets'] = OfferingCloneWorksheets
303
318
        ctx['GroupsView'] = GroupsView
304
319
        ctx['EnrolmentsView'] = EnrolmentsView
 
320
        ctx['Project'] = ivle.database.Project
305
321
 
306
322
        # As we go, calculate the total score for this subject
307
323
        # (Assessable worksheets only, mandatory problems only)
308
324
 
309
325
        ctx['worksheets'], problems_total, problems_done = (
310
326
            ivle.worksheet.utils.create_list_of_fake_worksheets_and_stats(
311
 
                req.store, req.user, self.context))
 
327
                req.config, req.store, req.user, self.context,
 
328
                as_of=self.context.worksheet_cutoff))
312
329
 
313
330
        ctx['exercises_total'] = problems_total
314
331
        ctx['exercises_done'] = problems_done
355
372
            year = semester = None
356
373
 
357
374
        semester = state.store.find(
358
 
            Semester, year=year, semester=semester).one()
 
375
            Semester, year=year, url_name=semester).one()
359
376
        if semester:
360
377
            return semester
361
378
        else:
385
402
    description = formencode.validators.UnicodeString(
386
403
        if_missing=None, not_empty=False)
387
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)
388
408
 
389
409
 
390
410
class OfferingAdminSchema(OfferingSchema):
412
432
        super(OfferingEdit, self).populate(req, ctx)
413
433
        ctx['subjects'] = req.store.find(Subject).order_by(Subject.name)
414
434
        ctx['semesters'] = req.store.find(Semester).order_by(
415
 
            Semester.year, Semester.semester)
 
435
            Semester.year, Semester.display_name)
416
436
        ctx['force_subject'] = None
417
437
 
418
438
    def populate_state(self, state):
422
442
        return {
423
443
            'subject': self.context.subject.short_name,
424
444
            'semester': self.context.semester.year + '/' +
425
 
                        self.context.semester.semester,
 
445
                        self.context.semester.url_name,
426
446
            'url': self.context.url,
427
447
            'description': self.context.description,
 
448
            'worksheet_cutoff': self.context.worksheet_cutoff,
 
449
            'show_worksheet_marks': self.context.show_worksheet_marks,
428
450
            }
429
451
 
430
452
    def save_object(self, req, data):
433
455
            self.context.semester = data['semester']
434
456
        self.context.description = data['description']
435
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']
436
460
        return self.context
437
461
 
438
462
 
452
476
        super(OfferingNew, self).populate(req, ctx)
453
477
        ctx['subjects'] = req.store.find(Subject).order_by(Subject.name)
454
478
        ctx['semesters'] = req.store.find(Semester).order_by(
455
 
            Semester.year, Semester.semester)
 
479
            Semester.year, Semester.display_name)
456
480
        ctx['force_subject'] = None
457
481
 
458
482
    def populate_state(self, state):
467
491
        new_offering.semester = data['semester']
468
492
        new_offering.description = data['description']
469
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']
470
496
 
471
497
        req.store.add(new_offering)
472
498
        return new_offering
502
528
        super(OfferingCloneWorksheets, self).populate(req, ctx)
503
529
        ctx['subjects'] = req.store.find(Subject).order_by(Subject.name)
504
530
        ctx['semesters'] = req.store.find(Semester).order_by(
505
 
            Semester.year, Semester.semester)
 
531
            Semester.year, Semester.display_name)
506
532
 
507
533
    def get_default_data(self, req):
508
534
        return {}
615
641
        ctx['offering'] = self.context
616
642
        ctx['roles_auth'] = self.context.get_permissions(req.user, req.config)
617
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
618
647
 
619
648
 
620
649
class EnrolmentEditSchema(formencode.Schema):
679
708
 
680
709
    def populate(self, req, ctx):
681
710
        self.plugin_styles[Plugin] = ["project.css"]
682
 
        self.plugin_scripts[Plugin] = ["project.js"]
683
711
        ctx['req'] = req
684
712
        ctx['offering'] = self.context
685
713
        ctx['projectsets'] = []
686
 
        ctx['OfferingRESTView'] = OfferingRESTView
687
714
 
688
715
        #Open the projectset Fragment, and render it for inclusion
689
716
        #into the ProjectSets page
690
 
        #XXX: This could be a lot cleaner
691
 
        loader = genshi.template.TemplateLoader(".", auto_reload=True)
692
 
 
693
717
        set_fragment = os.path.join(os.path.dirname(__file__),
694
718
                "templates/projectset_fragment.html")
695
719
        project_fragment = os.path.join(os.path.dirname(__file__),
696
720
                "templates/project_fragment.html")
697
721
 
698
 
        for projectset in self.context.project_sets:
699
 
            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)
700
725
            setCtx = Context()
701
726
            setCtx['req'] = req
702
727
            setCtx['projectset'] = projectset
703
728
            setCtx['projects'] = []
704
729
            setCtx['GroupsView'] = GroupsView
705
 
            setCtx['ProjectSetRESTView'] = ProjectSetRESTView
 
730
            setCtx['ProjectSetEdit'] = ProjectSetEdit
 
731
            setCtx['ProjectNew'] = ProjectNew
706
732
 
707
 
            for project in projectset.projects:
708
 
                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)
709
736
                projectCtx = Context()
710
737
                projectCtx['req'] = req
711
738
                projectCtx['project'] = project
 
739
                projectCtx['ProjectEdit'] = ProjectEdit
 
740
                projectCtx['ProjectDelete'] = ProjectDelete
712
741
 
713
742
                setCtx['projects'].append(
714
743
                        projecttmpl.generate(projectCtx))
722
751
    permission = "view_project_submissions"
723
752
    tab = 'subjects'
724
753
 
725
 
    def build_subversion_url(self, svnroot, submission):
726
 
        princ = submission.assessed.principal
727
 
 
728
 
        if isinstance(princ, User):
729
 
            path = 'users/%s' % princ.login
730
 
        else:
731
 
            path = 'groups/%s_%s_%s_%s' % (
732
 
                    princ.project_set.offering.subject.short_name,
733
 
                    princ.project_set.offering.semester.year,
734
 
                    princ.project_set.offering.semester.semester,
735
 
                    princ.name
736
 
                    )
737
 
        return urlparse.urljoin(
738
 
                    svnroot,
739
 
                    os.path.join(path, submission.path[1:] if
740
 
                                       submission.path.startswith(os.sep) else
741
 
                                       submission.path))
742
 
 
743
754
    def populate(self, req, ctx):
744
755
        self.plugin_styles[Plugin] = ["project.css"]
745
756
 
746
757
        ctx['req'] = req
 
758
        ctx['permissions'] = self.context.get_permissions(req.user,req.config)
747
759
        ctx['GroupsView'] = GroupsView
748
760
        ctx['EnrolView'] = EnrolView
749
 
        ctx['format_datetime_short'] = ivle.date.format_datetime_for_paragraph
750
 
        ctx['build_subversion_url'] = self.build_subversion_url
751
 
        ctx['svn_addr'] = req.config['urls']['svn_addr']
752
 
        ctx['project'] = self.context
753
 
        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
754
953
 
755
954
class Plugin(ViewPlugin, MediaPlugin):
756
955
    forward_routes = (root_to_subject, root_to_semester, subject_to_offering,
777
976
             (Enrolment, '+edit', EnrolmentEdit),
778
977
             (Enrolment, '+delete', EnrolmentDelete),
779
978
             (Offering, ('+projects', '+index'), OfferingProjectsView),
 
979
             (Offering, ('+projects', '+new-set'), ProjectSetNew),
 
980
             (ProjectSet, '+edit', ProjectSetEdit),
 
981
             (ProjectSet, '+new', ProjectNew),
780
982
             (Project, '+index', ProjectView),
781
 
 
782
 
             (Offering, ('+projectsets', '+new'), OfferingRESTView, 'api'),
783
 
             (ProjectSet, ('+projects', '+new'), ProjectSetRESTView, 'api'),
 
983
             (Project, '+edit', ProjectEdit),
 
984
             (Project, '+delete', ProjectDelete),
 
985
             (Project, ('+export', 'project-export.sh'),
 
986
                ProjectBashExportView),
784
987
             ]
785
988
 
786
989
    breadcrumbs = {Subject: SubjectBreadcrumb,