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

« back to all changes in this revision

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

  • Committer: Matt Giuca
  • Date: 2010-07-21 04:21:50 UTC
  • Revision ID: matt.giuca@gmail.com-20100721042150-qovg2sth81sgbdq7
Project page: Replaced the confusing title 'Assigned submitters' with 'Expected submitters', and the heading 'assigned' with 'name'.

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
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
 
125
128
class SubjectSchema(formencode.Schema):
126
129
    short_name = formencode.All(
127
130
        SubjectShortNameUniquenessValidator(),
128
 
        formencode.validators.UnicodeString(not_empty=True))
 
131
        URLNameValidator(not_empty=True))
129
132
    name = formencode.validators.UnicodeString(not_empty=True)
130
133
    code = formencode.validators.UnicodeString(not_empty=True)
131
134
 
144
147
    def validator(self):
145
148
        return SubjectSchema()
146
149
 
147
 
    def get_return_url(self, obj):
148
 
        return '/subjects'
149
 
 
150
150
 
151
151
class SubjectNew(SubjectFormView):
152
152
    """A form to create a subject."""
202
202
 
203
203
 
204
204
class SemesterSchema(formencode.Schema):
205
 
    year = formencode.validators.UnicodeString()
206
 
    semester = formencode.validators.UnicodeString()
 
205
    year = URLNameValidator()
 
206
    semester = URLNameValidator()
207
207
    state = formencode.All(
208
208
        formencode.validators.OneOf(["past", "current", "future"]),
209
209
        formencode.validators.UnicodeString())
266
266
 
267
267
        return self.context
268
268
 
 
269
class SubjectView(XHTMLView):
 
270
    '''The view of the list of offerings in a given subject.'''
 
271
    template = 'templates/subject.html'
 
272
    tab = 'subjects'
 
273
 
 
274
    def authorize(self, req):
 
275
        return req.user is not None
 
276
 
 
277
    def populate(self, req, ctx):
 
278
        ctx['context'] = self.context
 
279
        ctx['req'] = req
 
280
        ctx['user'] = req.user
 
281
        ctx['offerings'] = list(self.context.offerings)
 
282
        ctx['permissions'] = self.context.get_permissions(req.user,req.config)
 
283
        ctx['SubjectEdit'] = SubjectEdit
 
284
        ctx['SubjectOfferingNew'] = SubjectOfferingNew
 
285
 
269
286
 
270
287
class OfferingView(XHTMLView):
271
288
    """The home page of an offering."""
286
303
        ctx['OfferingCloneWorksheets'] = OfferingCloneWorksheets
287
304
        ctx['GroupsView'] = GroupsView
288
305
        ctx['EnrolmentsView'] = EnrolmentsView
 
306
        ctx['Project'] = ivle.database.Project
289
307
 
290
308
        # As we go, calculate the total score for this subject
291
309
        # (Assessable worksheets only, mandatory problems only)
292
310
 
293
311
        ctx['worksheets'], problems_total, problems_done = (
294
312
            ivle.worksheet.utils.create_list_of_fake_worksheets_and_stats(
295
 
                req.store, req.user, self.context))
 
313
                req.config, req.store, req.user, self.context,
 
314
                as_of=self.context.worksheet_cutoff))
296
315
 
297
316
        ctx['exercises_total'] = problems_total
298
317
        ctx['exercises_done'] = problems_done
369
388
    description = formencode.validators.UnicodeString(
370
389
        if_missing=None, not_empty=False)
371
390
    url = formencode.validators.URL(if_missing=None, not_empty=False)
 
391
    worksheet_cutoff = DateTimeValidator(if_missing=None, not_empty=False)
 
392
    show_worksheet_marks = formencode.validators.StringBoolean(
 
393
        if_missing=False)
372
394
 
373
395
 
374
396
class OfferingAdminSchema(OfferingSchema):
397
419
        ctx['subjects'] = req.store.find(Subject).order_by(Subject.name)
398
420
        ctx['semesters'] = req.store.find(Semester).order_by(
399
421
            Semester.year, Semester.semester)
 
422
        ctx['force_subject'] = None
400
423
 
401
424
    def populate_state(self, state):
402
425
        state.existing_offering = self.context
408
431
                        self.context.semester.semester,
409
432
            'url': self.context.url,
410
433
            'description': self.context.description,
 
434
            'worksheet_cutoff': self.context.worksheet_cutoff,
 
435
            'show_worksheet_marks': self.context.show_worksheet_marks,
411
436
            }
412
437
 
413
438
    def save_object(self, req, data):
416
441
            self.context.semester = data['semester']
417
442
        self.context.description = data['description']
418
443
        self.context.url = unicode(data['url']) if data['url'] else None
 
444
        self.context.worksheet_cutoff = data['worksheet_cutoff']
 
