~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-11 09:09:53 UTC
  • Revision ID: grantw@unimelb.edu.au-20100211090953-592dk5jruwdg1qrq
Declare appropriate tabs on the rest of the views.

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
218
215
            Semester.id == Offering.semester_id,
219
216
            (not active_only) or (Semester.state == u'current'),
220
217
            Enrolment.offering_id == Offering.id,
221
 
            Enrolment.user_id == self.id,
222
 
            Enrolment.active == True)
 
218
            Enrolment.user_id == self.id)
223
219
 
224
220
    @staticmethod
225
221
    def hash_password(password):
231
227
        """Find a user in a store by login name."""
232
228
        return store.find(cls, cls.login == unicode(login)).one()
233
229
 
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
 
    def get_permissions(self, user, config):
 
230
    def get_permissions(self, user):
241
231
        """Determine privileges held by a user over this object.
242
232
 
243
233
        If the user requesting privileges is this user or an admin,
267
257
    def __repr__(self):
268
258
        return "<%s '%s'>" % (type(self).__name__, self.short_name)
269
259
 
270
 
    def get_permissions(self, user, config):
 
260
    def get_permissions(self, user):
271
261
        """Determine privileges held by a user over this object.
272
262
 
273
263
        If the user requesting privileges is an admin, they may edit.
333
323
    semester = Reference(semester_id, Semester.id)
334
324
    description = Unicode()
335
325
    url = Unicode()
336
 
    show_worksheet_marks = Bool()
337
 
    worksheet_cutoff = DateTime()
338
326
    groups_student_permissions = Unicode()
339
327
 
340
328
    enrolments = ReferenceSet(id, 'Enrolment.offering_id')
383
371
                               Enrolment.offering_id == self.id).one()
384
372
        Store.of(enrolment).remove(enrolment)
385
373
 
386
 
    def get_permissions(self, user, config):
 
374
    def get_permissions(self, user):
387
375
        perms = set()
388
376
        if user is not None:
389
377
            enrolment = self.get_enrolment(user)
390
378
            if enrolment or user.admin:
391
379
                perms.add('view')
392
 
            if enrolment and enrolment.role == u'tutor':
393
 
                perms.add('view_project_submissions')
394
 
                # Site-specific policy on the role of tutors
395
 
                if config['policy']['tutors_can_enrol_students']:
396
 
                    perms.add('enrol')
397
 
                    perms.add('enrol_student')
398
 
                if config['policy']['tutors_can_edit_worksheets']:
399
 
                    perms.add('edit_worksheets')
400
 
                if config['policy']['tutors_can_admin_groups']:
401
 
                    perms.add('admin_groups')
402
 
            if (enrolment and enrolment.role in (u'lecturer')) or user.admin:
403
 
                perms.add('view_project_submissions')
404
 
                perms.add('admin_groups')
405
 
                perms.add('edit_worksheets')
406
 
                perms.add('view_worksheet_marks')
407
 
                perms.add('edit')           # Can edit projects & details
 
380
            if (enrolment and enrolment.role in (u'tutor', u'lecturer')) \
 
381
               or user.admin:
 
382
                perms.add('edit')
 
383
                # XXX Bug #493945 -- should tutors have these permissions?
 
384
                # Potentially move into the next category (lecturer & admin)
408
385
                perms.add('enrol')          # Can see enrolment screen at all
409
386
                perms.add('enrol_student')  # Can enrol students
 
387
            if (enrolment and enrolment.role in (u'lecturer')) or user.admin:
410
388
                perms.add('enrol_tutor')    # Can enrol tutors
411
389
            if user.admin:
412
390
                perms.add('enrol_lecturer') # Can enrol lecturers
437
415
        # XXX: Respect extensions.
438
416
        return self.projects.find(Project.deadline > datetime.datetime.now())
439
417
 
440
 
    def has_worksheet_cutoff_passed(self, user):
441
 
        """Check whether the worksheet cutoff has passed.
442
 
        A user is required, in case we support extensions.
443
 
        """
