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

« back to all changes in this revision

Viewing changes to ivle/database.py

  • Committer: William Grant
  • Date: 2012-06-28 01:52:02 UTC
  • Revision ID: me@williamgrant.id.au-20120628015202-f6ru7o367gt6nvgz
Hah

Show diffs side-by-side

added added

removed removed

Lines of Context:
26
26
import hashlib
27
27
import datetime
28
28
import os
 
29
import urlparse
 
30
import urllib
29
31
 
30
32
from storm.locals import create_database, Store, Int, Unicode, DateTime, \
31
33
                         Reference, ReferenceSet, Bool, Storm, Desc
148
150
            Offering.semester_id == Semester.id,
149
151
            Offering.subject_id == Subject.id).order_by(
150
152
                Desc(Semester.year),
151
 
                Desc(Semester.semester),
 
153
                Desc(Semester.display_name),
152
154
                Desc(Subject.code)
153
155
            )
154
156
 
229
231
        """Find a user in a store by login name."""
230
232
        return store.find(cls, cls.login == unicode(login)).one()
231
233
 
 
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
 
232
240
    def get_permissions(self, user, config):
233
241
        """Determine privileges held by a user over this object.
234
242
 
290
298
        """
291
299
        return self.offerings.find(Offering.semester_id == Semester.id,
292
300
                               Semester.year == unicode(year),
293
 
                               Semester.semester == unicode(semester)).one()
 
301
                               Semester.url_name == unicode(semester)).one()
294
302
 
295
303
class Semester(Storm):
296
304
    """A semester in which subjects can be run."""
299
307
 
300
308
    id = Int(primary=True, name="semesterid")
301
309
    year = Unicode()
302
 
    semester = Unicode()
 
310
    code = Unicode()
 
311
    url_name = Unicode()
 
312
    display_name = Unicode()
303
313
    state = Unicode()
304
314
 
305
315
    offerings = ReferenceSet(id, 'Offering.semester_id')
311
321
    __init__ = _kwarg_init
312
322
 
313
323
    def __repr__(self):
314
 
        return "<%s %s/%s>" % (type(self).__name__, self.year, self.semester)
 
324
        return "<%s %s/%s>" % (type(self).__name__, self.year, self.code)
315
325
 
316
326
class Offering(Storm):
317
327
    """An offering of a subject in a particular semester."""
325
335
    semester = Reference(semester_id, Semester.id)
326
336
    description = Unicode()
327
337
    url = Unicode()
 
338
    show_worksheet_marks = Bool()
 
339
    worksheet_cutoff = DateTime()
328
340
    groups_student_permissions = Unicode()
329
341
 
330
342
    enrolments = ReferenceSet(id, 'Enrolment.offering_id')
393
405
                perms.add('view_project_submissions')
394
406
                perms.add('admin_groups')
395
407
                perms.add('edit_worksheets')
 
408
                perms.add('view_worksheet_marks')
396
409
                perms.add('edit')           # Can edit projects & details
397
410
                perms.add('enrol')          # Can see enrolment screen at all
398
411
                perms.add('enrol_student')  # Can enrol students
426
439
        # XXX: Respect extensions.
427
440
        return self.projects.find(Project.deadline > datetime.datetime.now())
428
441
 
 
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
 
429
451
    def clone_worksheets(self, source):
430
452
        """Clone all worksheets from the specified source to this offering."""
431
453
        import ivle.worksheet.utils
435
457
            newws.identifier = worksheet.identifier
436
458
            newws.name = worksheet.name
437
459
            newws.assessable = worksheet.assessable
 
460
            newws.published = worksheet.published
438
461
            newws.data = worksheet.data
439
462
            newws.format = worksheet.format
440
463
            newws.offering = self
589
612
        return "<%s '%s' in %r>" % (type(self).__name__, self.short_name,
590
613
                                  self.project_set.offering)
591
614
 
592
 
    def can_submit(self, principal, user):
 
615
    def can_submit(self, principal, user, late=False):
 
616
        """
 
617
        @param late: If True, does not take the deadline into account.
 
618
        """
593
619
        return (self in principal.get_projects() and
594
 
                not self.has_deadline_passed(user))
 
620
                (late or not self.has_deadline_passed(user)))
595
621
 
596
 
    def submit(self, principal, path, revision, who):
 
622
    def submit(self, principal, path, revision, who, late=False):
597
623
        """Submit a Subversion path and revision to a project.
598
624
 
599
625
        @param principal: The owner of the Subversion repository, and the
601
627
        @param path: A path within that repository to submit.
602
628
        @param revision: The revision of that path to submit.
603
629
        @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.)
604
632
        """
605
633
 
606
 
        if not self.can_submit(principal, who):
 
634
        if not self.can_submit(principal, who, late=late):
607
635
            raise DeadlinePassed()
