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

« back to all changes in this revision

Viewing changes to ivle/database.py

  • Committer: Matt Giuca
  • Date: 2010-03-05 06:40:59 UTC
  • Revision ID: matt.giuca@gmail.com-20100305064059-wc6jsup5v66lo1o4
Added an entry on the user settings page to display the user's Subversion password. This was previously only possible through an arcane console command. Updated documentation. This fixes Launchpad bug #528450.

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
28
29
 
29
30
from storm.locals import create_database, Store, Int, Unicode, DateTime, \
30
31
                         Reference, ReferenceSet, Bool, Storm, Desc
324
325
    semester = Reference(semester_id, Semester.id)
325
326
    description = Unicode()
326
327
    url = Unicode()
 
328
    show_worksheet_marks = Bool()
 
329
    worksheet_cutoff = DateTime()
327
330
    groups_student_permissions = Unicode()
328
331
 
329
332
    enrolments = ReferenceSet(id, 'Enrolment.offering_id')
392
395
                perms.add('view_project_submissions')
393
396
                perms.add('admin_groups')
394
397
                perms.add('edit_worksheets')
 
398
                perms.add('view_worksheet_marks')
395
399
                perms.add('edit')           # Can edit projects & details
396
400
                perms.add('enrol')          # Can see enrolment screen at all
397
401
                perms.add('enrol_student')  # Can enrol students
425
429
        # XXX: Respect extensions.
426
430
        return self.projects.find(Project.deadline > datetime.datetime.now())
427
431
 
 
432
    def has_worksheet_cutoff_passed(self, user):
 
433
        """Check whether the worksheet cutoff has passed.
 
434
        A user is required, in case we support extensions.
 
435
        """
 
436
        if self.worksheet_cutoff is None:
 
437
            return False
 
438
        else:
 
439
            return self.worksheet_cutoff < datetime.datetime.now()
 
440
 
 
441
    def clone_worksheets(self, source):
 
442
        """Clone all worksheets from the specified source to this offering."""
 
443
        import ivle.worksheet.utils
 
444
        for worksheet in source.worksheets:
 
445
            newws = Worksheet()
 
446
            newws.seq_no = worksheet.seq_no
 
447
            newws.identifier = worksheet.identifier
 
448
            newws.name = worksheet.name
 
449
            newws.assessable = worksheet.assessable
 
450
            newws.published = worksheet.published
 
451
            newws.data = worksheet.data
 
452
            newws.format = worksheet.format
 
453
            newws.offering = self
 
454
            Store.of(self).add(newws)
 
455
            ivle.worksheet.utils.update_exerciselist(newws)
 
456
 
 
457
 
428
458
class Enrolment(Storm):
429
459
    """An enrolment of a user in an offering.
430
460
 
456
486
        return "<%s %r in %r>" % (type(self).__name__, self.user,
457
487
                                  self.offering)
458
488
 
 
489
    def get_permissions(self, user, config):
 
490
        # A user can edit any enrolment that they could have created.
 
491
        perms = set()
 
492
        if ('enrol_' + str(self.role)) in self.offering.get_permissions(
 
493
            user, config):
 
494
            perms.add('edit')
 
495
        return perms
 
496
 
 
497
    def delete(self):
 
498
        """Delete this enrolment."""
 
499
        Store.of(self).remove(self)
 
500
 
 
501
 
459
502
# PROJECTS #
460
503
 
461
504
class ProjectSet(Storm):
578
621
 
579
622
        a = Assessed.get(Store.of(self), principal, self)
580
623
        ps = ProjectSubmission()
581
 
        ps.path = path
 
624
        # Raise SubmissionError if the path is illegal
 
625
        ps.path = ProjectSubmission.test_and_normalise_path(path)
582
626
        ps.revision = revision
583
627
        ps.date_submitted = datetime.datetime.now()
584
628
        ps.assessed = a
614
658
            return
615
659
        return assessed.submissions
616
660
 
 
661
    @property
 
662
    def can_delete(self):
 
663
        """Can only delete if there are no submissions."""
 
664
        return self.submissions.count() == 0
617
665
 
 
666
    def delete(self):
 
667
        """Delete the project. Fails if can_delete is False."""
 
668
        if not self.can_delete:
 
669
            raise IntegrityError()
 
670
        for assessed in self.assesseds:
 
671
            assessed.delete()
 
672
        Store.of(self).remove(self)
618
673
 
619
674
class ProjectGroup(Storm):
620
675
    """A group of students working together on a project."""
767
822
 
768
823
        return a
769
824
 
 
825
    def delete(self):
 
826
        """Delete the assessed. Fails if there are any submissions. Deletes
 
827
        extensions."""
 
828
        if self.submissions.count() > 0:
 
829
            raise IntegrityError()
 
830
        for extension in self.extensions:
 
831
            extension.delete()
 
832
        Store.of(self).remove(self)
770
833
 
771
834
class ProjectExtension(Storm):
772
835
    """An extension granted to a user or group on a particular project.
784
847
    approver = Reference(approver_id, User.id)
785
848
    notes = Unicode()
786
849
 
 
850
    def delete(self):
 
851
        """Delete the extension."""
 
852
        Store.of(self).remove(self)
 
853
 
 
854
class SubmissionError(Exception):
 
855
    """Denotes a validation error during submission."""
 
856
    pass
 
857
 
787
858
class ProjectSubmission(Storm):
788
859
    """A submission from a user or group repository to a particular project.
789
860
 
819
890
        return "/files/%s/%s/%s?r=%d" % (user.login,
820
891
            self.assessed.checkout_location, submitpath, self.revision)
821
892
 
 
893
    @staticmethod
 
894
    def test_and_normalise_path(path):
 
895
        """Test that path is valid, and normalise it. This prevents possible
 