444
 
        if self.worksheet_cutoff is None:
445
 
            return False
446
 
        else:
447
 
            return self.worksheet_cutoff < datetime.datetime.now()
448
 
 
449
 
    def clone_worksheets(self, source):
450
 
        """Clone all worksheets from the specified source to this offering."""
451
 
        import ivle.worksheet.utils
452
 
        for worksheet in source.worksheets:
453
 
            newws = Worksheet()
454
 
            newws.seq_no = worksheet.seq_no
455
 
            newws.identifier = worksheet.identifier
456
 
            newws.name = worksheet.name
457
 
            newws.assessable = worksheet.assessable
458
 
            newws.published = worksheet.published
459
 
            newws.data = worksheet.data
460
 
            newws.format = worksheet.format
461
 
            newws.offering = self
462
 
            Store.of(self).add(newws)
463
 
            ivle.worksheet.utils.update_exerciselist(newws)
464
 
 
465
 
 
466
418
class Enrolment(Storm):
467
419
    """An enrolment of a user in an offering.
468
420
 
494
446
        return "<%s %r in %r>" % (type(self).__name__, self.user,
495
447
                                  self.offering)
496
448
 
497
 
    def get_permissions(self, user, config):
498
 
        # A user can edit any enrolment that they could have created.
499
 
        perms = set()
500
 
        if ('enrol_' + str(self.role)) in self.offering.get_permissions(
501
 
            user, config):
502
 
            perms.add('edit')
503
 
        return perms
504
 
 
505
 
    def delete(self):
506
 
        """Delete this enrolment."""
507
 
        Store.of(self).remove(self)
508
 
 
509
 
 
510
449
# PROJECTS #
511
450
 
512
451
class ProjectSet(Storm):
532
471
        return "<%s %d in %r>" % (type(self).__name__, self.id,
533
472
                                  self.offering)
534
473
 
535
 
    def get_permissions(self, user, config):
536
 
        return self.offering.get_permissions(user, config)
 
474
    def get_permissions(self, user):
 
475
        return self.offering.get_permissions(user)
537
476
 
538
477
    def get_groups_for_user(self, user):
539
478
        """List all groups in this offering of which the user is a member."""
610
549
        return "<%s '%s' in %r>" % (type(self).__name__, self.short_name,
611
550
                                  self.project_set.offering)
612
551
 
613
 
    def can_submit(self, principal, user, late=False):
614
 
        """
615
 
        @param late: If True, does not take the deadline into account.
616
 
        """
 
552
    def can_submit(self, principal, user):
617
553
        return (self in principal.get_projects() and
618
 
                (late or not self.has_deadline_passed(user)))
 
554
                not self.has_deadline_passed(user))
619
555
 
620
 
    def submit(self, principal, path, revision, who, late=False):
 
556
    def submit(self, principal, path, revision, who):
621
557
        """Submit a Subversion path and revision to a project.
622
558
 
623
559
        @param principal: The owner of the Subversion repository, and the
625
561
        @param path: A path within that repository to submit.
626
562
        @param revision: The revision of that path to submit.
627
563
        @param who: The user who is actually making the submission.
628
 
        @param late: If True, will not raise a DeadlinePassed exception even
629
 
            after the deadline. (Default False.)
