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