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

1080.1.2 by matt.giuca
New module: ivle.database. Classes and utilities for Storm ORM.
1
# IVLE - Informatics Virtual Learning Environment
2
# Copyright (C) 2007-2009 The University of Melbourne
3
#
4
# This program is free software; you can redistribute it and/or modify
5
# it under the terms of the GNU General Public License as published by
6
# the Free Software Foundation; either version 2 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU General Public License for more details.
13
#
14
# You should have received a copy of the GNU General Public License
15
# along with this program; if not, write to the Free Software
16
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
17
18
# Author: Matt Giuca, Will Grant
19
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
20
"""Database utilities and content classes.
1080.1.2 by matt.giuca
New module: ivle.database. Classes and utilities for Storm ORM.
21
22
This module provides all of the classes which map to database tables.
23
It also provides miscellaneous utility functions for database interaction.
24
"""
25
1197 by Matt Giuca
ivle.chat, ivle.database, ivle.makeuser: Replaced use of md5 library with
26
import hashlib
1080.1.15 by me at id
Give ivle.database.User {password,account}_expired attributes, and get
27
import datetime
1660 by Matt Giuca
ivle.database: Added a security check on Project.submit() that the path meets certain constraints, to avoid path injection. Fixes LP bug #522462.
28
import os
1784 by Matt Giuca
ivle.webapp.admin.subject: Abstracted code to generate Subversion URL into the database. User and ProjectGroup objects now have a get_svn_url method.
29
import urlparse
1080.1.13 by me at id
ivle.database.User: Add an authenticate() method, and a hash_password()
30
1080.1.4 by matt.giuca
ivle.database: Added User class.
31
from storm.locals import create_database, Store, Int, Unicode, DateTime, \
1080.1.27 by me at id
ivle.database.User: Add an 'active_enrolments' property, which returns a list
32
                         Reference, ReferenceSet, Bool, Storm, Desc
1165.3.45 by William Grant
Add Project.latest_submissions, which excludes superseded submissions.
33
from storm.expr import Select, Max
1099.1.242 by Nick Chadwick
Fixed a problem with exercise editor, which wasn't editing or adding
34
from storm.exceptions import NotOneError, IntegrityError
1080.1.2 by matt.giuca
New module: ivle.database. Classes and utilities for Storm ORM.
35
1099.1.220 by Nick Chadwick
Merged from trunk
36
from ivle.worksheet.rst import rst
1080.1.2 by matt.giuca
New module: ivle.database. Classes and utilities for Storm ORM.
37
1080.1.39 by Matt Giuca
ivle.database: Added __all__ to the top of the file.
38
__all__ = ['get_store',
39
            'User',
40
            'Subject', 'Semester', 'Offering', 'Enrolment',
41
            'ProjectSet', 'Project', 'ProjectGroup', 'ProjectGroupMembership',
1165.1.4 by William Grant
Add database classes for assessed, project_extension and project_submission.
42
            'Assessed', 'ProjectSubmission', 'ProjectExtension',
1080.1.59 by Matt Giuca
ivle.worksheet, ivle.database: Added/updated __all__.
43
            'Exercise', 'Worksheet', 'WorksheetExercise',
1080.1.61 by William Grant
ivle.database: Add an Offering.enrol(user) method, which enrols the user in
44
            'ExerciseSave', 'ExerciseAttempt',
1110 by William Grant
ivle-enrol now allows updating of existing enrolments. It also sets the role.
45
            'TestCase', 'TestSuite', 'TestSuiteVar'
1080.1.39 by Matt Giuca
ivle.database: Added __all__ to the top of the file.
46
        ]
47
1080.1.25 by me at id
ivle.database: Add Subject, Semester and Offering.
48
def _kwarg_init(self, **kwargs):
49
    for k,v in kwargs.items():
1080.1.46 by William Grant
ivle.database._kwarg_init: Check with hasattr() on the class, not the object,
50
        if k.startswith('_') or not hasattr(self.__class__, k):
1080.1.25 by me at id
ivle.database: Add Subject, Semester and Offering.
51
            raise TypeError("%s got an unexpected keyword argument '%s'"
1080.1.45 by William Grant
ivle.database._kwarg_init: Fix exception throwing.
52
                % (self.__class__.__name__, k))
1080.1.25 by me at id
ivle.database: Add Subject, Semester and Offering.
53
        setattr(self, k, v)
54
1201 by William Grant
ivle.database.get_store() now takes a configuration object.
55
def get_conn_string(config):
56
    """Create a Storm connection string to the IVLE database
57
58
    @param config: The IVLE configuration.
1080.1.2 by matt.giuca
New module: ivle.database. Classes and utilities for Storm ORM.
59
    """
1099.1.174 by William Grant
ivle.database.get_conn_string() now defaults to localhost:5432, rather than
60
61
    clusterstr = ''
1201 by William Grant
ivle.database.get_store() now takes a configuration object.
62
    if config['database']['username']:
63
        clusterstr += config['database']['username']
64
        if config['database']['password']:
65
            clusterstr += ':' + config['database']['password']
1099.1.174 by William Grant
ivle.database.get_conn_string() now defaults to localhost:5432, rather than
66
        clusterstr += '@'
67
1201 by William Grant
ivle.database.get_store() now takes a configuration object.
68
    host = config['database']['host'] or 'localhost'
69
    port = config['database']['port'] or 5432
1099.1.174 by William Grant
ivle.database.get_conn_string() now defaults to localhost:5432, rather than
70
71
    clusterstr += '%s:%d' % (host, port)
72
1201 by William Grant
ivle.database.get_store() now takes a configuration object.
73
    return "postgres://%s/%s" % (clusterstr, config['database']['name'])
74
75
def get_store(config):
76
    """Create a Storm store connected to the IVLE database.
77
78
    @param config: The IVLE configuration.
79
    """
80
    return Store(create_database(get_conn_string(config)))
1080.1.4 by matt.giuca
ivle.database: Added User class.
81
1080.1.39 by Matt Giuca
ivle.database: Added __all__ to the top of the file.
82
# USERS #
83
1080.1.26 by me at id
ivle.database: Add an Enrolment class, and reference(set)s between all of the
84
class User(Storm):
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
85
    """An IVLE user account."""
1080.1.4 by matt.giuca
ivle.database: Added User class.
86
    __storm_table__ = "login"
87
88
    id = Int(primary=True, name="loginid")
89
    login = Unicode()
90
    passhash = Unicode()
91
    state = Unicode()
1101 by William Grant
Privileges (apart from admin) are now offering-local, not global.
92
    admin = Bool()
1080.1.4 by matt.giuca
ivle.database: Added User class.
93
    unixid = Int()
94
    nick = Unicode()
95
    pass_exp = DateTime()
96
    acct_exp = DateTime()
97
    last_login = DateTime()
98
    svn_pass = Unicode()
99
    email = Unicode()
100
    fullname = Unicode()
101
    studentid = Unicode()
102
    settings = Unicode()
103
1080.1.25 by me at id
ivle.database: Add Subject, Semester and Offering.
104
    __init__ = _kwarg_init
1080.1.4 by matt.giuca
ivle.database: Added User class.
105
106
    def __repr__(self):
107
        return "<%s '%s'>" % (type(self).__name__, self.login)
1080.1.5 by matt.giuca
ivle.database.User: Add the missing methods from ivle.user.User.
108
1080.1.13 by me at id
ivle.database.User: Add an authenticate() method, and a hash_password()
109
    def authenticate(self, password):
110
        """Validate a given password against this user.
111
112
        Returns True if the given password matches the password hash for this
113
        User, False if it doesn't match, and None if there is no hash for the
114
        user.
115
        """
116
        if self.passhash is None:
117
            return None
118
        return self.hash_password(password) == self.passhash
119
1080.1.15 by me at id
Give ivle.database.User {password,account}_expired attributes, and get
120
    @property
1165.1.26 by William Grant
Add display_name properties to users and groups.
121
    def display_name(self):
1165.3.69 by Matt Giuca
ivle.database: User/ProjectGroup: Added 'short_name' methods so we have a
122
        """Returns the "nice name" of the user or group."""
1165.1.26 by William Grant
Add display_name properties to users and groups.
123
        return self.fullname
124
125
    @property
1165.3.69 by Matt Giuca
ivle.database: User/ProjectGroup: Added 'short_name' methods so we have a
126
    def short_name(self):
127
        """Returns the database "identifier" name of the user or group."""
128
        return self.login
129
130
    @property
1080.1.15 by me at id
Give ivle.database.User {password,account}_expired attributes, and get
131
    def password_expired(self):
1080.1.5 by matt.giuca
ivle.database.User: Add the missing methods from ivle.user.User.
132
        fieldval = self.pass_exp
1080.1.15 by me at id
Give ivle.database.User {password,account}_expired attributes, and get
133
        return fieldval is not None and datetime.datetime.now() > fieldval
134
135
    @property
136
    def account_expired(self):
1080.1.5 by matt.giuca
ivle.database.User: Add the missing methods from ivle.user.User.
137
        fieldval = self.acct_exp
1080.1.15 by me at id
Give ivle.database.User {password,account}_expired attributes, and get
138
        return fieldval is not None and datetime.datetime.now() > fieldval
1080.1.6 by matt.giuca
ivle.database.User: Added get_by_login method.
139
1099.1.121 by William Grant
Don't set req.user unless the login in the session specifies a valid user.
140
    @property
141
    def valid(self):
142
        return self.state == 'enabled' and not self.account_expired
143
1080.1.29 by me at id
ivle.database.User: Order 'enrolments' the same way as 'active_enrolments'.
144
    def _get_enrolments(self, justactive):
1080.1.27 by me at id
ivle.database.User: Add an 'active_enrolments' property, which returns a list
145
        return Store.of(self).find(Enrolment,
146
            Enrolment.user_id == self.id,
1080.1.29 by me at id
ivle.database.User: Order 'enrolments' the same way as 'active_enrolments'.
147
            (Enrolment.active == True) if justactive else True,
1080.1.27 by me at id
ivle.database.User: Add an 'active_enrolments' property, which returns a list
148
            Enrolment.offering_id == Offering.id,
149
            Offering.semester_id == Semester.id,
150
            Offering.subject_id == Subject.id).order_by(
151
                Desc(Semester.year),
152
                Desc(Semester.semester),
153
                Desc(Subject.code)
154
            )
155
1080.1.68 by William Grant
ivle.database.User: Add a write-only 'password' attribute. When set, it will
156
    def _set_password(self, password):
157
        if password is None:
158
            self.passhash = None
159
        else:
160
            self.passhash = unicode(User.hash_password(password))
161
    password = property(fset=_set_password)
162
1080.1.29 by me at id
ivle.database.User: Order 'enrolments' the same way as 'active_enrolments'.
163
    @property
1080.1.31 by me at id
ivle.database.User: Add 'subjects', an attribute containing currently
164
    def subjects(self):
165
        return Store.of(self).find(Subject,
166
            Enrolment.user_id == self.id,
167
            Enrolment.active == True,
168
            Offering.id == Enrolment.offering_id,
169
            Subject.id == Offering.subject_id).config(distinct=True)
