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

« back to all changes in this revision

Viewing changes to ivle/database.py

  • Committer: William Grant
  • Date: 2010-02-17 08:37:22 UTC
  • Revision ID: grantw@unimelb.edu.au-20100217083722-ji4v6fbztngy0p3s
Add UI to edit/delete enrolments.

Show diffs side-by-side

added added

removed removed

Lines of Context:
25
25
 
26
26
import hashlib
27
27
import datetime
28
 
import os
29
 
import urlparse
30
 
import urllib
31
28
 
32
29
from storm.locals import create_database, Store, Int, Unicode, DateTime, \
33
30
                         Reference, ReferenceSet, Bool, Storm, Desc
150
147
            Offering.semester_id == Semester.id,
151
148
            Offering.subject_id == Subject.id).order_by(
152
149
                Desc(Semester.year),
153
 
                Desc(Semester.display_name),
 
150
                Desc(Semester.semester),
154
151
                Desc(Subject.code)
155
152
            )
156
153
 
231
228
        """Find a user in a store by login name."""
232
229
        return store.find(cls, cls.login == unicode(login)).one()
233
230
 
234
 
    def get_svn_url(self, config):
235
 
        """Get the subversion repository URL for this user or group."""
236
 
        url = config['urls']['svn_addr']
237
 
        path = 'users/%s' % self.login
238
 
        return urlparse.urljoin(url, path)
239
 
 
240
231
    def get_permissions(self, user, config):
241
232
        """Determine privileges held by a user over this object.
242
233
 
298
289
        """
299
290
        return self.offerings.find(Offering.semester_id == Semester.id,
300
291
                               Semester.year == unicode(year),
301
 
                               Semester.url_name == unicode(semester)).one()
 
292
                               Semester.semester == unicode(semester)).one()
302
293
 
303
294
class Semester(Storm):
304
295
    """A semester in which subjects can be run."""
307
298
 
308
299
    id = Int(primary=True, name="semesterid")
309
300
    year = Unicode()
310
 
    code = Unicode()
311
 
    url_name = Unicode()
312
 
    display_name = Unicode()
 
301
    semester = Unicode()
313
302
    state = Unicode()
314
303
 
315
304
    offerings = ReferenceSet(id, 'Offering.semester_id')
321
310
    __init__ = _kwarg_init
322
311
 
323
312
    def __repr__(self):
324
 
        return "<%s %s/%s>" % (type(self).__name__, self.year, self.code)
 
313
        return "<%s %s/%s>" % (type(self).__name__, self.year, self.semester)
325
314
 
326
315
class Offering(Storm):
327
316
    """An offering of a subject in a particular semester."""
335
324
    semester = Reference(semester_id, Semester.id)
336
325
    description = Unicode()
337
326
    url = Unicode()
338
 
    show_worksheet_marks = Bool()
339
 
    worksheet_cutoff = DateTime()
340
327
    groups_student_permissions = Unicode()
341
328
 
342
329
    enrolments = ReferenceSet(id, 'Enrolment.offering_id')
405
392
                perms.add('view_project_submissions')
406
393
                perms.add('admin_groups')
407
394
                perms.add('edit_worksheets')
408
 
                perms.add('view_worksheet_marks')
409
395
                perms.add('edit')           # Can edit projects & details
410
396
                perms.add('enrol')          # Can see enrolment screen at all
411
397
                perms.add('enrol_student')  # Can enrol students
439
425
        # XXX: Respect extensions.
440
426
        return self.projects.find(Project.deadline > datetime.datetime.now())
441
427
 
442
 
    def has_worksheet_cutoff_passed(self, user):
443
 
        """Check whether the worksheet cutoff has passed.
444
 
        A user is required, in case we support extensions.
445
 
        """
446
 
        if self.worksheet_cutoff is None:
447
 
            return False
448
 
        else:
449
 
            return self.worksheet_cutoff < datetime.datetime.now()
450
 
 
451
428
    def clone_worksheets(self, source):
452
429
        """Clone all worksheets from the specified source to this offering."""
453
430
        import ivle.worksheet.utils