896
        injections using malicious paths.
 
897
        Returns the updated path, if successful.
 
898
        Raises SubmissionError if invalid.
 
899
        """
 
900
        # Ensure the path is absolute to prevent being tacked onto working
 
901
        # directories.
 
902
        # Prevent '\n' because it will break all sorts of things.
 
903
        # Prevent '[' and ']' because they can be used to inject into the
 
904
        # svn.conf.
 
905
        # Normalise to avoid resulting in ".." path segments.
 
906
        if not os.path.isabs(path):
 
907
            raise SubmissionError("Path is not absolute")
 
908
        if any(c in path for c in "\n[]"):
 
909
            raise SubmissionError("Path must not contain '\\n', '[' or ']'")
 
910
        return os.path.normpath(path)
 
911
 
822
912
# WORKSHEETS AND EXERCISES #
823
913
 
824
914
class Exercise(Storm):
831
921
    id = Unicode(primary=True, name="identifier")
832
922
    name = Unicode()
833
923
    description = Unicode()
 
924
    _description_xhtml_cache = Unicode(name='description_xhtml_cache')
834
925
    partial = Unicode()
835
926
    solution = Unicode()
836
927
    include = Unicode()
879
970
 
880
971
        return perms
881
972
 
882
 
    def get_description(self):
883
 
        """Return the description interpreted as reStructuredText."""
884
 
        return rst(self.description)
 
973
    def _cache_description_xhtml(self, invalidate=False):
 
974
        # Don't regenerate an existing cache unless forced.
 
975
        if self._description_xhtml_cache is not None and not invalidate:
 
976
            return
 
977
 
 
978
        if self.description:
 
979
            self._description_xhtml_cache = rst(self.description)
 
980
        else:
 
981
            self._description_xhtml_cache = None
 
982
 
 
983
    @property
 
984
    def description_xhtml(self):
 
985
        """The XHTML exercise description, converted from reStructuredText."""
 
986
        self._cache_description_xhtml()
 
987
        return self._description_xhtml_cache
 
988
 
 
989
    def set_description(self, description):
 
990
        self.description = description
 
991
        self._cache_description_xhtml(invalidate=True)
885
992
 
886
993
    def delete(self):
887
994
        """Deletes the exercise, providing it has no associated worksheets."""
904
1011
    identifier = Unicode()
905
1012
    name = Unicode()
906
1013
    assessable = Bool()
 
1014
    published = Bool()
907
1015
    data = Unicode()
 
1016
    _data_xhtml_cache = Unicode(name='data_xhtml_cache')
908
1017
    seq_no = Int()
909
1018
    format = Unicode()
910
1019
 
941
1050
            WorksheetExercise.worksheet == self).remove()
942
1051
 
943
1052
    def get_permissions(self, user, config):
944
 
        # Almost the same permissions as for the offering itself
945
 
        perms = self.offering.get_permissions(user, config)
946
 
        # However, "edit" permission is derived from the "edit_worksheets"
947
 
        # permission of the offering
948
 
        if 'edit_worksheets' in perms:
 
1053
        offering_perms = self.offering.get_permissions(user, config)
 
1054
 
 
1055
        perms = set()
 
1056
 
 
1057
        # Anybody who can view an offering can view a published
 
1058
        # worksheet.
 
1059
        if 'view' in offering_perms and self.published:
 
1060
            perms.add('view')
 
1061
 
 
1062
        # Any worksheet editors can both view and edit.
 
1063
        if 'edit_worksheets' in offering_perms:
 
1064
            perms.add('view')
949
1065
            perms.add('edit')
950
 
        else:
951
 
            perms.discard('edit')
 
1066
 
952
1067
        return perms
953
1068
 
954
 
    def get_xml(self):
955
 
        """Returns the xml of this worksheet, converts from rst if required."""
956
 
        if self.format == u'rst':
957
 
            ws_xml = rst(self.data)
958
 
            return ws_xml
 
1069
    def _cache_data_xhtml(self, invalidate=False):
 
1070
        # Don't regenerate an existing cache unless forced.
 
1071
        if self._data_xhtml_cache is not None and not invalidate:
 
1072
            return
 
1073
 
 
1074
        if self.format == u'rst':
 
1075
            self._data_xhtml_cache = rst(self.data)
 
1076
        else:
 
1077
            self._data_xhtml_cache = None
 
1078
 
 
1079
    @property
 
1080
    def data_xhtml(self):
 
1081
        """The XHTML of this worksheet, converted from rST if required."""
 
1082
        # Update the rST -> XHTML cache, if required.
 
1083
        self._cache_data_xhtml()
 
1084
 
 
1085
        if self.format == u'rst':
 
1086
            return self._data_xhtml_cache
959
1087
        else:
960
1088
            return self.data
961
1089
 
 
1090
    def set_data(self, data):
 
1091
        self.data = data
 
1092
        self._cache_data_xhtml(invalidate=True)
 
1093
 
962
1094
    def delete(self):
963
1095
        """Deletes the worksheet, provided it has no attempts on any exercises.
964
1096
 
1026
1158
 
1027
1159
    def __repr__(self):
1028
1160
        return "<%s %s by %s at %s>" % (type(self).__name__,
1029
 
            self.exercise.name, self.user.login, self.date.strftime("%c"))
 
1161
            self.worksheet_exercise.exercise.name, self.user.login,
 
1162
            self.date.strftime("%c"))
1030
1163
 
1031
1164
class ExerciseAttempt(ExerciseSave):
1032
1165
    """An attempt at solving an exercise.