170
1080.1.36 by William Grant
ivle.database: Add ProjectSet, Project, ProjectGroup, ProjectGroupMembership
171
    # TODO: Invitations should be listed too?
172
    def get_groups(self, offering=None):
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
173
        """Get groups of which this user is a member.
174
175
        @param offering: An optional offering to restrict the search to.
176
        """
1080.1.36 by William Grant
ivle.database: Add ProjectSet, Project, ProjectGroup, ProjectGroupMembership
177
        preds = [
178
            ProjectGroupMembership.user_id == self.id,
179
            ProjectGroup.id == ProjectGroupMembership.project_group_id,
180
        ]
181
        if offering:
182
            preds.extend([
183
                ProjectSet.offering_id == offering.id,
184
                ProjectGroup.project_set_id == ProjectSet.id,
185
            ])
186
        return Store.of(self).find(ProjectGroup, *preds)
187
188
    @property
189
    def groups(self):
190
        return self.get_groups()
191
1080.1.31 by me at id
ivle.database.User: Add 'subjects', an attribute containing currently
192
    @property
1080.1.29 by me at id
ivle.database.User: Order 'enrolments' the same way as 'active_enrolments'.
193
    def active_enrolments(self):
194
        '''A sanely ordered list of the user's active enrolments.'''
195
        return self._get_enrolments(True)
196
197
    @property
198
    def enrolments(self):
199
        '''A sanely ordered list of all of the user's enrolments.'''
200
        return self._get_enrolments(False) 
1080.1.27 by me at id
ivle.database.User: Add an 'active_enrolments' property, which returns a list
201
1165.1.11 by William Grant
Let callsites ask User.get_projects() to show inactive offerings too.
202
    def get_projects(self, offering=None, active_only=True):
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
203
        """Find projects that the user can submit.
1165.1.10 by William Grant
Add User.get_projects(), returning a list of submission targets.
204
1165.1.11 by William Grant
Let callsites ask User.get_projects() to show inactive offerings too.
205
        This will include projects for offerings in which the user is
1165.1.10 by William Grant
Add User.get_projects(), returning a list of submission targets.
206
        enrolled, as long as the project is not in a project set which has
207
        groups (ie. if maximum number of group members is 0).
208
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
209
        @param active_only: Whether to only search active offerings.
210
        @param offering: An optional offering to restrict the search to.
211
        """
1165.1.10 by William Grant
Add User.get_projects(), returning a list of submission targets.
212
        return Store.of(self).find(Project,
213
            Project.project_set_id == ProjectSet.id,
1165.1.46 by William Grant
Respect the new max_students_per_group semantics in Python.
214
            ProjectSet.max_students_per_group == None,
1165.1.10 by William Grant
Add User.get_projects(), returning a list of submission targets.
215
            ProjectSet.offering_id == Offering.id,
1165.1.11 by William Grant
Let callsites ask User.get_projects() to show inactive offerings too.
216
            (offering is None) or (Offering.id == offering.id),
1165.1.10 by William Grant
Add User.get_projects(), returning a list of submission targets.
217
            Semester.id == Offering.semester_id,
1165.1.11 by William Grant
Let callsites ask User.get_projects() to show inactive offerings too.
218
            (not active_only) or (Semester.state == u'current'),
1165.1.10 by William Grant
Add User.get_projects(), returning a list of submission targets.
219
            Enrolment.offering_id == Offering.id,
1564 by William Grant
Restrict some queries to active enrolments.
220
            Enrolment.user_id == self.id,
221
            Enrolment.active == True)
1165.1.10 by William Grant
Add User.get_projects(), returning a list of submission targets.
222
1080.1.13 by me at id
ivle.database.User: Add an authenticate() method, and a hash_password()
223
    @staticmethod
224
    def hash_password(password):
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
225
        """Hash a password with MD5."""
1197 by Matt Giuca
ivle.chat, ivle.database, ivle.makeuser: Replaced use of md5 library with
226
        return hashlib.md5(password).hexdigest()
1080.1.13 by me at id
ivle.database.User: Add an authenticate() method, and a hash_password()
227
1080.1.6 by matt.giuca
ivle.database.User: Added get_by_login method.
228
    @classmethod
229
    def get_by_login(cls, store, login):
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
230
        """Find a user in a store by login name."""
1080.1.7 by matt.giuca
The new ivle.database.User class is now used in Request and usrmgt, which
231
        return store.find(cls, cls.login == unicode(login)).one()