457
434
            newws.identifier = worksheet.identifier
458
435
            newws.name = worksheet.name
459
436
            newws.assessable = worksheet.assessable
460
 
            newws.published = worksheet.published
461
437
            newws.data = worksheet.data
462
438
            newws.format = worksheet.format
463
439
            newws.offering = self
612
588
        return "<%s '%s' in %r>" % (type(self).__name__, self.short_name,
613
589
                                  self.project_set.offering)
614
590
 
615
 
    def can_submit(self, principal, user, late=False):
616
 
        """
617
 
        @param late: If True, does not take the deadline into account.
618
 
        """
 
591
    def can_submit(self, principal, user):
619
592
        return (self in principal.get_projects() and
620
 
                (late or not self.has_deadline_passed(user)))
 
593
                not self.has_deadline_passed(user))
621
594
 
622
 
    def submit(self, principal, path, revision, who, late=False):
 
595
    def submit(self, principal, path, revision, who):
623
596
        """Submit a Subversion path and revision to a project.
624
597
 
625
598
        @param principal: The owner of the Subversion repository, and the
627
600
        @param path: A path within that repository to submit.
628
601
        @param revision: The revision of that path to submit.
629
602
        @param who: The user who is actually making the submission.
630
 
        @param late: If True, will not raise a DeadlinePassed exception even
631
 
            after the deadline. (Default False.)
632
603
        """
633
604
 
634
 
        if not self.can_submit(principal, who, late=late):
 
605
        if not self.can_submit(principal, who):
635
606
            raise DeadlinePassed()
636
607
 
637
608
        a = Assessed.get(Store.of(self), principal, self)
638
609
        ps = ProjectSubmission()
639
 
        # Raise SubmissionError if the path is illegal
640
 
        ps.path = ProjectSubmission.test_and_normalise_path(path)
 
610
        ps.path = path
641
611
        ps.revision = revision
642
612
        ps.date_submitted = datetime.datetime.now()
643
613
        ps.assessed = a
673
643
            return
674
644
        return assessed.submissions
675
645
 
676
 
    @property
677
 
    def can_delete(self):
678
 
        """Can only delete if there are no submissions."""
679
 
        return self.submissions.count() == 0
680
646
 
681
 
    def delete(self):
682
 
        """Delete the project. Fails if can_delete is False."""
683
 
        if not self.can_delete:
684
 
            raise IntegrityError()
685
 
        for assessed in self.assesseds:
686
 
            assessed.delete()
687
 
        Store.of(self).remove(self)
688
647
 
689
648
class ProjectGroup(Storm):
690
649
    """A group of students working together on a project."""
740
699
            Semester.id == Offering.semester_id,
741
700
            (not active_only) or (Semester.state == u'current'))
742
701
 
743
 
    def get_svn_url(self, config):
744
 
        """Get the subversion repository URL for this user or group."""
745
 
        url = config['urls']['svn_addr']
746
 
        path = 'groups/%s_%s_%s_%s' % (
747
 
                self.project_set.offering.subject.short_name,
748
 
                self.project_set.offering.semester.year,
749
 
                self.project_set.offering.semester.url_name,
750
 
                self.name
751
 
                )
752
 
        return urlparse.urljoin(url, path)
753
702
 
754
703
    def get_permissions(self, user, config):
755
704
        if user.admin or user in self.members:
847
796
 
848
797
        return a
849
798
 
850
 
    def delete(self):
851
 
        """Delete the assessed. Fails if there are any submissions. Deletes
852
 
        extensions."""
853
 
        if self.submissions.count() > 0:
854
 
            raise IntegrityError()
855
 
        for extension in self.extensions:
856
 
            extension.delete()
857
 
        Store.of(self).remove(self)
858
799
 