445
        self.context.show_worksheet_marks = data['show_worksheet_marks']
419
446
        return self.context
420
447
 
421
448
 
436
463
        ctx['subjects'] = req.store.find(Subject).order_by(Subject.name)
437
464
        ctx['semesters'] = req.store.find(Semester).order_by(
438
465
            Semester.year, Semester.semester)
 
466
        ctx['force_subject'] = None
439
467
 
440
468
    def populate_state(self, state):
441
469
        state.existing_offering = None
449
477
        new_offering.semester = data['semester']
450
478
        new_offering.description = data['description']
451
479
        new_offering.url = unicode(data['url']) if data['url'] else None
 
480
        new_offering.worksheet_cutoff = data['worksheet_cutoff']
 
481
        new_offering.show_worksheet_marks = data['show_worksheet_marks']
452
482
 
453
483
        req.store.add(new_offering)
454
484
        return new_offering
455
485
 
 
486
class SubjectOfferingNew(OfferingNew):
 
487
    """A form to create an offering for a given subject."""
 
488
    # Identical to OfferingNew, except it forces the subject to be the subject
 
489
    # in context
 
490
    def populate(self, req, ctx):
 
491
        super(SubjectOfferingNew, self).populate(req, ctx)
 
492
        ctx['force_subject'] = self.context
456
493
 