630
564
        """
631
565
 
632
 
        if not self.can_submit(principal, who, late=late):
 
566
        if not self.can_submit(principal, who):
633
567
            raise DeadlinePassed()
634
568
 
635
569
        a = Assessed.get(Store.of(self), principal, self)
636
570
        ps = ProjectSubmission()
637
 
        # Raise SubmissionError if the path is illegal
638
 
        ps.path = ProjectSubmission.test_and_normalise_path(path)
 
571
        ps.path = path
639
572
        ps.revision = revision
640
573
        ps.date_submitted = datetime.datetime.now()
641
574
        ps.assessed = a
643
576
 
644
577
        return ps
645
578
 
646
 
    def get_permissions(self, user, config):
647
 
        return self.project_set.offering.get_permissions(user, config)
 
579
    def get_permissions(self, user):
 
580
        return self.project_set.offering.get_permissions(user)
648
581
 
649
582
    @property
650
583
    def latest_submissions(self):
671
604
            return
672
605
        return assessed.submissions
673
606
 
674
 
    @property
675
 
    def can_delete(self):
676
 
        """Can only delete if there are no submissions."""
677
 
        return self.submissions.count() == 0
678
607
 
679
 
    def delete(self):
680
 
        """Delete the project. Fails if can_delete is False."""
681
 
        if not self.can_delete:
682
 
            raise IntegrityError()
683
 
        for assessed in self.assesseds:
684
 
            assessed.delete()
685
 
        Store.of(self).remove(self)
686
608
 
687
609
class ProjectGroup(Storm):
688
610
    """A group of students working together on a project."""
738
660
            Semester.id == Offering.semester_id,
739
661
            (not active_only) or (Semester.state == u'current'))
740
662
 
741
 
    def get_svn_url(self, config):
742
 
        """Get the subversion repository URL for this user or group."""
743
 
        url = config['urls']['svn_addr']
744
 
        path = 'groups/%s_%s_%s_%s' % (
745
 
                self.project_set.offering.subject.short_name,
746
 
                self.project_set.offering.semester.year,
747
 
                self.project_set.offering.semester.semester,
748
 
                self.name
749
 
                )
750
 
        return urlparse.urljoin(url, path)
751
663
 
752
 
    def get_permissions(self, user, config):
 
664
    def get_permissions(self, user):
753
665
        if user.admin or user in self.members:
754
666
            return set(['submit_project'])
755
667
        else:
845
757
 
846
758
        return a
847
759
 
848
 
    def delete(self):
849
 
        """Delete the assessed. Fails if there are any submissions. Deletes
850
 
        extensions."""
851
 
        if self.submissions.count() > 0:
852
 
            raise IntegrityError()
853
 
        for extension in self.extensions:
854
 
            extension.delete()
855
 
        Store.of(self).remove(self)
856
760
 
857
761
class ProjectExtension(Storm):
858
762
    """An extension granted to a user or group on a particular project.
870
774
    approver = Reference(approver_id, User.id)
871
775
    notes = Unicode()
872
776
 
873
 
    def delete(self):
874
 
        """Delete the extension."""
875
 
        Store.of(self).remove(self)
876
 
 
877
 
class SubmissionError(Exception):
878
 
    """Denotes a validation error during submission."""
879
 
    pass
880
 
 
881
777
class ProjectSubmission(Storm):
882
778
    """A submission from a user or group repository to a particular project.
883
779
 
913
809
        return "/files/%s/%s/%s?r=%d" % (user.login,
914
810
            self.assessed.checkout_location, submitpath, self.revision)
915
811
 
916
 
    def get_svn_url(self, config):
917
 
        """Get subversion URL for this submission"""
918
 
        princ = self.assessed.principal
919
 
        base = princ.get_svn_url(config)
920
 
        if self.path.startswith(os.sep):
921
 
            return os.path.join(base,
922
 
                    urllib.quote(self.path[1:].encode('utf-8')))
923
 
        else:
924
 
            return os.path.join(base, urllib.quote(self.path.encode('utf-8')))
925
 
 
926
 
    def get_svn_export_command(self, req):
927
 
        """Returns a Unix shell command to export a submission"""
928
 
        svn_url = self.get_svn_url(req.config)
929
 
        username = (req.user.login if req.user.login.isalnum() else
930
 
                "'%s'"%req.user.login)
931
 
        export_dir = self.assessed.principal.short_name
932
 
        return "svn export --username %s -r%d '%s' %s"%(req.user.login,
933
 
                self.revision, svn_url, export_dir)
934
 
 
935
 
    @staticmethod
936
 
    def test_and_normalise_path(path):
937
 
        """Test that path is valid, and normalise it. This prevents possible