1080.1.25 by me at id
ivle.database: Add Subject, Semester and Offering.
232
1789 by Matt Giuca
Changed database.py get_svn_url to take a req; include the req.user.login in the Subversion URL. This allows you to check out repositories without separately supplying the IVLE URL (as Subversion won't ask for a username by default). Also removed --username= from the lecturer project view, as it's redundant now. This fixes Launchpad bug #543936.
233
    def get_svn_url(self, config, req):
1784 by Matt Giuca
ivle.webapp.admin.subject: Abstracted code to generate Subversion URL into the database. User and ProjectGroup objects now have a get_svn_url method.
234
        """Get the subversion repository URL for this user or group."""
1789 by Matt Giuca
Changed database.py get_svn_url to take a req; include the req.user.login in the Subversion URL. This allows you to check out repositories without separately supplying the IVLE URL (as Subversion won't ask for a username by default). Also removed --username= from the lecturer project view, as it's redundant now. This fixes Launchpad bug #543936.
235
        login = req.user.login
236
        url = urlparse.urlsplit(config['urls']['svn_addr'])
237
        url = urlparse.urlunsplit(url[:1] + (login+'@'+url[1],) + url[2:])
1784 by Matt Giuca
ivle.webapp.admin.subject: Abstracted code to generate Subversion URL into the database. User and ProjectGroup objects now have a get_svn_url method.
238
        path = 'users/%s' % self.login
1789 by Matt Giuca
Changed database.py get_svn_url to take a req; include the req.user.login in the Subversion URL. This allows you to check out repositories without separately supplying the IVLE URL (as Subversion won't ask for a username by default). Also removed --username= from the lecturer project view, as it's redundant now. This fixes Launchpad bug #543936.
239
        return urlparse.urljoin(url, path)
1784 by Matt Giuca
ivle.webapp.admin.subject: Abstracted code to generate Subversion URL into the database. User and ProjectGroup objects now have a get_svn_url method.
240
1544 by Matt Giuca
Added an argument 'config' to every single get_permissions method throughout the program. All calls to get_permissions pass a config. This is to allow per-site policy configurations on permissions.
241
    def get_permissions(self, user, config):
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
242
        """Determine privileges held by a user over this object.
243
244
        If the user requesting privileges is this user or an admin,
245
        they may do everything. Otherwise they may do nothing.
246
        """
1101 by William Grant
Privileges (apart from admin) are now offering-local, not global.
247
        if user and user.admin or user is self:
1294.2.137 by William Grant
Grant all users the view_public permission on Users.
248
            return set(['view_public', 'view', 'edit', 'submit_project'])
1099.1.110 by William Grant
Implement an authorization system in the new framework. This breaks the REST
249
        else:
1294.2.137 by William Grant
Grant all users the view_public permission on Users.
250
            return set(['view_public'])
1099.1.110 by William Grant
Implement an authorization system in the new framework. This breaks the REST
251
1080.1.39 by Matt Giuca
ivle.database: Added __all__ to the top of the file.
252
# SUBJECTS AND ENROLMENTS #
253
1080.1.26 by me at id
ivle.database: Add an Enrolment class, and reference(set)s between all of the
254
class Subject(Storm):
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
255
    """A subject (or course) which is run in some semesters."""
256
1080.1.25 by me at id
ivle.database: Add Subject, Semester and Offering.
257
    __storm_table__ = "subject"
258
259
    id = Int(primary=True, name="subjectid")
260
    code = Unicode(name="subj_code")
261
    name = Unicode(name="subj_name")
262
    short_name = Unicode(name="subj_short_name")
263
1080.1.26 by me at id
ivle.database: Add an Enrolment class, and reference(set)s between all of the
264
    offerings = ReferenceSet(id, 'Offering.subject_id')
265
1080.1.25 by me at id
ivle.database: Add Subject, Semester and Offering.
266
    __init__ = _kwarg_init
267
268
    def __repr__(self):
269
        return "<%s '%s'>" % (type(self).__name__, self.short_name)
270
1544 by Matt Giuca
Added an argument 'config' to every single get_permissions method throughout the program. All calls to get_permissions pass a config. This is to allow per-site policy configurations on permissions.
271
    def get_permissions(self, user, config):
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
272
        """Determine privileges held by a user over this object.
273
274
        If the user requesting privileges is an admin, they may edit.
275
        Otherwise they may only read.
276
        """
1099.1.110 by William Grant
Implement an authorization system in the new framework. This breaks the REST
277
        perms = set()
278
        if user is not None:
279
            perms.add('view')
1101 by William Grant
Privileges (apart from admin) are now offering-local, not global.
280
            if user.admin:
1099.1.141 by Nick Chadwick
Updated the exercises to be loaded from the database, not a local file.
281
                perms.add('edit')
1099.1.110 by William Grant
Implement an authorization system in the new framework. This breaks the REST
282
        return perms
283
1195.1.1 by Matt Giuca
ivle.database: Added Subject.active_offerings, which can be used by tools
284
    def active_offerings(self):
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
285
        """Find active offerings for this subject.
286
287
        Return a sequence of currently active offerings for this subject
1195.1.1 by Matt Giuca
ivle.database: Added Subject.active_offerings, which can be used by tools
288
        (offerings whose semester.state is "current"). There should be 0 or 1
289
        elements in this sequence, but it's possible there are more.
290
        """
1195.1.6 by Matt Giuca
ivle.database: Added Subject.offering_for_semester.
291
        return self.offerings.find(Offering.semester_id == Semester.id,
292
                                   Semester.state == u'current')
293
294
    def offering_for_semester(self, year, semester):
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
295
        """Get the offering for the given year/semester, or None.
296
297
        @param year: A string representation of the year.
298
        @param semester: A string representation of the semester.
299
        """
1195.1.6 by Matt Giuca
ivle.database: Added Subject.offering_for_semester.
300
        return self.offerings.find(Offering.semester_id == Semester.id,
301
                               Semester.year == unicode(year),
302
                               Semester.semester == unicode(semester)).one()
1195.1.1 by Matt Giuca
ivle.database: Added Subject.active_offerings, which can be used by tools
303
1080.1.26 by me at id
ivle.database: Add an Enrolment class, and reference(set)s between all of the
304
class Semester(Storm):
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
305
    """A semester in which subjects can be run."""
306
1080.1.25 by me at id
ivle.database: Add Subject, Semester and Offering.
307
    __storm_table__ = "semester"
308
309
    id = Int(primary=True, name="semesterid")
310
    year = Unicode()
311
    semester = Unicode()
1104 by William Grant
Replace Semester.active with Semester.state, allowing more useful state
312
    state = Unicode()
1080.1.25 by me at id
ivle.database: Add Subject, Semester and Offering.
313
1080.1.26 by me at id
ivle.database: Add an Enrolment class, and reference(set)s between all of the
314
    offerings = ReferenceSet(id, 'Offering.semester_id')
1124 by William Grant
Add Semester.enrolments.
315
    enrolments = ReferenceSet(id,
316
                              'Offering.semester_id',
317
                              'Offering.id',
318
                              'Enrolment.offering_id')
1080.1.26 by me at id
ivle.database: Add an Enrolment class, and reference(set)s between all of the
319
1080.1.25 by me at id
ivle.database: Add Subject, Semester and Offering.
320
    __init__ = _kwarg_init
321
322
    def __repr__(self):
323
        return "<%s %s/%s>" % (type(self).__name__, self.year, self.semester)
324
1080.1.26 by me at id
ivle.database: Add an Enrolment class, and reference(set)s between all of the
325
class Offering(Storm):
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
326
    """An offering of a subject in a particular semester."""
327
1080.1.25 by me at id
ivle.database: Add Subject, Semester and Offering.
328
    __storm_table__ = "offering"
329
330
    id = Int(primary=True, name="offeringid")
331
    subject_id = Int(name="subject")
332
    subject = Reference(subject_id, Subject.id)
333
    semester_id = Int(name="semesterid")
334
    semester = Reference(semester_id, Semester.id)
1451.1.2 by William Grant
Move Subject.url to Offering, and add Offering.description. Show these on the offering index.
335
    description = Unicode()
336
    url = Unicode()
1695.1.2 by William Grant
Integrate new columns into the model.
337
    show_worksheet_marks = Bool()
338
    worksheet_cutoff = DateTime()
1080.1.25 by me at id
ivle.database: Add Subject, Semester and Offering.
339
    groups_student_permissions = Unicode()
340
1080.1.26 by me at id
ivle.database: Add an Enrolment class, and reference(set)s between all of the
341
    enrolments = ReferenceSet(id, 'Enrolment.offering_id')
1080.1.79 by William Grant
ivle.database.Offering: Add a members ReferenceSet.
342
    members = ReferenceSet(id,
343
                           'Enrolment.offering_id',
344
                           'Enrolment.user_id',
345
                           'User.id')
1080.1.76 by William Grant
ivle.database.Offering: Add project_sets referenceset.
346
    project_sets = ReferenceSet(id, 'ProjectSet.offering_id')
1442.1.7 by William Grant
Add Offering.projects.
347
    projects = ReferenceSet(id,
348
                            'ProjectSet.offering_id',
349
                            'ProjectSet.id',
350
                            'Project.project_set_id')
1080.1.26 by me at id
ivle.database: Add an Enrolment class, and reference(set)s between all of the
351
1099.1.180 by Nick Chadwick
This commit changes the tutorial service, which now almost exclusively
352
    worksheets = ReferenceSet(id, 
353
        'Worksheet.offering_id', 
1099.1.212 by Nick Chadwick
Added a new page to display exercises. This will then be modified to
354
        order_by="seq_no"
1099.1.180 by Nick Chadwick
This commit changes the tutorial service, which now almost exclusively
355
    )
1099.1.114 by Nick Chadwick
Modified the database so that exercises are now stored in the database, rather
356
1080.1.25 by me at id
ivle.database: Add Subject, Semester and Offering.
357
    __init__ = _kwarg_init
358
359
    def __repr__(self):
360
        return "<%s %r in %r>" % (type(self).__name__, self.subject,
361
                                  self.semester)
1080.1.26 by me at id
ivle.database: Add an Enrolment class, and reference(set)s between all of the
362
1110 by William Grant
ivle-enrol now allows updating of existing enrolments. It also sets the role.
363
    def enrol(self, user, role=u'student'):
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
364
        """Enrol a user in this offering.
365
366
        Enrolments handle both the staff and student cases. The role controls
367
        the privileges granted by this enrolment.
368
        """
1110 by William Grant
ivle-enrol now allows updating of existing enrolments. It also sets the role.
369
        enrolment = Store.of(self).find(Enrolment,
1080.1.61 by William Grant
ivle.database: Add an Offering.enrol(user) method, which enrols the user in
370
                               Enrolment.user_id == user.id,
1110 by William Grant
ivle-enrol now allows updating of existing enrolments. It also sets the role.
371
                               Enrolment.offering_id == self.id).one()
372
373
        if enrolment is None:
374
            enrolment = Enrolment(user=user, offering=self)
375
            self.enrolments.add(enrolment)
376
377
        enrolment.active = True
378
        enrolment.role = role
1080.1.61 by William Grant
ivle.database: Add an Offering.enrol(user) method, which enrols the user in
379
1132 by William Grant
Add Offering.unenrol(), to unenrol a user from an offering.
380
    def unenrol(self, user):
381
        '''Unenrol a user from this offering.'''
382
        enrolment = Store.of(self).find(Enrolment,
383
                               Enrolment.user_id == user.id,
384
                               Enrolment.offering_id == self.id).one()
385
        Store.of(enrolment).remove(enrolment)
386
1544 by Matt Giuca
Added an argument 'config' to every single get_permissions method throughout the program. All calls to get_permissions pass a config. This is to allow per-site policy configurations on permissions.
387
    def get_permissions(self, user, config):
1099.1.141 by Nick Chadwick
Updated the exercises to be loaded from the database, not a local file.
388
        perms = set()
389
        if user is not None:
1131 by William Grant
Offerings now give 'view' only to user enrolled in them. 'edit' is granted
390
            enrolment = self.get_enrolment(user)
391
            if enrolment or user.admin:
392
                perms.add('view')
1558 by William Grant
Allow tutors to manage groups.
393
            if enrolment and enrolment.role == u'tutor':
1556 by William Grant
Allow tutors to view project submissions.
394
                perms.add('view_project_submissions')
1547 by Matt Giuca
Added two new configuration options under [policy], for deciding whether tutors can enrol students and edit worksheets. The permissions set in database.py reflects these config options. Documented. This entirely fixes Launchpad Bug #520232 and Bug #493945, regarding tutors having too much power.
395
                # Site-specific policy on the role of tutors
396
                if config['policy']['tutors_can_enrol_students']:
397
                    perms.add('enrol')
398
                    perms.add('enrol_student')
399
                if config['policy']['tutors_can_edit_worksheets']:
400
                    perms.add('edit_worksheets')
1558 by William Grant
Allow tutors to manage groups.
401
                if config['policy']['tutors_can_admin_groups']:
402
                    perms.add('admin_groups')
1547 by Matt Giuca
Added two new configuration options under [policy], for deciding whether tutors can enrol students and edit worksheets. The permissions set in database.py reflects these config options. Documented. This entirely fixes Launchpad Bug #520232 and Bug #493945, regarding tutors having too much power.
403
            if (enrolment and enrolment.role in (u'lecturer')) or user.admin:
1556 by William Grant
Allow tutors to view project submissions.
404
                perms.add('view_project_submissions')
1558 by William Grant
Allow tutors to manage groups.
405
                perms.add('admin_groups')
1542 by Matt Giuca
Tutors can now (once again) edit worksheets.
406
                perms.add('edit_worksheets')
1689.1.1 by Matt Giuca
Added permission view_worksheet_marks to lecturer/admin roles.
407
                perms.add('view_worksheet_marks')
1542 by Matt Giuca
Tutors can now (once again) edit worksheets.
408
                perms.add('edit')           # Can edit projects & details
1377 by Matt Giuca
database: Added finer-grained enrol permissions on offerings.
409
                perms.add('enrol')          # Can see enrolment screen at all
410
                perms.add('enrol_student')  # Can enrol students
411
                perms.add('enrol_tutor')    # Can enrol tutors
412
            if user.admin:
413
                perms.add('enrol_lecturer') # Can enrol lecturers
1099.1.141 by Nick Chadwick
Updated the exercises to be loaded from the database, not a local file.
414
        return perms
415
1129 by William Grant
Move the group admin view to per-offering.
416
    def get_enrolment(self, user):
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
417
        """Find the user's enrolment in this offering."""
1129 by William Grant
Move the group admin view to per-offering.
418
        try:
419
            enrolment = self.enrolments.find(user=user).one()
420
        except NotOneError:
421
            enrolment = None
422
423
        return enrolment
424
1165.3.42 by William Grant
Fix ProjectView's total assigned count by dealing with ResultSets only.
425
    def get_members_by_role(self, role):
426
        return Store.of(self).find(User,
427
                Enrolment.user_id == User.id,
428
                Enrolment.offering_id == self.id,
429
                Enrolment.role == role
1364 by Matt Giuca
database: Offering.get_members_by_role now sorts the data.
430
                ).order_by(User.login)
1165.3.42 by William Grant
Fix ProjectView's total assigned count by dealing with ResultSets only.
431
1165.3.43 by William Grant
Turn the new database methods into properties where appropriate.
432
    @property
433
    def students(self):
1165.3.42 by William Grant
Fix ProjectView's total assigned count by dealing with ResultSets only.
434
        return self.get_members_by_role(u'student')
1165.4.2 by Nick Chadwick
Added a get_assigned method to project sets, to find out which objects
435
1442.1.23 by William Grant
Add Offering.get_open_projects_for_user.
436
    def get_open_projects_for_user(self, user):
437
        """Find all projects currently open to submissions by a user."""
438
        # XXX: Respect extensions.
439
        return self.projects.find(Project.deadline > datetime.datetime.now())
440
1737 by Matt Giuca
Offering page: Display the worksheet cutoff to students, if there is one.
441
    def has_worksheet_cutoff_passed(self, user):
442
        """Check whether the worksheet cutoff has passed.
443
        A user is required, in case we support extensions.
444
        """
445
        if self.worksheet_cutoff is None:
446
            return False
447
        else:
448
            return self.worksheet_cutoff < datetime.datetime.now()
449
1603 by William Grant
Add UI to clone worksheets between offerings -- replacing ivle-cloneworksheets.
450
    def clone_worksheets(self, source):
451
        """Clone all worksheets from the specified source to this offering."""
452
        import ivle.worksheet.utils
453
        for worksheet in source.worksheets:
454
            newws = Worksheet()
455
            newws.seq_no = worksheet.seq_no
456
            newws.identifier = worksheet.identifier
457
            newws.name = worksheet.name
458
            newws.assessable = worksheet.assessable
1695.1.2 by William Grant
Integrate new columns into the model.
459
            newws.published = worksheet.published
1603 by William Grant
Add UI to clone worksheets between offerings -- replacing ivle-cloneworksheets.
460
            newws.data = worksheet.data
461
            newws.format = worksheet.format
462
            newws.offering = self
463
            Store.of(self).add(newws)
464
            ivle.worksheet.utils.update_exerciselist(newws)
465
466
1080.1.26 by me at id
ivle.database: Add an Enrolment class, and reference(set)s between all of the
467
class Enrolment(Storm):
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
468
    """An enrolment of a user in an offering.
469
470
    This represents the roles of both staff and students.
471
    """
472
1080.1.26 by me at id
ivle.database: Add an Enrolment class, and reference(set)s between all of the
473
    __storm_table__ = "enrolment"
474
    __storm_primary__ = "user_id", "offering_id"
475
476
    user_id = Int(name="loginid")
477
    user = Reference(user_id, User.id)
478
    offering_id = Int(name="offeringid")
479
    offering = Reference(offering_id, Offering.id)
1101 by William Grant
Privileges (apart from admin) are now offering-local, not global.
480
    role = Unicode()
1080.1.26 by me at id
ivle.database: Add an Enrolment class, and reference(set)s between all of the
481
    notes = Unicode()
482
    active = Bool()
483
1080.1.81 by William Grant
ivle.database.Enrolment: Add a groups attribute, containing groups of which
484
    @property
485
    def groups(self):
486
        return Store.of(self).find(ProjectGroup,
487
                ProjectSet.offering_id == self.offering.id,
488
                ProjectGroup.project_set_id == ProjectSet.id,
489
                ProjectGroupMembership.project_group_id == ProjectGroup.id,
490
                ProjectGroupMembership.user_id == self.user.id)
491
1080.1.26 by me at id
ivle.database: Add an Enrolment class, and reference(set)s between all of the
492
    __init__ = _kwarg_init
493
494
    def __repr__(self):
495
        return "<%s %r in %r>" % (type(self).__name__, self.user,
496
                                  self.offering)
1080.1.36 by William Grant
ivle.database: Add ProjectSet, Project, ProjectGroup, ProjectGroupMembership
497
1613 by William Grant
Add UI to edit/delete enrolments.
498
    def get_permissions(self, user, config):
499
        # A user can edit any enrolment that they could have created.
500
        perms = set()
501
        if ('enrol_' + str(self.role)) in self.offering.get_permissions(
502
            user, config):
503
            perms.add('edit')
504
        return perms
505
506
    def delete(self):
507
        """Delete this enrolment."""
508
        Store.of(self).remove(self)
509
510
1080.1.39 by Matt Giuca
ivle.database: Added __all__ to the top of the file.
511
# PROJECTS #
512
1080.1.36 by William Grant
ivle.database: Add ProjectSet, Project, ProjectGroup, ProjectGroupMembership
513
class ProjectSet(Storm):
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
514
    """A set of projects that share common groups.
515
516
    Each student project group is attached to a project set. The group is
517
    valid for all projects in the group's set.
518
    """
519
1080.1.36 by William Grant
ivle.database: Add ProjectSet, Project, ProjectGroup, ProjectGroupMembership
520
    __storm_table__ = "project_set"
521
522
    id = Int(name="projectsetid", primary=True)
523
    offering_id = Int(name="offeringid")
524
    offering = Reference(offering_id, Offering.id)
525
    max_students_per_group = Int()
526
1080.1.77 by William Grant
ivle.database.ProjectSet: Add projects and project_groups referencesets.
527
    projects = ReferenceSet(id, 'Project.project_set_id')
528
    project_groups = ReferenceSet(id, 'ProjectGroup.project_set_id')
529
1080.1.36 by William Grant
ivle.database: Add ProjectSet, Project, ProjectGroup, ProjectGroupMembership
530
    __init__ = _kwarg_init
531
532
    def __repr__(self):
533
        return "<%s %d in %r>" % (type(self).__name__, self.id,
534
                                  self.offering)
535
1544 by Matt Giuca
Added an argument 'config' to every single get_permissions method throughout the program. All calls to get_permissions pass a config. This is to allow per-site policy configurations on permissions.
536
    def get_permissions(self, user, config):
537
        return self.offering.get_permissions(user, config)
1165.3.2 by Nick Chadwick
Created a new view for IVLE, allowing lecturers and tutors to
538
1442.1.12 by William Grant
Display either 'solo' or the list of other group members on the offering project listing.
539
    def get_groups_for_user(self, user):
540
        """List all groups in this offering of which the user is a member."""
541
        assert self.is_group
542
        return Store.of(self).find(
543
            ProjectGroup,
544
            ProjectGroupMembership.user_id == user.id,
545
            ProjectGroupMembership.project_group_id == ProjectGroup.id,
546
            ProjectGroup.project_set_id == self.id)
547
1442.1.15 by William Grant
Show the group name in the case of a group project.
548
    def get_submission_principal(self, user):
549
        """Get the principal on behalf of which the user can submit.
1442.1.12 by William Grant
Display either 'solo' or the list of other group members on the offering project listing.
550
1442.1.15 by William Grant
Show the group name in the case of a group project.
551
        If this is a solo project set, the given user is returned. If
552
        the user is a member of exactly one group, all the group is
553
        returned. Otherwise, None is returned.
1442.1.12 by William Grant
Display either 'solo' or the list of other group members on the offering project listing.
554
        """
555
        if self.is_group:
556
            groups = self.get_groups_for_user(user)
557
            if groups.count() == 1:
1442.1.15 by William Grant
Show the group name in the case of a group project.
558
                return groups.one()
1442.1.12 by William Grant
Display either 'solo' or the list of other group members on the offering project listing.
559
            else:
560
                return None
561
        else:
1442.1.15 by William Grant
Show the group name in the case of a group project.
562
            return user
1442.1.12 by William Grant
Display either 'solo' or the list of other group members on the offering project listing.
563
1165.3.43 by William Grant
Turn the new database methods into properties where appropriate.
564
    @property
1375.1.3 by William Grant
Add ProjectSet.is_group, a property determining whether it is a group or solo set.
565
    def is_group(self):
566
        return self.max_students_per_group is not None
567
568
    @property
1165.3.43 by William Grant
Turn the new database methods into properties where appropriate.
569
    def assigned(self):
570
        """Get the entities (groups or users) assigned to submit this project.
571
572
        This will be a Storm ResultSet.
573
        """
574
        #If its a solo project, return everyone in offering
1375.1.3 by William Grant
Add ProjectSet.is_group, a property determining whether it is a group or solo set.
575
        if self.is_group:
576
            return self.project_groups
577
        else:
1165.3.43 by William Grant
Turn the new database methods into properties where appropriate.
578
            return self.offering.students
1165.4.2 by Nick Chadwick
Added a get_assigned method to project sets, to find out which objects
579
1513 by Matt Giuca
submit: Now produces a graceful user error if submitting a project after the deadline (forbidden by HTML, but still possible and not due to malice). Previously resulted in an internal server error.
580
class DeadlinePassed(Exception):
581
    """An exception indicating that a project cannot be submitted because the
582
    deadline has passed."""
583
    def __init__(self):
584
        pass
585
    def __str__(self):
586
        return "The project deadline has passed"
587
1080.1.36 by William Grant
ivle.database: Add ProjectSet, Project, ProjectGroup, ProjectGroupMembership
588
class Project(Storm):
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
589
    """A student project for which submissions can be made."""
590
1080.1.36 by William Grant
ivle.database: Add ProjectSet, Project, ProjectGroup, ProjectGroupMembership
591
    __storm_table__ = "project"
592
593
    id = Int(name="projectid", primary=True)
1165.1.4 by William Grant
Add database classes for assessed, project_extension and project_submission.
594
    name = Unicode()
595
    short_name = Unicode()
1080.1.36 by William Grant
ivle.database: Add ProjectSet, Project, ProjectGroup, ProjectGroupMembership
596
    synopsis = Unicode()
597
    url = Unicode()
598
    project_set_id = Int(name="projectsetid")
599
    project_set = Reference(project_set_id, ProjectSet.id)
600
    deadline = DateTime()
601
1165.1.5 by William Grant
Add relevant ReferenceSets to Project and Assessed.
602
    assesseds = ReferenceSet(id, 'Assessed.project_id')
603
    submissions = ReferenceSet(id,
604
                               'Assessed.project_id',
605
                               'Assessed.id',
606
                               'ProjectSubmission.assessed_id')
607
1080.1.36 by William Grant
ivle.database: Add ProjectSet, Project, ProjectGroup, ProjectGroupMembership
608
    __init__ = _kwarg_init
609
610
    def __repr__(self):
1165.1.4 by William Grant
Add database classes for assessed, project_extension and project_submission.
611
        return "<%s '%s' in %r>" % (type(self).__name__, self.short_name,
1080.1.36 by William Grant
ivle.database: Add ProjectSet, Project, ProjectGroup, ProjectGroupMembership
612
                                  self.project_set.offering)
613
1510 by Matt Giuca
All checks if a project deadline has passed call Project.has_deadline_passed, rather than manually checking with the current time. Important if the abstracted function changes to respect extensions.
614
    def can_submit(self, principal, user):
1165.1.19 by William Grant
Add Project.submit(), to create a submission for the principal, path and rev.
615
        return (self in principal.get_projects() and
1510 by Matt Giuca
All checks if a project deadline has passed call Project.has_deadline_passed, rather than manually checking with the current time. Important if the abstracted function changes to respect extensions.
616
                not self.has_deadline_passed(user))
1165.1.19 by William Grant
Add Project.submit(), to create a submission for the principal, path and rev.
617
1165.1.42 by William Grant
Record who submitted each submission.
618
    def submit(self, principal, path, revision, who):
619
        """Submit a Subversion path and revision to a project.
620
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
621
        @param principal: The owner of the Subversion repository, and the
622
                          entity on behalf of whom the submission is being made
623
        @param path: A path within that repository to submit.
624
        @param revision: The revision of that path to submit.
625
        @param who: The user who is actually making the submission.
1165.1.42 by William Grant
Record who submitted each submission.
626
        """
627
1510 by Matt Giuca
All checks if a project deadline has passed call Project.has_deadline_passed, rather than manually checking with the current time. Important if the abstracted function changes to respect extensions.
628
        if not self.can_submit(principal, who):
1513 by Matt Giuca
submit: Now produces a graceful user error if submitting a project after the deadline (forbidden by HTML, but still possible and not due to malice). Previously resulted in an internal server error.
629
            raise DeadlinePassed()
1165.1.19 by William Grant
Add Project.submit(), to create a submission for the principal, path and rev.
630
631
        a = Assessed.get(Store.of(self), principal, self)
632
        ps = ProjectSubmission()
1660 by Matt Giuca
ivle.database: Added a security check on Project.submit() that the path meets certain constraints, to avoid path injection. Fixes LP bug #522462.
633
        # Raise SubmissionError if the path is illegal
634
        ps.path = ProjectSubmission.test_and_normalise_path(path)
1165.1.19 by William Grant
Add Project.submit(), to create a submission for the principal, path and rev.
635
        ps.revision = revision
636
        ps.date_submitted = datetime.datetime.now()
637
        ps.assessed = a
1165.1.42 by William Grant
Record who submitted each submission.
638
        ps.submitter = who
1165.1.19 by William Grant
Add Project.submit(), to create a submission for the principal, path and rev.
639
640
        return ps
641
1544 by Matt Giuca
Added an argument 'config' to every single get_permissions method throughout the program. All calls to get_permissions pass a config. This is to allow per-site policy configurations on permissions.
642
    def get_permissions(self, user, config):
643
        return self.project_set.offering.get_permissions(user, config)
1165.3.2 by Nick Chadwick
Created a new view for IVLE, allowing lecturers and tutors to
644
1165.3.45 by William Grant
Add Project.latest_submissions, which excludes superseded submissions.
645
    @property
646
    def latest_submissions(self):
647
        """Return the latest submission for each Assessed."""
648
        return Store.of(self).find(ProjectSubmission,
649
            Assessed.project_id == self.id,
650
            ProjectSubmission.assessed_id == Assessed.id,
651
            ProjectSubmission.date_submitted == Select(
652
                    Max(ProjectSubmission.date_submitted),
653
                    ProjectSubmission.assessed_id == Assessed.id,
654
                    tables=ProjectSubmission
655
            )
656
        )
657
1442.1.13 by William Grant
Reduce the opacity of projects that are closed.
658
    def has_deadline_passed(self, user):
659
        """Check whether the deadline has passed."""
660
        # XXX: Need to respect extensions.
661
        return self.deadline < datetime.datetime.now()
662
1442.1.19 by William Grant
Add Project.get_submissions_for_principal, a helper to retrieve the submission ResultSet of submissions by a user or group.
663
    def get_submissions_for_principal(self, principal):
664
        """Fetch a ResultSet of all submissions by a particular principal."""
665
        assessed = Assessed.get(Store.of(self), principal, self)
666
        if assessed is None:
667
            return
668
        return assessed.submissions
669
1710.1.19 by Matt Giuca
Added project delete view, and ability to delete projects in the database. Currently unlinked.
670
    @property
671
    def can_delete(self):
672
        """Can only delete if there are no submissions."""
673
        return self.submissions.count() == 0
1442.1.19 by William Grant
Add Project.get_submissions_for_principal, a helper to retrieve the submission ResultSet of submissions by a user or group.
674
1710.1.19 by Matt Giuca
Added project delete view, and ability to delete projects in the database. Currently unlinked.
675
    def delete(self):
676
        """Delete the project. Fails if can_delete is False."""
677
        if not self.can_delete:
678
            raise IntegrityError()
679
        for assessed in self.assesseds:
680
            assessed.delete()
681
        Store.of(self).remove(self)
1165.1.19 by William Grant
Add Project.submit(), to create a submission for the principal, path and rev.
682
1080.1.36 by William Grant
ivle.database: Add ProjectSet, Project, ProjectGroup, ProjectGroupMembership
683
class ProjectGroup(Storm):
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
684
    """A group of students working together on a project."""
685
1080.1.36 by William Grant
ivle.database: Add ProjectSet, Project, ProjectGroup, ProjectGroupMembership
686
    __storm_table__ = "project_group"
687
688
    id = Int(name="groupid", primary=True)
689
    name = Unicode(name="groupnm")
690
    project_set_id = Int(name="projectsetid")
691
    project_set = Reference(project_set_id, ProjectSet.id)
692
    nick = Unicode()
693
    created_by_id = Int(name="createdby")
694
    created_by = Reference(created_by_id, User.id)
695
    epoch = DateTime()
696
1080.1.78 by William Grant
ivle.database.ProjectGroup.members: Use a ReferenceSet.
697
    members = ReferenceSet(id,
698
                           "ProjectGroupMembership.project_group_id",
699
                           "ProjectGroupMembership.user_id",
700
                           "User.id")
701
1080.1.36 by William Grant
ivle.database: Add ProjectSet, Project, ProjectGroup, ProjectGroupMembership
702
    __init__ = _kwarg_init
703
704
    def __repr__(self):
705
        return "<%s %s in %r>" % (type(self).__name__, self.name,
706
                                  self.project_set.offering)
707
1165.1.26 by William Grant
Add display_name properties to users and groups.
708
    @property
709
    def display_name(self):
1165.3.69 by Matt Giuca
ivle.database: User/ProjectGroup: Added 'short_name' methods so we have a
710
        """Returns the "nice name" of the user or group."""
711
        return self.nick
712
713
    @property
714
    def short_name(self):
715
        """Returns the database "identifier" name of the user or group."""
1165.3.67 by William Grant
Don't show the group display name for now; it's not settable.
716
        return self.name
1165.1.26 by William Grant
Add display_name properties to users and groups.
717
1165.1.12 by William Grant
Implement ProjectGroup.get_projects(), with identical interface.
718
    def get_projects(self, offering=None, active_only=True):
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
719
        '''Find projects that the group can submit.
1165.1.12 by William Grant
Implement ProjectGroup.get_projects(), with identical interface.
720
721
        This will include projects in the project set which owns this group,
722
        unless the project set disallows groups (in which case none will be
723
        returned).
724
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
725
        @param active_only: Whether to only search active offerings.
726
        @param offering: An optional offering to restrict the search to.
1165.1.12 by William Grant
Implement ProjectGroup.get_projects(), with identical interface.
727
        '''
728
        return Store.of(self).find(Project,
729
            Project.project_set_id == ProjectSet.id,
730
            ProjectSet.id == self.project_set.id,
1165.1.46 by William Grant
Respect the new max_students_per_group semantics in Python.
731
            ProjectSet.max_students_per_group != None,
1165.1.12 by William Grant
Implement ProjectGroup.get_projects(), with identical interface.
732
            ProjectSet.offering_id == Offering.id,
733
            (offering is None) or (Offering.id == offering.id),
734
            Semester.id == Offering.semester_id,
735
            (not active_only) or (Semester.state == u'current'))
736
1789 by Matt Giuca
Changed database.py get_svn_url to take a req; include the req.user.login in the Subversion URL. This allows you to check out repositories without separately supplying the IVLE URL (as Subversion won't ask for a username by default). Also removed --username= from the lecturer project view, as it's redundant now. This fixes Launchpad bug #543936.
737
    def get_svn_url(self, config, req):
1784 by Matt Giuca
ivle.webapp.admin.subject: Abstracted code to generate Subversion URL into the database. User and ProjectGroup objects now have a get_svn_url method.
738
        """Get the subversion repository URL for this user or group."""
1789 by Matt Giuca
Changed database.py get_svn_url to take a req; include the req.user.login in the Subversion URL. This allows you to check out repositories without separately supplying the IVLE URL (as Subversion won't ask for a username by default). Also removed --username= from the lecturer project view, as it's redundant now. This fixes Launchpad bug #543936.
739
        login = req.user.login
740
        url = urlparse.urlsplit(config['urls']['svn_addr'])
741
        url = urlparse.urlunsplit(url[:1] + (login+'@'+url[1],) + url[2:])
1784 by Matt Giuca
ivle.webapp.admin.subject: Abstracted code to generate Subversion URL into the database. User and ProjectGroup objects now have a get_svn_url method.
742
        path = 'groups/%s_%s_%s_%s' % (
743
                self.project_set.offering.subject.short_name,
744
                self.project_set.offering.semester.year,
745
                self.project_set.offering.semester.semester,
746
                self.name
747
                )
1789 by Matt Giuca
Changed database.py get_svn_url to take a req; include the req.user.login in the Subversion URL. This allows you to check out repositories without separately supplying the IVLE URL (as Subversion won't ask for a username by default). Also removed --username= from the lecturer project view, as it's redundant now. This fixes Launchpad bug #543936.
748
        return urlparse.urljoin(url, path)
1165.1.12 by William Grant
Implement ProjectGroup.get_projects(), with identical interface.
749
1544 by Matt Giuca
Added an argument 'config' to every single get_permissions method throughout the program. All calls to get_permissions pass a config. This is to allow per-site policy configurations on permissions.
750
    def get_permissions(self, user, config):
1165.1.7 by William Grant
Grant submit_project on users to themselves, and on groups to their members.
751
        if user.admin or user in self.members:
752
            return set(['submit_project'])
753
        else:
754
            return set()
755
1080.1.36 by William Grant
ivle.database: Add ProjectSet, Project, ProjectGroup, ProjectGroupMembership
756
class ProjectGroupMembership(Storm):
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
757
    """A student's membership in a project group."""
758
1080.1.36 by William Grant
ivle.database: Add ProjectSet, Project, ProjectGroup, ProjectGroupMembership
759
    __storm_table__ = "group_member"
760
    __storm_primary__ = "user_id", "project_group_id"
761
762
    user_id = Int(name="loginid")
763
    user = Reference(user_id, User.id)
764
    project_group_id = Int(name="groupid")
765
    project_group = Reference(project_group_id, ProjectGroup.id)
766
767
    __init__ = _kwarg_init
768
769
    def __repr__(self):
770
        return "<%s %r in %r>" % (type(self).__name__, self.user,
771
                                  self.project_group)
772
1165.1.4 by William Grant
Add database classes for assessed, project_extension and project_submission.
773
class Assessed(Storm):
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
774
    """A composite of a user or group combined with a project.
775
776
    Each project submission and extension refers to an Assessed. It is the
777
    sole specifier of the repository and project.
778
    """
779
1165.1.4 by William Grant
Add database classes for assessed, project_extension and project_submission.
780
    __storm_table__ = "assessed"
781
782
    id = Int(name="assessedid", primary=True)
783
    user_id = Int(name="loginid")
784
    user = Reference(user_id, User.id)
785
    project_group_id = Int(name="groupid")
786
    project_group = Reference(project_group_id, ProjectGroup.id)
787
788
    project_id = Int(name="projectid")
789
    project = Reference(project_id, Project.id)
790
1165.1.5 by William Grant
Add relevant ReferenceSets to Project and Assessed.
791
    extensions = ReferenceSet(id, 'ProjectExtension.assessed_id')
1442.1.18 by William Grant
Order Assessed.submissions by date_submitted.
792
    submissions = ReferenceSet(
793
        id, 'ProjectSubmission.assessed_id', order_by='date_submitted')
1165.1.5 by William Grant
Add relevant ReferenceSets to Project and Assessed.
794
1165.1.4 by William Grant
Add database classes for assessed, project_extension and project_submission.
795
    def __repr__(self):
796
        return "<%s %r in %r>" % (type(self).__name__,
797
            self.user or self.project_group, self.project)
798
1165.3.12 by William Grant
Add an Assessed.principal property.
799
    @property
1165.3.49 by Matt Giuca
ivle.database.Assessed: Added property is_group.
800
    def is_group(self):
801
        """True if the Assessed is a group, False if it is a user."""
802
        return self.project_group is not None
803
804
    @property
1165.3.12 by William Grant
Add an Assessed.principal property.
805
    def principal(self):
806
        return self.project_group or self.user
807
1522 by Matt Giuca
Subject page: Added Verify links on the subject pages to any projects with
808
    @property
809
    def checkout_location(self):
810
        """Returns the location of the Subversion workspace for this piece of
811
        assessment, relative to each group member's home directory."""
812
        subjectname = self.project.project_set.offering.subject.short_name
813
        if self.is_group:
814
            checkout_dir_name = self.principal.short_name
815
        else:
816
            checkout_dir_name = "mywork"
817
        return subjectname + "/" + checkout_dir_name
818
1165.1.18 by William Grant
Add a method to retrieve or create an Assessed given a principal and project.
819
    @classmethod
820
    def get(cls, store, principal, project):
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
821
        """Find or create an Assessed for the given user or group and project.
822
823
        @param principal: The user or group.
824
        @param project: The project.
825
        """
1165.1.18 by William Grant
Add a method to retrieve or create an Assessed given a principal and project.
826
        t = type(principal)
827
        if t not in (User, ProjectGroup):
828
            raise AssertionError('principal must be User or ProjectGroup')
829
830
        a = store.find(cls,
831
            (t is User) or (cls.project_group_id == principal.id),
832
            (t is ProjectGroup) or (cls.user_id == principal.id),
1445.1.2 by William Grant
Ensure that Assessed.get restricts the search to the requested project.
833
            cls.project_id == project.id).one()
1165.1.18 by William Grant
Add a method to retrieve or create an Assessed given a principal and project.
834
835
        if a is None:
836
            a = cls()
837
            if t is User:
838
                a.user = principal
839
            else:
840
                a.project_group = principal
841
            a.project = project
842
            store.add(a)
843
844
        return a
845
1710.1.19 by Matt Giuca
Added project delete view, and ability to delete projects in the database. Currently unlinked.
846
    def delete(self):
847
        """Delete the assessed. Fails if there are any submissions. Deletes
848
        extensions."""
849
        if self.submissions.count() > 0:
850
            raise IntegrityError()
851
        for extension in self.extensions:
852
            extension.delete()
853
        Store.of(self).remove(self)
1165.1.18 by William Grant
Add a method to retrieve or create an Assessed given a principal and project.
854
1165.1.4 by William Grant
Add database classes for assessed, project_extension and project_submission.
855
class ProjectExtension(Storm):
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
856
    """An extension granted to a user or group on a particular project.
857
858
    The user or group and project are specified by the Assessed.
859
    """
860
1165.1.4 by William Grant
Add database classes for assessed, project_extension and project_submission.
861
    __storm_table__ = "project_extension"
862
863
    id = Int(name="extensionid", primary=True)
864
    assessed_id = Int(name="assessedid")
865
    assessed = Reference(assessed_id, Assessed.id)
866
    deadline = DateTime()
867
    approver_id = Int(name="approver")
868
    approver = Reference(approver_id, User.id)
869
    notes = Unicode()
870
1710.1.19 by Matt Giuca
Added project delete view, and ability to delete projects in the database. Currently unlinked.
871
    def delete(self):
872
        """Delete the extension."""
873
        Store.of(self).remove(self)
874
1660 by Matt Giuca
ivle.database: Added a security check on Project.submit() that the path meets certain constraints, to avoid path injection. Fixes LP bug #522462.
875
class SubmissionError(Exception):
876
    """Denotes a validation error during submission."""
877
    pass
878
1165.1.4 by William Grant
Add database classes for assessed, project_extension and project_submission.
879
class ProjectSubmission(Storm):
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
880
    """A submission from a user or group repository to a particular project.
881
882
    The content of a submission is a single path and revision inside a
883
    repository. The repository is that owned by the submission's user and
884
    group, while the path and revision are explicit.
885
886
    The user or group and project are specified by the Assessed.
887
    """
888
1165.1.4 by William Grant
Add database classes for assessed, project_extension and project_submission.
889
    __storm_table__ = "project_submission"
890
891
    id = Int(name="submissionid", primary=True)
892
    assessed_id = Int(name="assessedid")
893
    assessed = Reference(assessed_id, Assessed.id)
894
    path = Unicode()
895
    revision = Int()
1165.1.42 by William Grant
Record who submitted each submission.
896
    submitter_id = Int(name="submitter")
897
    submitter = Reference(submitter_id, User.id)
1165.1.4 by William Grant
Add database classes for assessed, project_extension and project_submission.
898
    date_submitted = DateTime()
899
1522 by Matt Giuca
Subject page: Added Verify links on the subject pages to any projects with
900
    def get_verify_url(self, user):
901
        """Get the URL for verifying this submission, within the account of
902
        the given user."""
903
        # If this is a solo project, then self.path will be prefixed with the
904
        # subject name. Remove the first path segment.
905
        submitpath = self.path[1:] if self.path[:1] == '/' else self.path
906
        if not self.assessed.is_group:
907
            if '/' in submitpath:
908
                submitpath = submitpath.split('/', 1)[1]
909
            else:
910
                submitpath = ''
911
        return "/files/%s/%s/%s?r=%d" % (user.login,
912
            self.assessed.checkout_location, submitpath, self.revision)
1165.1.4 by William Grant
Add database classes for assessed, project_extension and project_submission.
913
1660 by Matt Giuca
ivle.database: Added a security check on Project.submit() that the path meets certain constraints, to avoid path injection. Fixes LP bug #522462.
914
    @staticmethod
915
    def test_and_normalise_path(path):
916
        """Test that path is valid, and normalise it. This prevents possible
917
        injections using malicious paths.
918
        Returns the updated path, if successful.
919
        Raises SubmissionError if invalid.
920
        """
921
        # Ensure the path is absolute to prevent being tacked onto working
922
        # directories.
923
        # Prevent '\n' because it will break all sorts of things.
924
        # Prevent '[' and ']' because they can be used to inject into the
925
        # svn.conf.
926
        # Normalise to avoid resulting in ".." path segments.
927
        if not os.path.isabs(path):
928
            raise SubmissionError("Path is not absolute")
929
        if any(c in path for c in "\n[]"):
930
            raise SubmissionError("Path must not contain '\\n', '[' or ']'")
931
        return os.path.normpath(path)
932
1080.1.40 by Matt Giuca
ivle.database: Added Worksheet and Exercise classes (more to come in this
933
# WORKSHEETS AND EXERCISES #
934
935
class Exercise(Storm):
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
936
    """An exercise for students to complete in a worksheet.
937
938
    An exercise may be present in any number of worksheets.
939
    """
940
1099.1.195 by William Grant
Rename problem to exercise in the DB.
941
    __storm_table__ = "exercise"
1099.1.114 by Nick Chadwick
Modified the database so that exercises are now stored in the database, rather
942
    id = Unicode(primary=True, name="identifier")
943
    name = Unicode()
944
    description = Unicode()
1720.1.4 by William Grant
Cache exercise and worksheet rST->XHTML conversions in the DB.
945
    _description_xhtml_cache = Unicode(name='description_xhtml_cache')
1099.1.114 by Nick Chadwick
Modified the database so that exercises are now stored in the database, rather
946
    partial = Unicode()
947
    solution = Unicode()
948
    include = Unicode()
949
    num_rows = Int()
1080.1.40 by Matt Giuca
ivle.database: Added Worksheet and Exercise classes (more to come in this
950
1099.6.2 by Nick Chadwick
Added a listing of all exercises
951
    worksheet_exercises =  ReferenceSet(id,
952
        'WorksheetExercise.exercise_id')
953
1080.1.50 by Matt Giuca
ivle.database: Added WorksheetExercise (relates worksheets to exercises), and
954
    worksheets = ReferenceSet(id,
955
        'WorksheetExercise.exercise_id',
956
        'WorksheetExercise.worksheet_id',
957
        'Worksheet.id'
958
    )
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
959
1099.1.212 by Nick Chadwick
Added a new page to display exercises. This will then be modified to
960
    test_suites = ReferenceSet(id, 
961
        'TestSuite.exercise_id',
962
        order_by='seq_no')
1080.1.50 by Matt Giuca
ivle.database: Added WorksheetExercise (relates worksheets to exercises), and
963
1080.1.40 by Matt Giuca
ivle.database: Added Worksheet and Exercise classes (more to come in this
964
    __init__ = _kwarg_init
965
966
    def __repr__(self):
967
        return "<%s %s>" % (type(self).__name__, self.name)
968
1544 by Matt Giuca
Added an argument 'config' to every single get_permissions method throughout the program. All calls to get_permissions pass a config. This is to allow per-site policy configurations on permissions.
969
    def get_permissions(self, user, config):
970
        return self.global_permissions(user, config)
1536 by Matt Giuca
Fixed policy on who is able to view the list of exercises and create a new one. Rather than being 'if you can edit any offering', it is now the same rule as determining whether you can edit exercises.
971
972
    @staticmethod
1544 by Matt Giuca
Added an argument 'config' to every single get_permissions method throughout the program. All calls to get_permissions pass a config. This is to allow per-site policy configurations on permissions.
973
    def global_permissions(user, config):
1536 by Matt Giuca
Fixed policy on who is able to view the list of exercises and create a new one. Rather than being 'if you can edit any offering', it is now the same rule as determining whether you can edit exercises.
974
        """Gets the set of permissions this user has over *all* exercises.
975
        This is used to determine who may view the exercises list, and create
976
        new exercises."""
1099.1.210 by Nick Chadwick
Modified the database layer so that exercises have a get_permissions
977
        perms = set()
1099.1.234 by Nick Chadwick
Permissions for editing and deleting exercises now come from the
978
        roles = set()
1099.1.210 by Nick Chadwick
Modified the database layer so that exercises have a get_permissions
979
        if user is not None:
1101 by William Grant
Privileges (apart from admin) are now offering-local, not global.
980
            if user.admin:
1099.1.210 by Nick Chadwick
Modified the database layer so that exercises have a get_permissions
981
                perms.add('edit')
982
                perms.add('view')
1165.2.2 by Nick Chadwick
fixed a bug in which tutors weren't able to edit the exercises of
983
            elif u'lecturer' in set((e.role for e in user.active_enrolments)):
984
                perms.add('edit')
985
                perms.add('view')
1547 by Matt Giuca
Added two new configuration options under [policy], for deciding whether tutors can enrol students and edit worksheets. The permissions set in database.py reflects these config options. Documented. This entirely fixes Launchpad Bug #520232 and Bug #493945, regarding tutors having too much power.
986
            elif (config['policy']['tutors_can_edit_worksheets']
987
            and u'tutor' in set((e.role for e in user.active_enrolments))):
988
                # Site-specific policy on the role of tutors
1099.1.234 by Nick Chadwick
Permissions for editing and deleting exercises now come from the
989
                perms.add('edit')
990
                perms.add('view')
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
991
1099.1.210 by Nick Chadwick
Modified the database layer so that exercises have a get_permissions
992
        return perms
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
993
1720.1.4 by William Grant
Cache exercise and worksheet rST->XHTML conversions in the DB.
994
    def _cache_description_xhtml(self, invalidate=False):
995
        # Don't regenerate an existing cache unless forced.
996
        if self._description_xhtml_cache is not None and not invalidate:
997
            return
998
999
        if self.description:
1000
            self._description_xhtml_cache = rst(self.description)
1001
        else:
1002
            self._description_xhtml_cache = None
1003
1720.1.1 by William Grant
Abstract reStructuredText rendering into ivle.database.
1004
    @property
1005
    def description_xhtml(self):
1006
        """The XHTML exercise description, converted from reStructuredText."""
1720.1.4 by William Grant
Cache exercise and worksheet rST->XHTML conversions in the DB.
1007
        self._cache_description_xhtml()
1008
        return self._description_xhtml_cache
1080.1.51 by Matt Giuca
tutorial: Replaced call to ivle.db.create_worksheet with local code (roughly
1009
1720.1.3 by William Grant
Abstract worksheet and exercise data setting.
1010
    def set_description(self, description):
1011
        self.description = description
1720.1.4 by William Grant
Cache exercise and worksheet rST->XHTML conversions in the DB.
1012
        self._cache_description_xhtml(invalidate=True)
1720.1.3 by William Grant
Abstract worksheet and exercise data setting.
1013
1099.1.242 by Nick Chadwick
Fixed a problem with exercise editor, which wasn't editing or adding
1014
    def delete(self):
1015
        """Deletes the exercise, providing it has no associated worksheets."""
1016
        if (self.worksheet_exercises.count() > 0):
1017
            raise IntegrityError()
1099.1.233 by Nick Chadwick
Exercise objects in the database module, along with their test cases,
1018
        for suite in self.test_suites:
1019
            suite.delete()
1099.1.242 by Nick Chadwick
Fixed a problem with exercise editor, which wasn't editing or adding
1020
        Store.of(self).remove(self)
1099.1.233 by Nick Chadwick
Exercise objects in the database module, along with their test cases,
1021
1080.1.40 by Matt Giuca
ivle.database: Added Worksheet and Exercise classes (more to come in this
1022
class Worksheet(Storm):
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
1023
    """A worksheet with exercises for students to complete.
1024
1025
    Worksheets are owned by offerings.
1026
    """
1027
1080.1.40 by Matt Giuca
ivle.database: Added Worksheet and Exercise classes (more to come in this
1028
    __storm_table__ = "worksheet"
1029
1030
    id = Int(primary=True, name="worksheetid")
1099.1.114 by Nick Chadwick
Modified the database so that exercises are now stored in the database, rather
1031
    offering_id = Int(name="offeringid")
1099.4.1 by Nick Chadwick
Working on putting worksheets into the database.
1032
    identifier = Unicode()
1033
    name = Unicode()
1080.1.40 by Matt Giuca
ivle.database: Added Worksheet and Exercise classes (more to come in this
1034
    assessable = Bool()
1695.1.2 by William Grant
Integrate new columns into the model.
1035
    published = Bool()
1099.4.1 by Nick Chadwick
Working on putting worksheets into the database.
1036
    data = Unicode()
1720.1.4 by William Grant
Cache exercise and worksheet rST->XHTML conversions in the DB.
1037
    _data_xhtml_cache = Unicode(name='data_xhtml_cache')
1099.1.180 by Nick Chadwick
This commit changes the tutorial service, which now almost exclusively
1038
    seq_no = Int()
1039
    format = Unicode()
1080.1.40 by Matt Giuca
ivle.database: Added Worksheet and Exercise classes (more to come in this
1040
1099.1.141 by Nick Chadwick
Updated the exercises to be loaded from the database, not a local file.
1041
    attempts = ReferenceSet(id, "ExerciseAttempt.worksheetid")
1099.1.118 by William Grant
Fix a bad reference introduced with the worksheet changes.
1042
    offering = Reference(offering_id, 'Offering.id')
1099.1.114 by Nick Chadwick
Modified the database so that exercises are now stored in the database, rather
1043
1103 by William Grant
Worksheet.worksheet_exercises now only contains active ones.
1044
    all_worksheet_exercises = ReferenceSet(id,
1045
        'WorksheetExercise.worksheet_id')
1046
1047
    # Use worksheet_exercises to get access to the *active* WorksheetExercise
1048
    # objects binding worksheets to exercises. This is required to access the
1080.1.50 by Matt Giuca
ivle.database: Added WorksheetExercise (relates worksheets to exercises), and
1049
    # "optional" field.
1099.1.220 by Nick Chadwick
Merged from trunk
1050
1103 by William Grant
Worksheet.worksheet_exercises now only contains active ones.
1051
    @property
1052
    def worksheet_exercises(self):
1053
        return self.all_worksheet_exercises.find(active=True)
1080.1.50 by Matt Giuca
ivle.database: Added WorksheetExercise (relates worksheets to exercises), and
1054
1080.1.40 by Matt Giuca
ivle.database: Added Worksheet and Exercise classes (more to come in this
1055
    __init__ = _kwarg_init
1056
1057
    def __repr__(self):
1058
        return "<%s %s>" % (type(self).__name__, self.name)
1080.1.47 by Matt Giuca
ivle.database: Added Worksheet.get_by_name method.
1059
1099.1.242 by Nick Chadwick
Fixed a problem with exercise editor, which wasn't editing or adding
1060
    def remove_all_exercises(self):
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
1061
        """Remove all exercises from this worksheet.
1062
1080.1.51 by Matt Giuca
tutorial: Replaced call to ivle.db.create_worksheet with local code (roughly
1063
        This does not delete the exercises themselves. It just removes them
1064
        from the worksheet.
1065
        """
1099.1.242 by Nick Chadwick
Fixed a problem with exercise editor, which wasn't editing or adding
1066
        store = Store.of(self)
1067
        for ws_ex in self.all_worksheet_exercises:
1068
            if ws_ex.saves.count() > 0 or ws_ex.attempts.count() > 0:
1069
                raise IntegrityError()
1080.1.51 by Matt Giuca
tutorial: Replaced call to ivle.db.create_worksheet with local code (roughly
1070
        store.find(WorksheetExercise,
1071
            WorksheetExercise.worksheet == self).remove()
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
1072
1544 by Matt Giuca
Added an argument 'config' to every single get_permissions method throughout the program. All calls to get_permissions pass a config. This is to allow per-site policy configurations on permissions.
1073
    def get_permissions(self, user, config):
1695.1.9 by William Grant
Forbid normal users from viewing unpublished worksheets.
1074
        offering_perms = self.offering.get_permissions(user, config)
1075
1076
        perms = set()
1077
1078
        # Anybody who can view an offering can view a published
1079
        # worksheet.
1080
        if 'view' in offering_perms and self.published:
1081
            perms.add('view')
1082
1083
        # Any worksheet editors can both view and edit.
1084
        if 'edit_worksheets' in offering_perms:
1085
            perms.add('view')
1542 by Matt Giuca
Tutors can now (once again) edit worksheets.
1086
            perms.add('edit')
1695.1.9 by William Grant
Forbid normal users from viewing unpublished worksheets.
1087
1542 by Matt Giuca
Tutors can now (once again) edit worksheets.
1088
        return perms
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
1089
1720.1.4 by William Grant
Cache exercise and worksheet rST->XHTML conversions in the DB.
1090
    def _cache_data_xhtml(self, invalidate=False):
1091
        # Don't regenerate an existing cache unless forced.
1092
        if self._data_xhtml_cache is not None and not invalidate:
1093
            return
1094
1095
        if self.format == u'rst':
1096
            self._data_xhtml_cache = rst(self.data)
1097
        else:
1098
            self._data_xhtml_cache = None
1099
1720.1.1 by William Grant
Abstract reStructuredText rendering into ivle.database.
1100
    @property
1101
    def data_xhtml(self):
1102
        """The XHTML of this worksheet, converted from rST if required."""
1720.1.4 by William Grant
Cache exercise and worksheet rST->XHTML conversions in the DB.
1103
        # Update the rST -> XHTML cache, if required.
1104
        self._cache_data_xhtml()
1105
1099.1.220 by Nick Chadwick
Merged from trunk
1106
        if self.format == u'rst':
1720.1.4 by William Grant
Cache exercise and worksheet rST->XHTML conversions in the DB.
1107
            return self._data_xhtml_cache
1099.1.220 by Nick Chadwick
Merged from trunk
1108
        else:
1109
            return self.data
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
1110
1720.1.3 by William Grant
Abstract worksheet and exercise data setting.
1111
    def set_data(self, data):
1112
        self.data = data
1720.1.4 by William Grant
Cache exercise and worksheet rST->XHTML conversions in the DB.
1113
        self._cache_data_xhtml(invalidate=True)
1720.1.3 by William Grant
Abstract worksheet and exercise data setting.
1114
1099.1.242 by Nick Chadwick
Fixed a problem with exercise editor, which wasn't editing or adding
1115
    def delete(self):
1099.1.233 by Nick Chadwick
Exercise objects in the database module, along with their test cases,
1116
        """Deletes the worksheet, provided it has no attempts on any exercises.
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
1117
1099.1.233 by Nick Chadwick
Exercise objects in the database module, along with their test cases,
1118
        Returns True if delete succeeded, or False if this worksheet has
1119
        attempts attached."""
1120
        for ws_ex in self.all_worksheet_exercises:
1121
            if ws_ex.saves.count() > 0 or ws_ex.attempts.count() > 0:
1099.1.242 by Nick Chadwick
Fixed a problem with exercise editor, which wasn't editing or adding
1122
                raise IntegrityError()
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
1123
1099.1.233 by Nick Chadwick
Exercise objects in the database module, along with their test cases,
1124
        self.remove_all_exercises()
1099.1.242 by Nick Chadwick
Fixed a problem with exercise editor, which wasn't editing or adding
1125
        Store.of(self).remove(self)
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
1126
1080.1.50 by Matt Giuca
ivle.database: Added WorksheetExercise (relates worksheets to exercises), and
1127
class WorksheetExercise(Storm):
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
1128
    """A link between a worksheet and one of its exercises.
1129
1130
    These may be marked optional, in which case the exercise does not count
1131
    for marking purposes. The sequence number is used to order the worksheet
1132
    ToC.
1133
    """
1134
1099.1.195 by William Grant
Rename problem to exercise in the DB.
1135
    __storm_table__ = "worksheet_exercise"
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
1136
1099.1.195 by William Grant
Rename problem to exercise in the DB.
1137
    id = Int(primary=True, name="ws_ex_id")
1080.1.50 by Matt Giuca
ivle.database: Added WorksheetExercise (relates worksheets to exercises), and
1138
1139
    worksheet_id = Int(name="worksheetid")
1140
    worksheet = Reference(worksheet_id, Worksheet.id)
1099.1.195 by William Grant
Rename problem to exercise in the DB.
1141
    exercise_id = Unicode(name="exerciseid")
1080.1.50 by Matt Giuca
ivle.database: Added WorksheetExercise (relates worksheets to exercises), and
1142
    exercise = Reference(exercise_id, Exercise.id)
1143
    optional = Bool()
1099.4.3 by Nick Chadwick
Updated the tutorial service, to now allow users to edit worksheets
1144
    active = Bool()
1145
    seq_no = Int()
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
1146
1099.1.180 by Nick Chadwick
This commit changes the tutorial service, which now almost exclusively
1147
    saves = ReferenceSet(id, "ExerciseSave.ws_ex_id")
1099.1.183 by William Grant
Fix a reference typo in ivle.database.
1148
    attempts = ReferenceSet(id, "ExerciseAttempt.ws_ex_id")
1080.1.50 by Matt Giuca
ivle.database: Added WorksheetExercise (relates worksheets to exercises), and
1149
1150
    __init__ = _kwarg_init
1151
1152
    def __repr__(self):
1153
        return "<%s %s in %s>" % (type(self).__name__, self.exercise.name,
1099.4.1 by Nick Chadwick
Working on putting worksheets into the database.
1154
                                  self.worksheet.identifier)
1080.1.55 by Matt Giuca
ivle.database: Added ExerciseAttempt and ExerciseSave classes.
1155
1544 by Matt Giuca
Added an argument 'config' to every single get_permissions method throughout the program. All calls to get_permissions pass a config. This is to allow per-site policy configurations on permissions.
1156
    def get_permissions(self, user, config):
1157
        return self.worksheet.get_permissions(user, config)
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
1158
1131 by William Grant
Offerings now give 'view' only to user enrolled in them. 'edit' is granted
1159
1080.1.55 by Matt Giuca
ivle.database: Added ExerciseAttempt and ExerciseSave classes.
1160
class ExerciseSave(Storm):
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
1161
    """A potential exercise solution submitted by a user for storage.
1162
1163
    This is not an actual tested attempt at an exercise, it's just a save of
1164
    the editing session.
1165
    """
1166
1099.1.195 by William Grant
Rename problem to exercise in the DB.
1167
    __storm_table__ = "exercise_save"
1099.1.180 by Nick Chadwick
This commit changes the tutorial service, which now almost exclusively
1168
    __storm_primary__ = "ws_ex_id", "user_id"
1169
1099.1.195 by William Grant
Rename problem to exercise in the DB.
1170
    ws_ex_id = Int(name="ws_ex_id")
1099.1.180 by Nick Chadwick
This commit changes the tutorial service, which now almost exclusively
1171
    worksheet_exercise = Reference(ws_ex_id, "WorksheetExercise.id")
1172
1080.1.55 by Matt Giuca
ivle.database: Added ExerciseAttempt and ExerciseSave classes.
1173
    user_id = Int(name="loginid")
1174
    user = Reference(user_id, User.id)
1175
    date = DateTime()
1176
    text = Unicode()
1177
1178
    __init__ = _kwarg_init
1179
1180
    def __repr__(self):
1181
        return "<%s %s by %s at %s>" % (type(self).__name__,
1696 by Matt Giuca
ivle.database: Fixed crash on repr of ExerciseSave, looked up wrong attribute.
1182
            self.worksheet_exercise.exercise.name, self.user.login,
1183
            self.date.strftime("%c"))
1080.1.55 by Matt Giuca
ivle.database: Added ExerciseAttempt and ExerciseSave classes.
1184
1185
class ExerciseAttempt(ExerciseSave):
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
1186
    """An attempt at solving an exercise.
1187
1188
    This is a special case of ExerciseSave, used when the user submits a
1189
    candidate solution. Like an ExerciseSave, it constitutes exercise solution
1190
    data.
1191
1192
    In addition, it contains information about the result of the submission:
1193
1194
     - complete - True if this submission was successful, rendering this
1195
                  exercise complete for this user in this worksheet.
1196
     - active   - True if this submission is "active" (usually true).
1197
                  Submissions may be de-activated by privileged users for
1198
                  special reasons, and then they won't count (either as a
1199
                  penalty or success), but will still be stored.
1200
    """
1201
1099.1.195 by William Grant
Rename problem to exercise in the DB.
1202
    __storm_table__ = "exercise_attempt"
1099.1.180 by Nick Chadwick
This commit changes the tutorial service, which now almost exclusively
1203
    __storm_primary__ = "ws_ex_id", "user_id", "date"
1080.1.55 by Matt Giuca
ivle.database: Added ExerciseAttempt and ExerciseSave classes.
1204
1205
    # The "text" field is the same but has a different name in the DB table
1206
    # for some reason.
1207
    text = Unicode(name="attempt")
1208
    complete = Bool()
1209
    active = Bool()
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
1210
1544 by Matt Giuca
Added an argument 'config' to every single get_permissions method throughout the program. All calls to get_permissions pass a config. This is to allow per-site policy configurations on permissions.
1211
    def get_permissions(self, user, config):
1099.1.113 by William Grant
Give console and tutorial services security declarations.
1212
        return set(['view']) if user is self.user else set()
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
1213
1099.1.114 by Nick Chadwick
Modified the database so that exercises are now stored in the database, rather
1214
class TestSuite(Storm):
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
1215
    """A container to group an exercise's test cases.
1216
1217
    The test suite contains some information on how to test. The function to
1218
    test, variables to set and stdin data are stored here.
1219
    """
1220
1099.1.114 by Nick Chadwick
Modified the database so that exercises are now stored in the database, rather
1221
    __storm_table__ = "test_suite"
1222
    __storm_primary__ = "exercise_id", "suiteid"
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
1223
1099.1.114 by Nick Chadwick
Modified the database so that exercises are now stored in the database, rather
1224
    suiteid = Int()
1099.1.195 by William Grant
Rename problem to exercise in the DB.
1225
    exercise_id = Unicode(name="exerciseid")
1099.1.141 by Nick Chadwick
Updated the exercises to be loaded from the database, not a local file.
1226
    description = Unicode()
1227
    seq_no = Int()
1228
    function = Unicode()
1229
    stdin = Unicode()
1099.1.114 by Nick Chadwick
Modified the database so that exercises are now stored in the database, rather
1230
    exercise = Reference(exercise_id, Exercise.id)
1099.1.212 by Nick Chadwick
Added a new page to display exercises. This will then be modified to
1231
    test_cases = ReferenceSet(suiteid, 'TestCase.suiteid', order_by="seq_no")
1232
    variables = ReferenceSet(suiteid, 'TestSuiteVar.suiteid', order_by='arg_no')
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
1233
1099.1.242 by Nick Chadwick
Fixed a problem with exercise editor, which wasn't editing or adding
1234
    def delete(self):
1099.1.233 by Nick Chadwick
Exercise objects in the database module, along with their test cases,
1235
        """Delete this suite, without asking questions."""
1427 by William Grant
Fix deletion of test suites with variables.
1236
        for variable in self.variables:
1099.1.233 by Nick Chadwick
Exercise objects in the database module, along with their test cases,
1237
            variable.delete()
1238
        for test_case in self.test_cases:
1239
            test_case.delete()
1099.1.242 by Nick Chadwick
Fixed a problem with exercise editor, which wasn't editing or adding
1240
        Store.of(self).remove(self)
1099.1.114 by Nick Chadwick
Modified the database so that exercises are now stored in the database, rather
1241
1242
class TestCase(Storm):
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
1243
    """A container for actual tests (see TestCasePart), inside a test suite.
1244
1245
    It is the lowest level shown to students on their pass/fail status."""
1246
1099.1.114 by Nick Chadwick
Modified the database so that exercises are now stored in the database, rather
1247
    __storm_table__ = "test_case"
1248
    __storm_primary__ = "testid", "suiteid"
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
1249
1099.1.114 by Nick Chadwick
Modified the database so that exercises are now stored in the database, rather
1250
    testid = Int()
1251
    suiteid = Int()
1099.1.141 by Nick Chadwick
Updated the exercises to be loaded from the database, not a local file.
1252
    suite = Reference(suiteid, "TestSuite.suiteid")
1099.1.114 by Nick Chadwick
Modified the database so that exercises are now stored in the database, rather
1253
    passmsg = Unicode()
1254
    failmsg = Unicode()
1394.1.5 by William Grant
Drop TestSuite file match default from the UI -- we don't support file tests any more.
1255
    test_default = Unicode() # Currently unused - only used for file matching.
1099.1.114 by Nick Chadwick
Modified the database so that exercises are now stored in the database, rather
1256
    seq_no = Int()
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
1257
1099.1.141 by Nick Chadwick
Updated the exercises to be loaded from the database, not a local file.
1258
    parts = ReferenceSet(testid, "TestCasePart.testid")
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
1259
1099.1.141 by Nick Chadwick
Updated the exercises to be loaded from the database, not a local file.
1260
    __init__ = _kwarg_init
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
1261
1099.1.242 by Nick Chadwick
Fixed a problem with exercise editor, which wasn't editing or adding
1262
    def delete(self):
1099.1.233 by Nick Chadwick
Exercise objects in the database module, along with their test cases,
1263
        for part in self.parts:
1264
            part.delete()
1099.1.242 by Nick Chadwick
Fixed a problem with exercise editor, which wasn't editing or adding
1265
        Store.of(self).remove(self)
1099.1.141 by Nick Chadwick
Updated the exercises to be loaded from the database, not a local file.
1266
1267
class TestSuiteVar(Storm):
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
1268
    """A variable used by an exercise test suite.
1269
1270
    This may represent a function argument or a normal variable.
1271
    """
1272
1099.1.195 by William Grant
Rename problem to exercise in the DB.
1273
    __storm_table__ = "suite_variable"
1099.1.141 by Nick Chadwick
Updated the exercises to be loaded from the database, not a local file.
1274
    __storm_primary__ = "varid"
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
1275
1099.1.141 by Nick Chadwick
Updated the exercises to be loaded from the database, not a local file.
1276
    varid = Int()
1277
    suiteid = Int()
1278
    var_name = Unicode()
1279
    var_value = Unicode()
1280
    var_type = Unicode()
1281
    arg_no = Int()
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
1282
1099.1.141 by Nick Chadwick
Updated the exercises to be loaded from the database, not a local file.
1283
    suite = Reference(suiteid, "TestSuite.suiteid")
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
1284
1099.1.141 by Nick Chadwick
Updated the exercises to be loaded from the database, not a local file.
1285
    __init__ = _kwarg_init
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
1286
1099.1.242 by Nick Chadwick
Fixed a problem with exercise editor, which wasn't editing or adding
1287
    def delete(self):
1288
        Store.of(self).remove(self)
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
1289
1099.1.141 by Nick Chadwick
Updated the exercises to be loaded from the database, not a local file.
1290
class TestCasePart(Storm):
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
1291
    """An actual piece of code to test an exercise solution."""
1292
1099.1.195 by William Grant
Rename problem to exercise in the DB.
1293
    __storm_table__ = "test_case_part"
1099.1.141 by Nick Chadwick
Updated the exercises to be loaded from the database, not a local file.
1294
    __storm_primary__ = "partid"
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
1295
1099.1.141 by Nick Chadwick
Updated the exercises to be loaded from the database, not a local file.
1296
    partid = Int()
1297
    testid = Int()
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
1298
1099.1.141 by Nick Chadwick
Updated the exercises to be loaded from the database, not a local file.
1299
    part_type = Unicode()
1300
    test_type = Unicode()
1301
    data = Unicode()
1302
    filename = Unicode()
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
1303
1099.1.141 by Nick Chadwick
Updated the exercises to be loaded from the database, not a local file.
1304
    test = Reference(testid, "TestCase.testid")
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
1305
1099.1.114 by Nick Chadwick
Modified the database so that exercises are now stored in the database, rather
1306
    __init__ = _kwarg_init
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
1307
1099.1.242 by Nick Chadwick
Fixed a problem with exercise editor, which wasn't editing or adding
1308
    def delete(self):
1309
        Store.of(self).remove(self)