608
636
 
609
637
        a = Assessed.get(Store.of(self), principal, self)
645
673
            return
646
674
        return assessed.submissions
647
675
 
 
676
    @property
 
677
    def can_delete(self):
 
678
        """Can only delete if there are no submissions."""
 
679
        return self.submissions.count() == 0
648
680
 
 
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)
649
688
 
650
689
class ProjectGroup(Storm):
651
690
    """A group of students working together on a project."""
701
740
            Semester.id == Offering.semester_id,
702
741
            (not active_only) or (Semester.state == u'current'))
703
742
 
 
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)
704
753
 
705
754
    def get_permissions(self, user, config):
706
755
        if user.admin or user in self.members:
798
847
 
799
848
        return a
800
849
 
 
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)
801
858
 
802
859
class ProjectExtension(Storm):
803
860
    """An extension granted to a user or group on a particular project.
810
867
    id = Int(name="extensionid", primary=True)
811
868
    assessed_id = Int(name="assessedid")
812
869
    assessed = Reference(assessed_id, Assessed.id)
813
 
    deadline = DateTime()
 
870
    days = Int()
814
871
    approver_id = Int(name="approver")
815
872
    approver = Reference(approver_id, User.id)
816
873
    notes = Unicode()
817
874
 
 
875
    def delete(self):
 
876
        """Delete the extension."""
 
877
        Store.of(self).remove(self)
 
878
 
818
879
class SubmissionError(Exception):
819
880
    """Denotes a validation error during submission."""
820
881
    pass
854
915
        return "/files/%s/%s/%s?r=%d" % (user.login,
855
916
            self.assessed.checkout_location, submitpath, self.revision)
856
917
 
 
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
 
857
941
    @staticmethod
858
942
    def test_and_normalise_path(path):
859
943
        """Test that path is valid, and normalise it. This prevents possible
873
957
            raise SubmissionError("Path must not contain '\\n', '[' or ']'")
874
958
        return os.path.normpath(path)
875
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
 
876
973
# WORKSHEETS AND EXERCISES #
877
974
 
878
975
class Exercise(Storm):
885
982
    id = Unicode(primary=True, name="identifier")
886
983
    name = Unicode()
887
984
    description = Unicode()
 
985
    _description_xhtml_cache = Unicode(name='description_xhtml_cache')
888
986
    partial = Unicode()
889
987
    solution = Unicode()
890
988
    include = Unicode()
933
1031
 
934
1032
        return perms
935
1033
 
936
 
    def get_description(self):
937
 
        """Return the description interpreted as reStructuredText."""
938
 
        return rst(self.description)
 
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)
939
1053
 
940
1054
    def delete(self):
941
1055
        """Deletes the exercise, providing it has no associated worksheets."""
958
1072
    identifier = Unicode()
959
1073
    name = Unicode()
960
1074
    assessable = Bool()
 
1075
    published = Bool()
961
1076
    data = Unicode()
 
1077
    _data_xhtml_cache = Unicode(name='data_xhtml_cache')
962
1078
    seq_no = Int()
963
1079
    format = Unicode()
964
1080
 
995
1111
            WorksheetExercise.worksheet == self).remove()
996
1112
 
997
1113
    def get_permissions(self, user, config):
998
 
        # Almost the same permissions as for the offering itself
999
 
        perms = self.offering.get_permissions(user, config)
1000
 
        # However, "edit" permission is derived from the "edit_worksheets"
1001
 
        # permission of the offering
1002
 
        if 'edit_worksheets' in perms:
 
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')
1003
1126
            perms.add('edit')
1004
 
        else:
1005
 
            perms.discard('edit')
 
1127
 
1006
1128
        return perms
1007
1129
 
1008
 
    def get_xml(self):
1009
 
        """Returns the xml of this worksheet, converts from rst if required."""
1010
 
        if self.format == u'rst':
1011
 
            ws_xml = rst(self.data)
1012
 
            return ws_xml
 
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
1013
1148
        else:
1014
1149
            return self.data
1015
1150
 
 
1151
    def set_data(self, data):
 
1152
        self.data = data
 
1153
        self._cache_data_xhtml(invalidate=True)
 
1154
 
1016
1155
    def delete(self):
1017
1156
        """Deletes the worksheet, provided it has no attempts on any exercises.
1018
1157
 
1080
1219
 
1081
1220
    def __repr__(self):
1082
1221
        return "<%s %s by %s at %s>" % (type(self).__name__,
1083
 
            self.exercise.name, self.user.login, self.date.strftime("%c"))
 
1222
            self.worksheet_exercise.exercise.name, self.user.login,
 
1223
            self.date.strftime("%c"))
1084
1224
 
1085
1225
class ExerciseAttempt(ExerciseSave):
1086
1226
    """An attempt at solving an exercise.