457
494
class OfferingCloneWorksheetsSchema(formencode.Schema):
458
495
    subject = formencode.All(
590
627
        ctx['offering'] = self.context
591
628
        ctx['roles_auth'] = self.context.get_permissions(req.user, req.config)
592
629
        ctx['errors'] = errors
 
630
        # If all of the fields validated, set the global form error.
 
631
        if isinstance(errors, basestring):
 
632
            ctx['error_value'] = errors
593
633
 
594
634
 
595
635
class EnrolmentEditSchema(formencode.Schema):
654
694
 
655
695
    def populate(self, req, ctx):
656
696
        self.plugin_styles[Plugin] = ["project.css"]
657
 
        self.plugin_scripts[Plugin] = ["project.js"]
658
697
        ctx['req'] = req
659
698
        ctx['offering'] = self.context
660
699
        ctx['projectsets'] = []
661
 
        ctx['OfferingRESTView'] = OfferingRESTView
662
700
 
663
701
        #Open the projectset Fragment, and render it for inclusion
664
702
        #into the ProjectSets page
665
 
        #XXX: This could be a lot cleaner
666
 
        loader = genshi.template.TemplateLoader(".", auto_reload=True)
667
 
 
668
703
        set_fragment = os.path.join(os.path.dirname(__file__),
669
704
                "templates/projectset_fragment.html")
670
705
        project_fragment = os.path.join(os.path.dirname(__file__),
671
706
                "templates/project_fragment.html")
672
707
 
673
 
        for projectset in self.context.project_sets:
674
 
            settmpl = loader.load(set_fragment)
 
708
        for projectset in \
 
709
            self.context.project_sets.order_by(ivle.database.ProjectSet.id):
 
710
            settmpl = self._loader.load(set_fragment)
675
711
            setCtx = Context()
676
712
            setCtx['req'] = req
677
713
            setCtx['projectset'] = projectset
678
714
            setCtx['projects'] = []
679
715
            setCtx['GroupsView'] = GroupsView
680
 
            setCtx['ProjectSetRESTView'] = ProjectSetRESTView
 
716
            setCtx['ProjectSetEdit'] = ProjectSetEdit
 
717
            setCtx['ProjectNew'] = ProjectNew
681
718
 
682
 
            for project in projectset.projects:
683
 
                projecttmpl = loader.load(project_fragment)
 
719
            for project in \
 
720
                projectset.projects.order_by(ivle.database.Project.deadline):
 
721
                projecttmpl = self._loader.load(project_fragment)
684
722
                projectCtx = Context()
685
723
                projectCtx['req'] = req
686
724
                projectCtx['project'] = project
 
725
                projectCtx['ProjectEdit'] = ProjectEdit
 
726
                projectCtx['ProjectDelete'] = ProjectDelete
687
727
 
688
728
                setCtx['projects'].append(
689
729
                        projecttmpl.generate(projectCtx))
697
737
    permission = "view_project_submissions"
698
738
    tab = 'subjects'
699
739
 
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
740
    def populate(self, req, ctx):
719
741
        self.plugin_styles[Plugin] = ["project.css"]
720
742
 
721
743
        ctx['req'] = req
 
744
        ctx['permissions'] = self.context.get_permissions(req.user,req.config)
722
745
        ctx['GroupsView'] = GroupsView
723
746
        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
 
747
        ctx['format_datetime'] = ivle.date.make_date_nice
 
748
        ctx['format_datetime_short'] = ivle.date.format_datetime_for_paragraph
 
749
        ctx['project'] = self.context
 
750
        ctx['user'] = req.user
 
751
        ctx['ProjectEdit'] = ProjectEdit
 
752
        ctx['ProjectDelete'] = ProjectDelete
 
753
        ctx['ProjectExport'] = ProjectBashExportView
 
754
 
 
755
class ProjectBashExportView(TextView):
 
756
    """Produce a Bash script for exporting projects"""
 
757
    template = "templates/project-export.sh"
 
758
    content_type = "text/x-sh"
 
759
    permission = "view_project_submissions"
 
760
 
 
761
    def populate(self, req, ctx):
 
762
        ctx['req'] = req
 
763
        ctx['permissions'] = self.context.get_permissions(req.user,req.config)
 
764
        ctx['format_datetime'] = ivle.date.make_date_nice
 
765
        ctx['format_datetime_short'] = ivle.date.format_datetime_for_paragraph
 
766
        ctx['project'] = self.context
 
767
        ctx['user'] = req.user
 
768
        ctx['now'] = datetime.datetime.now()
 
769
        ctx['format_datetime'] = ivle.date.make_date_nice
 
770
        ctx['format_datetime_short'] = ivle.date.format_datetime_for_paragraph
 
771
 
 
772
class ProjectUniquenessValidator(formencode.FancyValidator):
 
773
    """A FormEncode validator that checks that a project short_name is unique
 
774
    in a given offering.
 
775
 
 
776
    The project referenced by state.existing_project is permitted to
 
777
    hold that short_name. If any other project holds it, the input is rejected.
 
778
    """
 
779
    def _to_python(self, value, state):
 
780
        if (state.store.find(
 
781
            Project,
 
782
            Project.short_name == unicode(value),
 
783
            Project.project_set_id == ProjectSet.id,
 
784
            ProjectSet.offering == state.offering).one() not in
 
785
            (None, state.existing_project)):
 
786
            raise formencode.Invalid(
 
787
                "A project with that URL name already exists in this offering."
 
788
                , value, state)
 
789
        return value
 
790
 
 
791
class ProjectSchema(formencode.Schema):
 
792
    name = formencode.validators.UnicodeString(not_empty=True)
 
793
    short_name = formencode.All(
 
794
        URLNameValidator(not_empty=True),
 
795
        ProjectUniquenessValidator())
 
796
    deadline = DateTimeValidator(not_empty=True)
 
797
    url = formencode.validators.URL(if_missing=None, not_empty=False)
 
798
    synopsis = formencode.validators.UnicodeString(not_empty=True)
 
799
 
 
800
class ProjectEdit(BaseFormView):
 
801
    """A form to edit a project."""
 
802
    template = 'templates/project-edit.html'
 
803
    tab = 'subjects'
 
804
    permission = 'edit'
 
805
 
 
806
    @property
 
807
    def validator(self):
 
808
        return ProjectSchema()
 
809
 
 
810
    def populate(self, req, ctx):
 
811
        super(ProjectEdit, self).populate(req, ctx)
 
812
        ctx['projectset'] = self.context.project_set
 
813
 
 
814
    def populate_state(self, state):
 
815
        state.offering = self.context.project_set.offering
 
816
        state.existing_project = self.context
 
817
 
 
818
    def get_default_data(self, req):
 
819
        return {
 
820
            'name':         self.context.name,
 
821
            'short_name':   self.context.short_name,
 
822
            'deadline':     self.context.deadline,
 
823
            'url':          self.context.url,
 
824
            'synopsis':     self.context.synopsis,
 
825
            }
 
826
 
 
827
    def save_object(self, req, data):
 
828
        self.context.name = data['name']
 
829
        self.context.short_name = data['short_name']
 
830
        self.context.deadline = data['deadline']
 
831
        self.context.url = unicode(data['url']) if data['url'] else None
 
832
        self.context.synopsis = data['synopsis']
 
833
        return self.context
 
834
 
 
835
class ProjectNew(BaseFormView):
 
836
    """A form to create a new project."""
 
837
    template = 'templates/project-new.html'
 
838
    tab = 'subjects'
 
839
    permission = 'edit'
 
840
 
 
841
    @property
 
842
    def validator(self):
 
843
        return ProjectSchema()
 
844
 
 
845
    def populate(self, req, ctx):
 
846
        super(ProjectNew, self).populate(req, ctx)
 
847
        ctx['projectset'] = self.context
 
848
 
 
849
    def populate_state(self, state):
 
850
        state.offering = self.context.offering
 
851
        state.existing_project = None
 
852
 
 
853
    def get_default_data(self, req):
 
854
        return {}
 
855
 
 
856
    def save_object(self, req, data):
 
857
        new_project = Project()
 
858
        new_project.project_set = self.context
 
859
        new_project.name = data['name']
 
860
        new_project.short_name = data['short_name']
 
861
        new_project.deadline = data['deadline']
 
862
        new_project.url = unicode(data['url']) if data['url'] else None
 
863
        new_project.synopsis = data['synopsis']
 
864
        req.store.add(new_project)
 
865
        return new_project
 
866
 
 
867
class ProjectDelete(XHTMLView):
 
868
    """A form to delete a project."""
 
869
    template = 'templates/project-delete.html'
 
870
    tab = 'subjects'
 
871
    permission = 'edit'
 
872
 
 
873
    def populate(self, req, ctx):
 
874
        # If post, delete the project, or display a message explaining that
 
875
        # the project cannot be deleted
 
876
        if self.context.can_delete:
 
877
            if req.method == 'POST':
 
878
                self.context.delete()
 
879
                self.template = 'templates/project-deleted.html'
 
880
        else:
 
881
            # Can't delete
 
882
            self.template = 'templates/project-undeletable.html'
 
883
 
 
884
        # If get and can delete, display a delete confirmation page
 
885
 
 
886
        # Variables for the template
 
887
        ctx['req'] = req
 
888
        ctx['project'] = self.context
 
889
        ctx['OfferingProjectsView'] = OfferingProjectsView
 
890
 
 
891
class ProjectSetSchema(formencode.Schema):
 
892
    group_size = formencode.validators.Int(if_missing=None, not_empty=False)
 
893
 
 
894
class ProjectSetEdit(BaseFormView):
 
895
    """A form to edit a project set."""
 
896
    template = 'templates/projectset-edit.html'
 
897
    tab = 'subjects'
 
898
    permission = 'edit'
 
899
 
 
900
    @property
 
901
    def validator(self):
 
902
        return ProjectSetSchema()
 
903
 
 
904
    def populate(self, req, ctx):
 
905
        super(ProjectSetEdit, self).populate(req, ctx)
 
906
 
 
907
    def get_default_data(self, req):
 
908
        return {
 
909
            'group_size': self.context.max_students_per_group,
 
910
            }
 
911
 
 
912
    def save_object(self, req, data):
 
913
        self.context.max_students_per_group = data['group_size']
 
914
        return self.context
 
915
 
 
916
class ProjectSetNew(BaseFormView):
 
917
    """A form to create a new project set."""
 
918
    template = 'templates/projectset-new.html'
 
919
    tab = 'subjects'
 
920
    permission = 'edit'
 
921
    breadcrumb_text = "Projects"
 
922
 
 
923
    @property
 
924
    def validator(self):
 
925
        return ProjectSetSchema()
 
926
 
 
927
    def populate(self, req, ctx):
 
928
        super(ProjectSetNew, self).populate(req, ctx)
 
929
 
 
930
    def get_default_data(self, req):
 
931
        return {}
 
932
 
 
933
    def save_object(self, req, data):
 
934
        new_set = ProjectSet()
 
935
        new_set.offering = self.context
 
936
        new_set.max_students_per_group = data['group_size']
 
937
        req.store.add(new_set)
 
938
        return new_set
729
939
 
730
940
class Plugin(ViewPlugin, MediaPlugin):
731
941
    forward_routes = (root_to_subject, root_to_semester, subject_to_offering,
740
950
             (ApplicationRoot, ('subjects', '+new'), SubjectNew),
741
951
             (ApplicationRoot, ('subjects', '+new-offering'), OfferingNew),
742
952
             (ApplicationRoot, ('+semesters', '+new'), SemesterNew),
 
953
             (Subject, '+index', SubjectView),
743
954
             (Subject, '+edit', SubjectEdit),
 
955
             (Subject, '+new-offering', SubjectOfferingNew),
744
956
             (Semester, '+edit', SemesterEdit),
745
957
             (Offering, '+index', OfferingView),
746
958
             (Offering, '+edit', OfferingEdit),
750
962
             (Enrolment, '+edit', EnrolmentEdit),
751
963
             (Enrolment, '+delete', EnrolmentDelete),
752
964
             (Offering, ('+projects', '+index'), OfferingProjectsView),
 
965
             (Offering, ('+projects', '+new-set'), ProjectSetNew),
 
966
             (ProjectSet, '+edit', ProjectSetEdit),
 
967
             (ProjectSet, '+new', ProjectNew),
753
968
             (Project, '+index', ProjectView),
754
 
 
755
 
             (Offering, ('+projectsets', '+new'), OfferingRESTView, 'api'),
756
 
             (ProjectSet, ('+projects', '+new'), ProjectSetRESTView, 'api'),
 
969
             (Project, '+edit', ProjectEdit),
 
970
             (Project, '+delete', ProjectDelete),
 
971
             (Project, ('+export', 'project-export.sh'),
 
972
                ProjectBashExportView),
757
973
             ]
758
974
 
759
975
    breadcrumbs = {Subject: SubjectBreadcrumb,