859
800
class ProjectExtension(Storm):
860
801
    """An extension granted to a user or group on a particular project.
867
808
    id = Int(name="extensionid", primary=True)
868
809
    assessed_id = Int(name="assessedid")
869
810
    assessed = Reference(assessed_id, Assessed.id)
870
 
    days = Int()
 
811
    deadline = DateTime()
871
812
    approver_id = Int(name="approver")
872
813
    approver = Reference(approver_id, User.id)
873
814
    notes = Unicode()
874
815
 
875
 
    def delete(self):
876
 
        """Delete the extension."""
877
 
        Store.of(self).remove(self)
878
 
 
879
 
class SubmissionError(Exception):
880
 
    """Denotes a validation error during submission."""
881
 
    pass
882
 
 
883
816
class ProjectSubmission(Storm):
884
817
    """A submission from a user or group repository to a particular project.
885
818
 
915
848
        return "/files/%s/%s/%s?r=%d" % (user.login,
916
849
            self.assessed.checkout_location, submitpath, self.revision)
917
850
 
918
 
    def get_svn_url(self, config):
919
 
        """Get subversion URL for this submission"""
920
 
        princ = self.assessed.principal
921
 
        base = princ.get_svn_url(config)
922
 
        if self.path.startswith(os.sep):
923
 
            return os.path.join(base,
924
 
                    urllib.quote(self.path[1:].encode('utf-8')))
925
 
        else:
926
 
            return os.path.join(base, urllib.quote(self.path.encode('utf-8')))
927
 
 
928
 
    def get_svn_export_command(self, req):
929
 
        """Returns a Unix shell command to export a submission"""
930
 
        svn_url = self.get_svn_url(req.config)
931
 
        _, ext = os.path.splitext(svn_url)
932
 
        username = (req.user.login if req.user.login.isalnum() else
933
 
                "'%s'"%req.user.login)
934
 
        # Export to a file or directory relative to the current directory,
935
 
        # with the student's login name, appended with the submitted file's
936
 
        # extension, if any
937
 
        export_path = self.assessed.principal.short_name + ext
938
 
        return "svn export --username %s -r%d '%s' %s"%(req.user.login,
939
 
                self.revision, svn_url, export_path)
940
 
 
941
 
    @staticmethod
942
 
    def test_and_normalise_path(path):
943
 
        """Test that path is valid, and normalise it. This prevents possible
944
 
        injections using malicious paths.
945
 
        Returns the updated path, if successful.
946
 
        Raises SubmissionError if invalid.
947
 
        """
948
 
        # Ensure the path is absolute to prevent being tacked onto working
949
 
        # directories.
950
 
        # Prevent '\n' because it will break all sorts of things.
951
 
        # Prevent '[' and ']' because they can be used to inject into the
952
 
        # svn.conf.
953
 
        # Normalise to avoid resulting in ".." path segments.
954
 
        if not os.path.isabs(path):
955
 
            raise SubmissionError("Path is not absolute")
956
 
        if any(c in path for c in "\n[]"):
957
 
            raise SubmissionError("Path must not contain '\\n', '[' or ']'")
958
 
        return os.path.normpath(path)
959
 
 
960
 
    @property
961
 
    def late(self):
962
 
        """True if the project was submitted late."""
963
 
        return self.days_late > 0
964
 
 
965
 
    @property
966
 
    def days_late(self):
967
 
        """The number of days the project was submitted late (rounded up), or
968
 
        0 if on-time."""
969
 
        # XXX: Need to respect extensions.
970
 
        return max(0,
971
 
            (self.date_submitted - self.assessed.project.deadline).days + 1)
972
 
 
973
851
# WORKSHEETS AND EXERCISES #
974
852
 
975
853
class Exercise(Storm):
982
860
    id = Unicode(primary=True, name="identifier")
983
861
    name = Unicode()
984
862
    description = Unicode()
985
 
    _description_xhtml_cache = Unicode(name='description_xhtml_cache')
986
863
    partial = Unicode()
987
864
    solution = Unicode()
988
865
    include = Unicode()
1031
908
 
1032
909
        return perms
1033
910
 
1034
 
    def _cache_description_xhtml(self, invalidate=False):
1035
 
        # Don't regenerate an existing cache unless forced.
1036
 
        if self._description_xhtml_cache is not None and not invalidate:
1037
 
            return
1038
 
 
1039
 
        if self.description:
1040
 
            self._description_xhtml_cache = rst(self.description)
1041
 
        else:
1042
 
            self._description_xhtml_cache = None
1043
 
 
1044
 
    @property
1045
 
    def description_xhtml(self):
1046
 
        """The XHTML exercise description, converted from reStructuredText."""
1047
 
        self._cache_description_xhtml()
1048
 
        return self._description_xhtml_cache
1049
 
 
1050
 
    def set_description(self, description):
1051
 
        self.description = description
1052
 
        self._cache_description_xhtml(invalidate=True)
 
911
    def get_description(self):
 
912
        """Return the description interpreted as reStructuredText."""
 
913
        return rst(self.description)
1053
914
 
1054
915
    def delete(self):
1055
916
        """Deletes the exercise, providing it has no associated worksheets."""
1072
933
    identifier = Unicode()
1073
934
    name = Unicode()
1074
935
    assessable = Bool()
1075
 
    published = Bool()
1076
936
    data = Unicode()
1077
 
    _data_xhtml_cache = Unicode(name='data_xhtml_cache')
1078
937
    seq_no = Int()
1079
938
    format = Unicode()
1080
939
 
1111
970
            WorksheetExercise.worksheet == self).remove()
1112
971
 
1113
972
    def get_permissions(self, user, config):
1114
 
        offering_perms = self.offering.get_permissions(user, config)
1115
 
 
1116
 
        perms = set()
1117
 
 
1118
 
        # Anybody who can view an offering can view a published
1119
 
        # worksheet.
1120
 
        if 'view' in offering_perms and self.published:
1121
 
            perms.add('view')
1122
 
 
1123
 
        # Any worksheet editors can both view and edit.
1124
 
        if 'edit_worksheets' in offering_perms:
1125
 
            perms.add('view')
 
973
        # Almost the same permissions as for the offering itself
 
974
        perms = self.offering.get_permissions(user, config)
 
975
        # However, "edit" permission is derived from the "edit_worksheets"
 
976
        # permission of the offering
 
977
        if 'edit_worksheets' in perms:
1126
978
            perms.add('edit')
1127
 
 
 
979
        else:
 
980
            perms.discard('edit')
1128
981
        return perms
1129
982
 
1130
 
    def _cache_data_xhtml(self, invalidate=False):
1131
 
        # Don't regenerate an existing cache unless forced.
1132
 
        if self._data_xhtml_cache is not None and not invalidate:
1133
 
            return
1134
 
 
1135
 
        if self.format == u'rst':
1136
 
            self._data_xhtml_cache = rst(self.data)
1137
 
        else:
1138
 
            self._data_xhtml_cache = None
1139
 
 
1140
 
    @property
1141
 
    def data_xhtml(self):
1142
 
        """The XHTML of this worksheet, converted from rST if required."""
1143
 
        # Update the rST -> XHTML cache, if required.
1144
 
        self._cache_data_xhtml()
1145
 
 
1146
 
        if self.format == u'rst':
1147
 
            return self._data_xhtml_cache
 
983
    def get_xml(self):
 
984
        """Returns the xml of this worksheet, converts from rst if required."""
 
985
        if self.format == u'rst':
 
986
            ws_xml = rst(self.data)
 
987
            return ws_xml
1148
988
        else:
1149
989
            return self.data
1150
990
 
1151
 
    def set_data(self, data):
1152
 
        self.data = data
1153
 
        self._cache_data_xhtml(invalidate=True)
1154
 
 
1155
991
    def delete(self):
1156
992
        """Deletes the worksheet, provided it has no attempts on any exercises.
1157
993
 
1219
1055
 
1220
1056
    def __repr__(self):
1221
1057
        return "<%s %s by %s at %s>" % (type(self).__name__,
1222
 
            self.worksheet_exercise.exercise.name, self.user.login,
1223
 
            self.date.strftime("%c"))
 
1058
            self.exercise.name, self.user.login, self.date.strftime("%c"))
1224
1059
 
1225
1060
class ExerciseAttempt(ExerciseSave):
1226
1061
    """An attempt at solving an exercise.