938
 
        injections using malicious paths.
939
 
        Returns the updated path, if successful.
940
 
        Raises SubmissionError if invalid.
941
 
        """
942
 
        # Ensure the path is absolute to prevent being tacked onto working
943
 
        # directories.
944
 
        # Prevent '\n' because it will break all sorts of things.
945
 
        # Prevent '[' and ']' because they can be used to inject into the
946
 
        # svn.conf.
947
 
        # Normalise to avoid resulting in ".." path segments.
948
 
        if not os.path.isabs(path):
949
 
            raise SubmissionError("Path is not absolute")
950
 
        if any(c in path for c in "\n[]"):
951
 
            raise SubmissionError("Path must not contain '\\n', '[' or ']'")
952
 
        return os.path.normpath(path)
953
 
 
954
 
    @property
955
 
    def late(self):
956
 
        """True if the project was submitted late."""
957
 
        return self.days_late > 0
958
 
 
959
 
    @property
960
 
    def days_late(self):
961
 
        """The number of days the project was submitted late (rounded up), or
962
 
        0 if on-time."""
963
 
        # XXX: Need to respect extensions.
964
 
        return max(0,
965
 
            (self.date_submitted - self.assessed.project.deadline).days + 1)
966
 
 
967
812
# WORKSHEETS AND EXERCISES #
968
813
 
969
814
class Exercise(Storm):
976
821
    id = Unicode(primary=True, name="identifier")
977
822
    name = Unicode()
978
823
    description = Unicode()
979
 
    _description_xhtml_cache = Unicode(name='description_xhtml_cache')
980
824
    partial = Unicode()
981
825
    solution = Unicode()
982
826
    include = Unicode()
1000
844
    def __repr__(self):
1001
845
        return "<%s %s>" % (type(self).__name__, self.name)
1002
846
 
1003
 
    def get_permissions(self, user, config):
1004
 
        return self.global_permissions(user, config)
1005
 
 
1006
 
    @staticmethod
1007
 
    def global_permissions(user, config):
1008
 
        """Gets the set of permissions this user has over *all* exercises.
1009
 
        This is used to determine who may view the exercises list, and create
