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

« back to all changes in this revision

Viewing changes to ivle/database.py

  • Committer: David Coles
  • Date: 2010-07-28 10:52:48 UTC
  • mfrom: (1791.2.10 mediahandlers)
  • Revision ID: coles.david@gmail.com-20100728105248-zvbn9g72v1nsskvd
A series of HTML5 based media handlers using the <audio> and <video> tags.  
This replaces the previous page that just showed a download link (which is 
already available on the menu).

Also solves issue where media files were downloaded by the client twice (once 
in an AJAX request intended only for text).

Known issues:
    * Bug #588285: External BHO will not be able to play media due to not
      having IVLE cookie.
    * Bug #610745: Does not correctly preview revisions
    * Bug #610780: Ogg media does not work in Chromium

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.