1010
 
        new exercises."""
 
847
    def get_permissions(self, user):
1011
848
        perms = set()
1012
849
        roles = set()
1013
850
        if user is not None:
1017
854
            elif u'lecturer' in set((e.role for e in user.active_enrolments)):
1018
855
                perms.add('edit')
1019
856
                perms.add('view')
1020
 
            elif (config['policy']['tutors_can_edit_worksheets']
1021
 
            and u'tutor' in set((e.role for e in user.active_enrolments))):
1022
 
                # Site-specific policy on the role of tutors
 
857
            elif u'tutor' in set((e.role for e in user.active_enrolments)):
1023
858
                perms.add('edit')
1024
859
                perms.add('view')
1025
860
 
1026
861
        return perms
1027
862
 
1028
 
    def _cache_description_xhtml(self, invalidate=False):
1029
 
        # Don't regenerate an existing cache unless forced.
1030
 
        if self._description_xhtml_cache is not None and not invalidate:
1031
 
            return
1032
 
 
1033
 
        if self.description:
1034
 
            self._description_xhtml_cache = rst(self.description)
1035
 
        else:
1036
 
            self._description_xhtml_cache = None
1037
 
 
1038
 
    @property
1039
 
    def description_xhtml(self):
1040
 
        """The XHTML exercise description, converted from reStructuredText."""
1041
 
        self._cache_description_xhtml()
1042
 
        return self._description_xhtml_cache
1043
 
 
1044
 
    def set_description(self, description):
1045
 
        self.description = description
1046
 
        self._cache_description_xhtml(invalidate=True)
 
863
    def get_description(self):
 
864
        """Return the description interpreted as reStructuredText."""
 
865
        return rst(self.description)
1047
866
 
1048
867
    def delete(self):
1049
868
        """Deletes the exercise, providing it has no associated worksheets."""
1066
885
    identifier = Unicode()
1067
886
    name = Unicode()
1068
887
    assessable = Bool()
1069
 
    published = Bool()
1070
888
    data = Unicode()
1071
 
    _data_xhtml_cache = Unicode(name='data_xhtml_cache')
1072
889
    seq_no = Int()
1073
890
    format = Unicode()
1074
891
 
1104
921
        store.find(WorksheetExercise,
1105
922
            WorksheetExercise.worksheet == self).remove()
1106
923
 
1107
 
    def get_permissions(self, user, config):
1108
 
        offering_perms = self.offering.get_permissions(user, config)
1109
 
 
1110
 
        perms = set()
1111
 
 
1112
 
        # Anybody who can view an offering can view a published
1113
 
        # worksheet.
1114
 
        if 'view' in offering_perms and self.published:
1115
 
            perms.add('view')
1116
 
 
1117
 
        # Any worksheet editors can both view and edit.
1118
 
        if 'edit_worksheets' in offering_perms:
1119
 
            perms.add('view')
1120
 
            perms.add('edit')
1121
 
 
1122
 
        return perms
1123
 
 
1124
 
    def _cache_data_xhtml(self, invalidate=False):
1125
 
        # Don't regenerate an existing cache unless forced.
1126
 
        if self._data_xhtml_cache is not None and not invalidate:
1127
 
            return
1128
 
 
1129
 
        if self.format == u'rst':
1130
 
            self._data_xhtml_cache = rst(self.data)
1131
 
        else:
1132
 
            self._data_xhtml_cache = None
1133
 
 
1134
 
    @property
1135
 
    def data_xhtml(self):
1136
 
        """The XHTML of this worksheet, converted from rST if required."""
1137
 
        # Update the rST -> XHTML cache, if required.
1138
 
        self._cache_data_xhtml()
1139
 
 
1140
 
        if self.format == u'rst':
1141
 
            return self._data_xhtml_cache
 
924
    def get_permissions(self, user):
 
925
        return self.offering.get_permissions(user)
 
926
 
 
927
    def get_xml(self):
 
928
        """Returns the xml of this worksheet, converts from rst if required."""
 
929
        if self.format == u'rst':
 
930
            ws_xml = rst(self.data)
 
931
            return ws_xml
1142
932
        else:
1143
933
            return self.data
1144
934
 
1145
 
    def set_data(self, data):
1146
 
        self.data = data
1147
 
        self._cache_data_xhtml(invalidate=True)
1148
 
 
1149
935
    def delete(self):
1150
936
        """Deletes the worksheet, provided it has no attempts on any exercises.
1151
937
 
1187
973
        return "<%s %s in %s>" % (type(self).__name__, self.exercise.name,
1188
974
                                  self.worksheet.identifier)
1189
975
 
1190
 
    def get_permissions(self, user, config):
1191
 
        return self.worksheet.get_permissions(user, config)
 
976
    def get_permissions(self, user):
 
977
        return self.worksheet.get_permissions(user)
1192
978
 
1193
979
 
1194
980
class ExerciseSave(Storm):
1213
999
 
1214
1000
    def __repr__(self):
1215
1001
        return "<%s %s by %s at %s>" % (type(self).__name__,
1216
 
            self.worksheet_exercise.exercise.name, self.user.login,
1217
 
            self.date.strftime("%c"))
 
1002
            self.exercise.name, self.user.login, self.date.strftime("%c"))
1218
1003
 
1219
1004
class ExerciseAttempt(ExerciseSave):
1220
1005
    """An attempt at solving an exercise.
1242
1027
    complete = Bool()
1243
1028
    active = Bool()
1244
1029
 
1245
 
    def get_permissions(self, user, config):
 
1030
    def get_permissions(self, user):
1246
1031
        return set(['view']) if user is self.user else set()
1247
1032
 
1248
1033
class TestSuite(Storm):