~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()
1080.1.25 by me at id
ivle.database: Add Subject, Semester and Offering.
328
    groups_student_permissions = Unicode()
329
1080.1.26 by me at id
ivle.database: Add an Enrolment class, and reference(set)s between all of the
330
    enrolments = ReferenceSet(id, 'Enrolment.offering_id')
1080.1.79 by William Grant
ivle.database.Offering: Add a members ReferenceSet.
331
    members = ReferenceSet(id,
332
                           'Enrolment.offering_id',
333
                           'Enrolment.user_id',
334
                           'User.id')
1080.1.76 by William Grant
ivle.database.Offering: Add project_sets referenceset.
335
    project_sets = ReferenceSet(id, 'ProjectSet.offering_id')
1442.1.7 by William Grant
Add Offering.projects.
336
    projects = ReferenceSet(id,
337
                            'ProjectSet.offering_id',
338
                            'ProjectSet.id',
339
                            '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
340
1099.1.180 by Nick Chadwick
This commit changes the tutorial service, which now almost exclusively
341
    worksheets = ReferenceSet(id, 
342
        'Worksheet.offering_id', 
1099.1.212 by Nick Chadwick
Added a new page to display exercises. This will then be modified to
343
        order_by="seq_no"
1099.1.180 by Nick Chadwick
This commit changes the tutorial service, which now almost exclusively
344
    )
1099.1.114 by Nick Chadwick
Modified the database so that exercises are now stored in the database, rather
345
1080.1.25 by me at id
ivle.database: Add Subject, Semester and Offering.
346
    __init__ = _kwarg_init
347
348
    def __repr__(self):
349
        return "<%s %r in %r>" % (type(self).__name__, self.subject,
350
                                  self.semester)
1080.1.26 by me at id
ivle.database: Add an Enrolment class, and reference(set)s between all of the
351
1110 by William Grant
ivle-enrol now allows updating of existing enrolments. It also sets the role.
352
    def enrol(self, user, role=u'student'):
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
353
        """Enrol a user in this offering.
354
355
        Enrolments handle both the staff and student cases. The role controls
356
        the privileges granted by this enrolment.
357
        """
1110 by William Grant
ivle-enrol now allows updating of existing enrolments. It also sets the role.
358
        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
359
                               Enrolment.user_id == user.id,
1110 by William Grant
ivle-enrol now allows updating of existing enrolments. It also sets the role.
360
                               Enrolment.offering_id == self.id).one()
361
362
        if enrolment is None:
363
            enrolment = Enrolment(user=user, offering=self)
364
            self.enrolments.add(enrolment)
365
366
        enrolment.active = True
367
        enrolment.role = role
1080.1.61 by William Grant
ivle.database: Add an Offering.enrol(user) method, which enrols the user in
368
1132 by William Grant
Add Offering.unenrol(), to unenrol a user from an offering.
369
    def unenrol(self, user):
370
        '''Unenrol a user from this offering.'''
371
        enrolment = Store.of(self).find(Enrolment,
372
                               Enrolment.user_id == user.id,
373
                               Enrolment.offering_id == self.id).one()
374
        Store.of(enrolment).remove(enrolment)
375
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.
376
    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.
377
        perms = set()
378
        if user is not None:
1131 by William Grant
Offerings now give 'view' only to user enrolled in them. 'edit' is granted
379
            enrolment = self.get_enrolment(user)
380
            if enrolment or user.admin:
381
                perms.add('view')
1558 by William Grant
Allow tutors to manage groups.
382
            if enrolment and enrolment.role == u'tutor':
1556 by William Grant
Allow tutors to view project submissions.
383
                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.
384
                # Site-specific policy on the role of tutors
385
                if config['policy']['tutors_can_enrol_students']:
386
                    perms.add('enrol')
387
                    perms.add('enrol_student')
388
                if config['policy']['tutors_can_edit_worksheets']:
389
                    perms.add('edit_worksheets')
1558 by William Grant
Allow tutors to manage groups.
390
                if config['policy']['tutors_can_admin_groups']:
391
                    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.
392
            if (enrolment and enrolment.role in (u'lecturer')) or user.admin:
1556 by William Grant
Allow tutors to view project submissions.
393
                perms.add('view_project_submissions')
1558 by William Grant
Allow tutors to manage groups.
394
                perms.add('admin_groups')
1542 by Matt Giuca
Tutors can now (once again) edit worksheets.
395
                perms.add('edit_worksheets')
396
                perms.add('edit')           # Can edit projects & details
1377 by Matt Giuca
database: Added finer-grained enrol permissions on offerings.
397
                perms.add('enrol')          # Can see enrolment screen at all
398
                perms.add('enrol_student')  # Can enrol students
399
                perms.add('enrol_tutor')    # Can enrol tutors
400
            if user.admin:
401
                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.
402
        return perms
403
1129 by William Grant
Move the group admin view to per-offering.
404
    def get_enrolment(self, user):
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
405
        """Find the user's enrolment in this offering."""
1129 by William Grant
Move the group admin view to per-offering.
406
        try:
407
            enrolment = self.enrolments.find(user=user).one()
408
        except NotOneError:
409
            enrolment = None
410
411
        return enrolment
412
1165.3.42 by William Grant
Fix ProjectView's total assigned count by dealing with ResultSets only.
413
    def get_members_by_role(self, role):
414
        return Store.of(self).find(User,
415
                Enrolment.user_id == User.id,
416
                Enrolment.offering_id == self.id,
417
                Enrolment.role == role
1364 by Matt Giuca
database: Offering.get_members_by_role now sorts the data.
418
                ).order_by(User.login)
1165.3.42 by William Grant
Fix ProjectView's total assigned count by dealing with ResultSets only.
419
1165.3.43 by William Grant
Turn the new database methods into properties where appropriate.
420
    @property
421
    def students(self):
1165.3.42 by William Grant
Fix ProjectView's total assigned count by dealing with ResultSets only.
422
        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
423
1442.1.23 by William Grant
Add Offering.get_open_projects_for_user.
424
    def get_open_projects_for_user(self, user):
425
        """Find all projects currently open to submissions by a user."""
426
        # XXX: Respect extensions.
427
        return self.projects.find(Project.deadline > datetime.datetime.now())
428
1603 by William Grant
Add UI to clone worksheets between offerings -- replacing ivle-cloneworksheets.
429
    def clone_worksheets(self, source):
430
        """Clone all worksheets from the specified source to this offering."""
431
        import ivle.worksheet.utils
432
        for worksheet in source.worksheets:
433
            newws = Worksheet()
434
            newws.seq_no = worksheet.seq_no
435
            newws.identifier = worksheet.identifier
436
            newws.name = worksheet.name
437
            newws.assessable = worksheet.assessable
438
            newws.data = worksheet.data
439
            newws.format = worksheet.format
440
            newws.offering = self
441
            Store.of(self).add(newws)
442
            ivle.worksheet.utils.update_exerciselist(newws)
443
444
1080.1.26 by me at id
ivle.database: Add an Enrolment class, and reference(set)s between all of the
445
class Enrolment(Storm):
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
446
    """An enrolment of a user in an offering.
447
448
    This represents the roles of both staff and students.
449
    """
450
1080.1.26 by me at id
ivle.database: Add an Enrolment class, and reference(set)s between all of the
451
    __storm_table__ = "enrolment"
452
    __storm_primary__ = "user_id", "offering_id"
453
454
    user_id = Int(name="loginid")
455
    user = Reference(user_id, User.id)
456
    offering_id = Int(name="offeringid")
457
    offering = Reference(offering_id, Offering.id)
1101 by William Grant
Privileges (apart from admin) are now offering-local, not global.
458
    role = Unicode()
1080.1.26 by me at id
ivle.database: Add an Enrolment class, and reference(set)s between all of the
459
    notes = Unicode()
460
    active = Bool()
461
1080.1.81 by William Grant
ivle.database.Enrolment: Add a groups attribute, containing groups of which
462
    @property
463
    def groups(self):
464
        return Store.of(self).find(ProjectGroup,
465
                ProjectSet.offering_id == self.offering.id,
466
                ProjectGroup.project_set_id == ProjectSet.id,
467
                ProjectGroupMembership.project_group_id == ProjectGroup.id,
468
                ProjectGroupMembership.user_id == self.user.id)
469
1080.1.26 by me at id
ivle.database: Add an Enrolment class, and reference(set)s between all of the
470
    __init__ = _kwarg_init
471
472
    def __repr__(self):
473
        return "<%s %r in %r>" % (type(self).__name__, self.user,
474
                                  self.offering)
1080.1.36 by William Grant
ivle.database: Add ProjectSet, Project, ProjectGroup, ProjectGroupMembership
475
1613 by William Grant
Add UI to edit/delete enrolments.
476
    def get_permissions(self, user, config):
477
        # A user can edit any enrolment that they could have created.
478
        perms = set()
479
        if ('enrol_' + str(self.role)) in self.offering.get_permissions(
480
            user, config):
481
            perms.add('edit')
482
        return perms
483
484
    def delete(self):
485
        """Delete this enrolment."""
486
        Store.of(self).remove(self)
487
488
1080.1.39 by Matt Giuca
ivle.database: Added __all__ to the top of the file.
489
# PROJECTS #
490
1080.1.36 by William Grant
ivle.database: Add ProjectSet, Project, ProjectGroup, ProjectGroupMembership
491
class ProjectSet(Storm):
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
492
    """A set of projects that share common groups.
493
494
    Each student project group is attached to a project set. The group is
495
    valid for all projects in the group's set.
496
    """
497
1080.1.36 by William Grant
ivle.database: Add ProjectSet, Project, ProjectGroup, ProjectGroupMembership
498
    __storm_table__ = "project_set"
499
500
    id = Int(name="projectsetid", primary=True)
501
    offering_id = Int(name="offeringid")
502
    offering = Reference(offering_id, Offering.id)
503
    max_students_per_group = Int()
504
1080.1.77 by William Grant
ivle.database.ProjectSet: Add projects and project_groups referencesets.
505
    projects = ReferenceSet(id, 'Project.project_set_id')
506
    project_groups = ReferenceSet(id, 'ProjectGroup.project_set_id')
507
1080.1.36 by William Grant
ivle.database: Add ProjectSet, Project, ProjectGroup, ProjectGroupMembership
508
    __init__ = _kwarg_init
509
510
    def __repr__(self):
511
        return "<%s %d in %r>" % (type(self).__name__, self.id,
512
                                  self.offering)
513
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.
514
    def get_permissions(self, user, config):
515
        return self.offering.get_permissions(user, config)
1165.3.2 by Nick Chadwick
Created a new view for IVLE, allowing lecturers and tutors to
516
1442.1.12 by William Grant
Display either 'solo' or the list of other group members on the offering project listing.
517
    def get_groups_for_user(self, user):
518
        """List all groups in this offering of which the user is a member."""
519
        assert self.is_group
520
        return Store.of(self).find(
521
            ProjectGroup,
522
            ProjectGroupMembership.user_id == user.id,
523
            ProjectGroupMembership.project_group_id == ProjectGroup.id,
524
            ProjectGroup.project_set_id == self.id)
525
1442.1.15 by William Grant
Show the group name in the case of a group project.
526
    def get_submission_principal(self, user):
527
        """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.
528
1442.1.15 by William Grant
Show the group name in the case of a group project.
529
        If this is a solo project set, the given user is returned. If
530
        the user is a member of exactly one group, all the group is
531
        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.
532
        """
533
        if self.is_group:
534
            groups = self.get_groups_for_user(user)
535
            if groups.count() == 1:
1442.1.15 by William Grant
Show the group name in the case of a group project.
536
                return groups.one()
1442.1.12 by William Grant
Display either 'solo' or the list of other group members on the offering project listing.
537
            else:
538
                return None
539
        else:
1442.1.15 by William Grant
Show the group name in the case of a group project.
540
            return user
1442.1.12 by William Grant
Display either 'solo' or the list of other group members on the offering project listing.
541
1165.3.43 by William Grant
Turn the new database methods into properties where appropriate.
542
    @property
1375.1.3 by William Grant
Add ProjectSet.is_group, a property determining whether it is a group or solo set.
543
    def is_group(self):
544
        return self.max_students_per_group is not None
545
546
    @property
1165.3.43 by William Grant
Turn the new database methods into properties where appropriate.
547
    def assigned(self):
548
        """Get the entities (groups or users) assigned to submit this project.
549
550
        This will be a Storm ResultSet.
551
        """
552
        #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.
553
        if self.is_group:
554
            return self.project_groups
555
        else:
1165.3.43 by William Grant
Turn the new database methods into properties where appropriate.
556
            return self.offering.students
1165.4.2 by Nick Chadwick
Added a get_assigned method to project sets, to find out which objects
557
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.
558
class DeadlinePassed(Exception):
559
    """An exception indicating that a project cannot be submitted because the
560
    deadline has passed."""
561
    def __init__(self):
562
        pass
563
    def __str__(self):
564
        return "The project deadline has passed"
565
1080.1.36 by William Grant
ivle.database: Add ProjectSet, Project, ProjectGroup, ProjectGroupMembership
566
class Project(Storm):
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
567
    """A student project for which submissions can be made."""
568
1080.1.36 by William Grant
ivle.database: Add ProjectSet, Project, ProjectGroup, ProjectGroupMembership
569
    __storm_table__ = "project"
570
571
    id = Int(name="projectid", primary=True)
1165.1.4 by William Grant
Add database classes for assessed, project_extension and project_submission.
572
    name = Unicode()
573
    short_name = Unicode()
1080.1.36 by William Grant
ivle.database: Add ProjectSet, Project, ProjectGroup, ProjectGroupMembership
574
    synopsis = Unicode()
575
    url = Unicode()
576
    project_set_id = Int(name="projectsetid")
577
    project_set = Reference(project_set_id, ProjectSet.id)
578
    deadline = DateTime()
579
1165.1.5 by William Grant
Add relevant ReferenceSets to Project and Assessed.
580
    assesseds = ReferenceSet(id, 'Assessed.project_id')
581
    submissions = ReferenceSet(id,
582
                               'Assessed.project_id',
583
                               'Assessed.id',
584
                               'ProjectSubmission.assessed_id')
585
1080.1.36 by William Grant
ivle.database: Add ProjectSet, Project, ProjectGroup, ProjectGroupMembership
586
    __init__ = _kwarg_init
587
588
    def __repr__(self):
1165.1.4 by William Grant
Add database classes for assessed, project_extension and project_submission.
589
        return "<%s '%s' in %r>" % (type(self).__name__, self.short_name,
1080.1.36 by William Grant
ivle.database: Add ProjectSet, Project, ProjectGroup, ProjectGroupMembership
590
                                  self.project_set.offering)
591
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.
592
    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.
593
        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.
594
                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.
595
1165.1.42 by William Grant
Record who submitted each submission.
596
    def submit(self, principal, path, revision, who):
597
        """Submit a Subversion path and revision to a project.
598
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
599
        @param principal: The owner of the Subversion repository, and the
600
                          entity on behalf of whom the submission is being made
601
        @param path: A path within that repository to submit.
602
        @param revision: The revision of that path to submit.
603
        @param who: The user who is actually making the submission.
1165.1.42 by William Grant
Record who submitted each submission.
604
        """
605
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.
606
        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.
607
            raise DeadlinePassed()
1165.1.19 by William Grant
Add Project.submit(), to create a submission for the principal, path and rev.
608
609
        a = Assessed.get(Store.of(self), principal, self)
610
        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.
611
        # Raise SubmissionError if the path is illegal
612
        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.
613
        ps.revision = revision
614
        ps.date_submitted = datetime.datetime.now()
615
        ps.assessed = a
1165.1.42 by William Grant
Record who submitted each submission.
616
        ps.submitter = who
1165.1.19 by William Grant
Add Project.submit(), to create a submission for the principal, path and rev.
617
618
        return ps
619
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.
620
    def get_permissions(self, user, config):
621
        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
622
1165.3.45 by William Grant
Add Project.latest_submissions, which excludes superseded submissions.
623
    @property
624
    def latest_submissions(self):
625
        """Return the latest submission for each Assessed."""
626
        return Store.of(self).find(ProjectSubmission,
627
            Assessed.project_id == self.id,
628
            ProjectSubmission.assessed_id == Assessed.id,
629
            ProjectSubmission.date_submitted == Select(
630
                    Max(ProjectSubmission.date_submitted),
631
                    ProjectSubmission.assessed_id == Assessed.id,
632
                    tables=ProjectSubmission
633
            )
634
        )
635
1442.1.13 by William Grant
Reduce the opacity of projects that are closed.
636
    def has_deadline_passed(self, user):
637
        """Check whether the deadline has passed."""
638
        # XXX: Need to respect extensions.
639
        return self.deadline < datetime.datetime.now()
640
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.
641
    def get_submissions_for_principal(self, principal):
642
        """Fetch a ResultSet of all submissions by a particular principal."""
643
        assessed = Assessed.get(Store.of(self), principal, self)
644
        if assessed is None:
645
            return
646
        return assessed.submissions
647
648
1165.1.19 by William Grant
Add Project.submit(), to create a submission for the principal, path and rev.
649
1080.1.36 by William Grant
ivle.database: Add ProjectSet, Project, ProjectGroup, ProjectGroupMembership
650
class ProjectGroup(Storm):
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
651
    """A group of students working together on a project."""
652
1080.1.36 by William Grant
ivle.database: Add ProjectSet, Project, ProjectGroup, ProjectGroupMembership
653
    __storm_table__ = "project_group"
654
655
    id = Int(name="groupid", primary=True)
656
    name = Unicode(name="groupnm")
657
    project_set_id = Int(name="projectsetid")
658
    project_set = Reference(project_set_id, ProjectSet.id)
659
    nick = Unicode()
660
    created_by_id = Int(name="createdby")
661
    created_by = Reference(created_by_id, User.id)
662
    epoch = DateTime()
663
1080.1.78 by William Grant
ivle.database.ProjectGroup.members: Use a ReferenceSet.
664
    members = ReferenceSet(id,
665
                           "ProjectGroupMembership.project_group_id",
666
                           "ProjectGroupMembership.user_id",
667
                           "User.id")
668
1080.1.36 by William Grant
ivle.database: Add ProjectSet, Project, ProjectGroup, ProjectGroupMembership
669
    __init__ = _kwarg_init
670
671
    def __repr__(self):
672
        return "<%s %s in %r>" % (type(self).__name__, self.name,
673
                                  self.project_set.offering)
674
1165.1.26 by William Grant
Add display_name properties to users and groups.
675
    @property
676
    def display_name(self):
1165.3.69 by Matt Giuca
ivle.database: User/ProjectGroup: Added 'short_name' methods so we have a
677
        """Returns the "nice name" of the user or group."""
678
        return self.nick
679
680
    @property
681
    def short_name(self):
682
        """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.
683
        return self.name
1165.1.26 by William Grant
Add display_name properties to users and groups.
684
1165.1.12 by William Grant
Implement ProjectGroup.get_projects(), with identical interface.
685
    def get_projects(self, offering=None, active_only=True):
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
686
        '''Find projects that the group can submit.
1165.1.12 by William Grant
Implement ProjectGroup.get_projects(), with identical interface.
687
688
        This will include projects in the project set which owns this group,
689
        unless the project set disallows groups (in which case none will be
690
        returned).
691
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
692
        @param active_only: Whether to only search active offerings.
693
        @param offering: An optional offering to restrict the search to.
1165.1.12 by William Grant
Implement ProjectGroup.get_projects(), with identical interface.
694
        '''
695
        return Store.of(self).find(Project,
696
            Project.project_set_id == ProjectSet.id,
697
            ProjectSet.id == self.project_set.id,
1165.1.46 by William Grant
Respect the new max_students_per_group semantics in Python.
698
            ProjectSet.max_students_per_group != None,
1165.1.12 by William Grant
Implement ProjectGroup.get_projects(), with identical interface.
699
            ProjectSet.offering_id == Offering.id,
700
            (offering is None) or (Offering.id == offering.id),
701
            Semester.id == Offering.semester_id,
702
            (not active_only) or (Semester.state == u'current'))
703
704
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.
705
    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.
706
        if user.admin or user in self.members:
707
            return set(['submit_project'])
708
        else:
709
            return set()
710
1080.1.36 by William Grant
ivle.database: Add ProjectSet, Project, ProjectGroup, ProjectGroupMembership
711
class ProjectGroupMembership(Storm):
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
712
    """A student's membership in a project group."""
713
1080.1.36 by William Grant
ivle.database: Add ProjectSet, Project, ProjectGroup, ProjectGroupMembership
714
    __storm_table__ = "group_member"
715
    __storm_primary__ = "user_id", "project_group_id"
716
717
    user_id = Int(name="loginid")
718
    user = Reference(user_id, User.id)
719
    project_group_id = Int(name="groupid")
720
    project_group = Reference(project_group_id, ProjectGroup.id)
721
722
    __init__ = _kwarg_init
723
724
    def __repr__(self):
725
        return "<%s %r in %r>" % (type(self).__name__, self.user,
726
                                  self.project_group)
727
1165.1.4 by William Grant
Add database classes for assessed, project_extension and project_submission.
728
class Assessed(Storm):
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
729
    """A composite of a user or group combined with a project.
730
731
    Each project submission and extension refers to an Assessed. It is the
732
    sole specifier of the repository and project.
733
    """
734
1165.1.4 by William Grant
Add database classes for assessed, project_extension and project_submission.
735
    __storm_table__ = "assessed"
736
737
    id = Int(name="assessedid", primary=True)
738
    user_id = Int(name="loginid")
739
    user = Reference(user_id, User.id)
740
    project_group_id = Int(name="groupid")
741
    project_group = Reference(project_group_id, ProjectGroup.id)
742
743
    project_id = Int(name="projectid")
744
    project = Reference(project_id, Project.id)
745
1165.1.5 by William Grant
Add relevant ReferenceSets to Project and Assessed.
746
    extensions = ReferenceSet(id, 'ProjectExtension.assessed_id')
1442.1.18 by William Grant
Order Assessed.submissions by date_submitted.
747
    submissions = ReferenceSet(
748
        id, 'ProjectSubmission.assessed_id', order_by='date_submitted')
1165.1.5 by William Grant
Add relevant ReferenceSets to Project and Assessed.
749
1165.1.4 by William Grant
Add database classes for assessed, project_extension and project_submission.
750
    def __repr__(self):
751
        return "<%s %r in %r>" % (type(self).__name__,
752
            self.user or self.project_group, self.project)
753
1165.3.12 by William Grant
Add an Assessed.principal property.
754
    @property
1165.3.49 by Matt Giuca
ivle.database.Assessed: Added property is_group.
755
    def is_group(self):
756
        """True if the Assessed is a group, False if it is a user."""
757
        return self.project_group is not None
758
759
    @property
1165.3.12 by William Grant
Add an Assessed.principal property.
760
    def principal(self):
761
        return self.project_group or self.user
762
1522 by Matt Giuca
Subject page: Added Verify links on the subject pages to any projects with
763
    @property
764
    def checkout_location(self):
765
        """Returns the location of the Subversion workspace for this piece of
766
        assessment, relative to each group member's home directory."""
767
        subjectname = self.project.project_set.offering.subject.short_name
768
        if self.is_group:
769
            checkout_dir_name = self.principal.short_name
770
        else:
771
            checkout_dir_name = "mywork"
772
        return subjectname + "/" + checkout_dir_name
773
1165.1.18 by William Grant
Add a method to retrieve or create an Assessed given a principal and project.
774
    @classmethod
775
    def get(cls, store, principal, project):
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
776
        """Find or create an Assessed for the given user or group and project.
777
778
        @param principal: The user or group.
779
        @param project: The project.
780
        """
1165.1.18 by William Grant
Add a method to retrieve or create an Assessed given a principal and project.
781
        t = type(principal)
782
        if t not in (User, ProjectGroup):
783
            raise AssertionError('principal must be User or ProjectGroup')
784
785
        a = store.find(cls,
786
            (t is User) or (cls.project_group_id == principal.id),
787
            (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.
788
            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.
789
790
        if a is None:
791
            a = cls()
792
            if t is User:
793
                a.user = principal
794
            else:
795
                a.project_group = principal
796
            a.project = project
797
            store.add(a)
798
799
        return a
800
801
1165.1.4 by William Grant
Add database classes for assessed, project_extension and project_submission.
802
class ProjectExtension(Storm):
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
803
    """An extension granted to a user or group on a particular project.
804
805
    The user or group and project are specified by the Assessed.
806
    """
807
1165.1.4 by William Grant
Add database classes for assessed, project_extension and project_submission.
808
    __storm_table__ = "project_extension"
809
810
    id = Int(name="extensionid", primary=True)
811
    assessed_id = Int(name="assessedid")
812
    assessed = Reference(assessed_id, Assessed.id)
813
    deadline = DateTime()
814
    approver_id = Int(name="approver")
815
    approver = Reference(approver_id, User.id)
816
    notes = Unicode()
817
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.
818
class SubmissionError(Exception):
819
    """Denotes a validation error during submission."""
820
    pass
821
1165.1.4 by William Grant
Add database classes for assessed, project_extension and project_submission.
822
class ProjectSubmission(Storm):
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
823
    """A submission from a user or group repository to a particular project.
824
825
    The content of a submission is a single path and revision inside a
826
    repository. The repository is that owned by the submission's user and
827
    group, while the path and revision are explicit.
828
829
    The user or group and project are specified by the Assessed.
830
    """
831
1165.1.4 by William Grant
Add database classes for assessed, project_extension and project_submission.
832
    __storm_table__ = "project_submission"
833
834
    id = Int(name="submissionid", primary=True)
835
    assessed_id = Int(name="assessedid")
836
    assessed = Reference(assessed_id, Assessed.id)
837
    path = Unicode()
838
    revision = Int()
1165.1.42 by William Grant
Record who submitted each submission.
839
    submitter_id = Int(name="submitter")
840
    submitter = Reference(submitter_id, User.id)
1165.1.4 by William Grant
Add database classes for assessed, project_extension and project_submission.
841
    date_submitted = DateTime()
842
1522 by Matt Giuca
Subject page: Added Verify links on the subject pages to any projects with
843
    def get_verify_url(self, user):
844
        """Get the URL for verifying this submission, within the account of
845
        the given user."""
846
        # If this is a solo project, then self.path will be prefixed with the
847
        # subject name. Remove the first path segment.
848
        submitpath = self.path[1:] if self.path[:1] == '/' else self.path
849
        if not self.assessed.is_group:
850
            if '/' in submitpath:
851
                submitpath = submitpath.split('/', 1)[1]
852
            else:
853
                submitpath = ''
854
        return "/files/%s/%s/%s?r=%d" % (user.login,
855
            self.assessed.checkout_location, submitpath, self.revision)
1165.1.4 by William Grant
Add database classes for assessed, project_extension and project_submission.
856
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.
857
    @staticmethod
858
    def test_and_normalise_path(path):
859
        """Test that path is valid, and normalise it. This prevents possible
860
        injections using malicious paths.
861
        Returns the updated path, if successful.
862
        Raises SubmissionError if invalid.
863
        """
864
        # Ensure the path is absolute to prevent being tacked onto working
865
        # directories.
866
        # Prevent '\n' because it will break all sorts of things.
867
        # Prevent '[' and ']' because they can be used to inject into the
868
        # svn.conf.
869
        # Normalise to avoid resulting in ".." path segments.
870
        if not os.path.isabs(path):
871
            raise SubmissionError("Path is not absolute")
872
        if any(c in path for c in "\n[]"):
873
            raise SubmissionError("Path must not contain '\\n', '[' or ']'")
874
        return os.path.normpath(path)
875
1080.1.40 by Matt Giuca
ivle.database: Added Worksheet and Exercise classes (more to come in this
876
# WORKSHEETS AND EXERCISES #
877
878
class Exercise(Storm):
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
879
    """An exercise for students to complete in a worksheet.
880
881
    An exercise may be present in any number of worksheets.
882
    """
883
1099.1.195 by William Grant
Rename problem to exercise in the DB.
884
    __storm_table__ = "exercise"
1099.1.114 by Nick Chadwick
Modified the database so that exercises are now stored in the database, rather
885
    id = Unicode(primary=True, name="identifier")
886
    name = Unicode()
887
    description = Unicode()
888
    partial = Unicode()
889
    solution = Unicode()
890
    include = Unicode()
891
    num_rows = Int()
1080.1.40 by Matt Giuca
ivle.database: Added Worksheet and Exercise classes (more to come in this
892
1099.6.2 by Nick Chadwick
Added a listing of all exercises
893
    worksheet_exercises =  ReferenceSet(id,
894
        'WorksheetExercise.exercise_id')
895
1080.1.50 by Matt Giuca
ivle.database: Added WorksheetExercise (relates worksheets to exercises), and
896
    worksheets = ReferenceSet(id,
897
        'WorksheetExercise.exercise_id',
898
        'WorksheetExercise.worksheet_id',
899
        'Worksheet.id'
900
    )
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
901
1099.1.212 by Nick Chadwick
Added a new page to display exercises. This will then be modified to
902
    test_suites = ReferenceSet(id, 
903
        'TestSuite.exercise_id',
904
        order_by='seq_no')
1080.1.50 by Matt Giuca
ivle.database: Added WorksheetExercise (relates worksheets to exercises), and
905
1080.1.40 by Matt Giuca
ivle.database: Added Worksheet and Exercise classes (more to come in this
906
    __init__ = _kwarg_init
907
908
    def __repr__(self):
909
        return "<%s %s>" % (type(self).__name__, self.name)
910
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.
911
    def get_permissions(self, user, config):
912
        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.
913
914
    @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.
915
    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.
916
        """Gets the set of permissions this user has over *all* exercises.
917
        This is used to determine who may view the exercises list, and create
918
        new exercises."""
1099.1.210 by Nick Chadwick
Modified the database layer so that exercises have a get_permissions
919
        perms = set()
1099.1.234 by Nick Chadwick
Permissions for editing and deleting exercises now come from the
920
        roles = set()
1099.1.210 by Nick Chadwick
Modified the database layer so that exercises have a get_permissions
921
        if user is not None:
1101 by William Grant
Privileges (apart from admin) are now offering-local, not global.
922
            if user.admin:
1099.1.210 by Nick Chadwick
Modified the database layer so that exercises have a get_permissions
923
                perms.add('edit')
924
                perms.add('view')
1165.2.2 by Nick Chadwick
fixed a bug in which tutors weren't able to edit the exercises of
925
            elif u'lecturer' in set((e.role for e in user.active_enrolments)):
926
                perms.add('edit')
927
                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.
928
            elif (config['policy']['tutors_can_edit_worksheets']
929
            and u'tutor' in set((e.role for e in user.active_enrolments))):
930
                # Site-specific policy on the role of tutors
1099.1.234 by Nick Chadwick
Permissions for editing and deleting exercises now come from the
931
                perms.add('edit')
932
                perms.add('view')
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
933
1099.1.210 by Nick Chadwick
Modified the database layer so that exercises have a get_permissions
934
        return perms
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
935
1099.6.3 by Nick Chadwick
Edited the exercise service to delete individual parts of an exercise.
936
    def get_description(self):
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
937
        """Return the description interpreted as reStructuredText."""
1099.1.232 by Nick Chadwick
Removed XML from database. RST now generates a full xml document, not
938
        return rst(self.description)
1080.1.51 by Matt Giuca
tutorial: Replaced call to ivle.db.create_worksheet with local code (roughly
939
1099.1.242 by Nick Chadwick
Fixed a problem with exercise editor, which wasn't editing or adding
940
    def delete(self):
941
        """Deletes the exercise, providing it has no associated worksheets."""
942
        if (self.worksheet_exercises.count() > 0):
943
            raise IntegrityError()
1099.1.233 by Nick Chadwick
Exercise objects in the database module, along with their test cases,
944
        for suite in self.test_suites:
945
            suite.delete()
1099.1.242 by Nick Chadwick
Fixed a problem with exercise editor, which wasn't editing or adding
946
        Store.of(self).remove(self)
1099.1.233 by Nick Chadwick
Exercise objects in the database module, along with their test cases,
947
1080.1.40 by Matt Giuca
ivle.database: Added Worksheet and Exercise classes (more to come in this
948
class Worksheet(Storm):
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
949
    """A worksheet with exercises for students to complete.
950
951
    Worksheets are owned by offerings.
952
    """
953
1080.1.40 by Matt Giuca
ivle.database: Added Worksheet and Exercise classes (more to come in this
954
    __storm_table__ = "worksheet"
955
956
    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
957
    offering_id = Int(name="offeringid")
1099.4.1 by Nick Chadwick
Working on putting worksheets into the database.
958
    identifier = Unicode()
959
    name = Unicode()
1080.1.40 by Matt Giuca
ivle.database: Added Worksheet and Exercise classes (more to come in this
960
    assessable = Bool()
1099.4.1 by Nick Chadwick
Working on putting worksheets into the database.
961
    data = Unicode()
1099.1.180 by Nick Chadwick
This commit changes the tutorial service, which now almost exclusively
962
    seq_no = Int()
963
    format = Unicode()
1080.1.40 by Matt Giuca
ivle.database: Added Worksheet and Exercise classes (more to come in this
964
1099.1.141 by Nick Chadwick
Updated the exercises to be loaded from the database, not a local file.
965
    attempts = ReferenceSet(id, "ExerciseAttempt.worksheetid")
1099.1.118 by William Grant
Fix a bad reference introduced with the worksheet changes.
966
    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
967
1103 by William Grant
Worksheet.worksheet_exercises now only contains active ones.
968
    all_worksheet_exercises = ReferenceSet(id,
969
        'WorksheetExercise.worksheet_id')
970
971
    # Use worksheet_exercises to get access to the *active* WorksheetExercise
972
    # 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
973
    # "optional" field.
1099.1.220 by Nick Chadwick
Merged from trunk
974
1103 by William Grant
Worksheet.worksheet_exercises now only contains active ones.
975
    @property
976
    def worksheet_exercises(self):
977
        return self.all_worksheet_exercises.find(active=True)
1080.1.50 by Matt Giuca
ivle.database: Added WorksheetExercise (relates worksheets to exercises), and
978
1080.1.40 by Matt Giuca
ivle.database: Added Worksheet and Exercise classes (more to come in this
979
    __init__ = _kwarg_init
980
981
    def __repr__(self):
982
        return "<%s %s>" % (type(self).__name__, self.name)
1080.1.47 by Matt Giuca
ivle.database: Added Worksheet.get_by_name method.
983
1099.1.242 by Nick Chadwick
Fixed a problem with exercise editor, which wasn't editing or adding
984
    def remove_all_exercises(self):
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
985
        """Remove all exercises from this worksheet.
986
1080.1.51 by Matt Giuca
tutorial: Replaced call to ivle.db.create_worksheet with local code (roughly
987
        This does not delete the exercises themselves. It just removes them
988
        from the worksheet.
989
        """
1099.1.242 by Nick Chadwick
Fixed a problem with exercise editor, which wasn't editing or adding
990
        store = Store.of(self)
991
        for ws_ex in self.all_worksheet_exercises:
992
            if ws_ex.saves.count() > 0 or ws_ex.attempts.count() > 0:
993
                raise IntegrityError()
1080.1.51 by Matt Giuca
tutorial: Replaced call to ivle.db.create_worksheet with local code (roughly
994
        store.find(WorksheetExercise,
995
            WorksheetExercise.worksheet == self).remove()
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
996
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.
997
    def get_permissions(self, user, config):
1542 by Matt Giuca
Tutors can now (once again) edit worksheets.
998
        # Almost the same permissions as for the offering itself
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.
999
        perms = self.offering.get_permissions(user, config)
1542 by Matt Giuca
Tutors can now (once again) edit worksheets.
1000
        # However, "edit" permission is derived from the "edit_worksheets"
1001
        # permission of the offering
1002
        if 'edit_worksheets' in perms:
1003
            perms.add('edit')
1004
        else:
1005
            perms.discard('edit')
1006
        return perms
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
1007
1099.1.220 by Nick Chadwick
Merged from trunk
1008
    def get_xml(self):
1009
        """Returns the xml of this worksheet, converts from rst if required."""
1010
        if self.format == u'rst':
1099.1.232 by Nick Chadwick
Removed XML from database. RST now generates a full xml document, not
1011
            ws_xml = rst(self.data)
1099.1.220 by Nick Chadwick
Merged from trunk
1012
            return ws_xml
1013
        else:
1014
            return self.data
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
1015
1099.1.242 by Nick Chadwick
Fixed a problem with exercise editor, which wasn't editing or adding
1016
    def delete(self):
1099.1.233 by Nick Chadwick
Exercise objects in the database module, along with their test cases,
1017
        """Deletes the worksheet, provided it has no attempts on any exercises.
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
1018
1099.1.233 by Nick Chadwick
Exercise objects in the database module, along with their test cases,
1019
        Returns True if delete succeeded, or False if this worksheet has
1020
        attempts attached."""
1021
        for ws_ex in self.all_worksheet_exercises:
1022
            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
1023
                raise IntegrityError()
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
1024
1099.1.233 by Nick Chadwick
Exercise objects in the database module, along with their test cases,
1025
        self.remove_all_exercises()
1099.1.242 by Nick Chadwick
Fixed a problem with exercise editor, which wasn't editing or adding
1026
        Store.of(self).remove(self)
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
1027
1080.1.50 by Matt Giuca
ivle.database: Added WorksheetExercise (relates worksheets to exercises), and
1028
class WorksheetExercise(Storm):
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
1029
    """A link between a worksheet and one of its exercises.
1030
1031
    These may be marked optional, in which case the exercise does not count
1032
    for marking purposes. The sequence number is used to order the worksheet
1033
    ToC.
1034
    """
1035
1099.1.195 by William Grant
Rename problem to exercise in the DB.
1036
    __storm_table__ = "worksheet_exercise"
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
1037
1099.1.195 by William Grant
Rename problem to exercise in the DB.
1038
    id = Int(primary=True, name="ws_ex_id")
1080.1.50 by Matt Giuca
ivle.database: Added WorksheetExercise (relates worksheets to exercises), and
1039
1040
    worksheet_id = Int(name="worksheetid")
1041
    worksheet = Reference(worksheet_id, Worksheet.id)
1099.1.195 by William Grant
Rename problem to exercise in the DB.
1042
    exercise_id = Unicode(name="exerciseid")
1080.1.50 by Matt Giuca
ivle.database: Added WorksheetExercise (relates worksheets to exercises), and
1043
    exercise = Reference(exercise_id, Exercise.id)
1044
    optional = Bool()
1099.4.3 by Nick Chadwick
Updated the tutorial service, to now allow users to edit worksheets
1045
    active = Bool()
1046
    seq_no = Int()
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
1047
1099.1.180 by Nick Chadwick
This commit changes the tutorial service, which now almost exclusively
1048
    saves = ReferenceSet(id, "ExerciseSave.ws_ex_id")
1099.1.183 by William Grant
Fix a reference typo in ivle.database.
1049
    attempts = ReferenceSet(id, "ExerciseAttempt.ws_ex_id")
1080.1.50 by Matt Giuca
ivle.database: Added WorksheetExercise (relates worksheets to exercises), and
1050
1051
    __init__ = _kwarg_init
1052
1053
    def __repr__(self):
1054
        return "<%s %s in %s>" % (type(self).__name__, self.exercise.name,
1099.4.1 by Nick Chadwick
Working on putting worksheets into the database.
1055
                                  self.worksheet.identifier)
1080.1.55 by Matt Giuca
ivle.database: Added ExerciseAttempt and ExerciseSave classes.
1056
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.
1057
    def get_permissions(self, user, config):
1058
        return self.worksheet.get_permissions(user, config)
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
1059
1131 by William Grant
Offerings now give 'view' only to user enrolled in them. 'edit' is granted
1060
1080.1.55 by Matt Giuca
ivle.database: Added ExerciseAttempt and ExerciseSave classes.
1061
class ExerciseSave(Storm):
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
1062
    """A potential exercise solution submitted by a user for storage.
1063
1064
    This is not an actual tested attempt at an exercise, it's just a save of
1065
    the editing session.
1066
    """
1067
1099.1.195 by William Grant
Rename problem to exercise in the DB.
1068
    __storm_table__ = "exercise_save"
1099.1.180 by Nick Chadwick
This commit changes the tutorial service, which now almost exclusively
1069
    __storm_primary__ = "ws_ex_id", "user_id"
1070
1099.1.195 by William Grant
Rename problem to exercise in the DB.
1071
    ws_ex_id = Int(name="ws_ex_id")
1099.1.180 by Nick Chadwick
This commit changes the tutorial service, which now almost exclusively
1072
    worksheet_exercise = Reference(ws_ex_id, "WorksheetExercise.id")
1073
1080.1.55 by Matt Giuca
ivle.database: Added ExerciseAttempt and ExerciseSave classes.
1074
    user_id = Int(name="loginid")
1075
    user = Reference(user_id, User.id)
1076
    date = DateTime()
1077
    text = Unicode()
1078
1079
    __init__ = _kwarg_init
1080
1081
    def __repr__(self):
1082
        return "<%s %s by %s at %s>" % (type(self).__name__,
1083
            self.exercise.name, self.user.login, self.date.strftime("%c"))
1084
1085
class ExerciseAttempt(ExerciseSave):
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
1086
    """An attempt at solving an exercise.
1087
1088
    This is a special case of ExerciseSave, used when the user submits a
1089
    candidate solution. Like an ExerciseSave, it constitutes exercise solution
1090
    data.
1091
1092
    In addition, it contains information about the result of the submission:
1093
1094
     - complete - True if this submission was successful, rendering this
1095
                  exercise complete for this user in this worksheet.
1096
     - active   - True if this submission is "active" (usually true).
1097
                  Submissions may be de-activated by privileged users for
1098
                  special reasons, and then they won't count (either as a
1099
                  penalty or success), but will still be stored.
1100
    """
1101
1099.1.195 by William Grant
Rename problem to exercise in the DB.
1102
    __storm_table__ = "exercise_attempt"
1099.1.180 by Nick Chadwick
This commit changes the tutorial service, which now almost exclusively
1103
    __storm_primary__ = "ws_ex_id", "user_id", "date"
1080.1.55 by Matt Giuca
ivle.database: Added ExerciseAttempt and ExerciseSave classes.
1104
1105
    # The "text" field is the same but has a different name in the DB table
1106
    # for some reason.
1107
    text = Unicode(name="attempt")
1108
    complete = Bool()
1109
    active = Bool()
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
1110
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.
1111
    def get_permissions(self, user, config):
1099.1.113 by William Grant
Give console and tutorial services security declarations.
1112
        return set(['view']) if user is self.user else set()
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
1113
1099.1.114 by Nick Chadwick
Modified the database so that exercises are now stored in the database, rather
1114
class TestSuite(Storm):
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
1115
    """A container to group an exercise's test cases.
1116
1117
    The test suite contains some information on how to test. The function to
1118
    test, variables to set and stdin data are stored here.
1119
    """
1120
1099.1.114 by Nick Chadwick
Modified the database so that exercises are now stored in the database, rather
1121
    __storm_table__ = "test_suite"
1122
    __storm_primary__ = "exercise_id", "suiteid"
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
1123
1099.1.114 by Nick Chadwick
Modified the database so that exercises are now stored in the database, rather
1124
    suiteid = Int()
1099.1.195 by William Grant
Rename problem to exercise in the DB.
1125
    exercise_id = Unicode(name="exerciseid")
1099.1.141 by Nick Chadwick
Updated the exercises to be loaded from the database, not a local file.
1126
    description = Unicode()
1127
    seq_no = Int()
1128
    function = Unicode()
1129
    stdin = Unicode()
1099.1.114 by Nick Chadwick
Modified the database so that exercises are now stored in the database, rather
1130
    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
1131
    test_cases = ReferenceSet(suiteid, 'TestCase.suiteid', order_by="seq_no")
1132
    variables = ReferenceSet(suiteid, 'TestSuiteVar.suiteid', order_by='arg_no')
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
1133
1099.1.242 by Nick Chadwick
Fixed a problem with exercise editor, which wasn't editing or adding
1134
    def delete(self):
1099.1.233 by Nick Chadwick
Exercise objects in the database module, along with their test cases,
1135
        """Delete this suite, without asking questions."""
1427 by William Grant
Fix deletion of test suites with variables.
1136
        for variable in self.variables:
1099.1.233 by Nick Chadwick
Exercise objects in the database module, along with their test cases,
1137
            variable.delete()
1138
        for test_case in self.test_cases:
1139
            test_case.delete()
1099.1.242 by Nick Chadwick
Fixed a problem with exercise editor, which wasn't editing or adding
1140
        Store.of(self).remove(self)
1099.1.114 by Nick Chadwick
Modified the database so that exercises are now stored in the database, rather
1141
1142
class TestCase(Storm):
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
1143
    """A container for actual tests (see TestCasePart), inside a test suite.
1144
1145
    It is the lowest level shown to students on their pass/fail status."""
1146
1099.1.114 by Nick Chadwick
Modified the database so that exercises are now stored in the database, rather
1147
    __storm_table__ = "test_case"
1148
    __storm_primary__ = "testid", "suiteid"
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
1149
1099.1.114 by Nick Chadwick
Modified the database so that exercises are now stored in the database, rather
1150
    testid = Int()
1151
    suiteid = Int()
1099.1.141 by Nick Chadwick
Updated the exercises to be loaded from the database, not a local file.
1152
    suite = Reference(suiteid, "TestSuite.suiteid")
1099.1.114 by Nick Chadwick
Modified the database so that exercises are now stored in the database, rather
1153
    passmsg = Unicode()
1154
    failmsg = Unicode()
1394.1.5 by William Grant
Drop TestSuite file match default from the UI -- we don't support file tests any more.
1155
    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
1156
    seq_no = Int()
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
1157
1099.1.141 by Nick Chadwick
Updated the exercises to be loaded from the database, not a local file.
1158
    parts = ReferenceSet(testid, "TestCasePart.testid")
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
1159
1099.1.141 by Nick Chadwick
Updated the exercises to be loaded from the database, not a local file.
1160
    __init__ = _kwarg_init
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
1161
1099.1.242 by Nick Chadwick
Fixed a problem with exercise editor, which wasn't editing or adding
1162
    def delete(self):
1099.1.233 by Nick Chadwick
Exercise objects in the database module, along with their test cases,
1163
        for part in self.parts:
1164
            part.delete()
1099.1.242 by Nick Chadwick
Fixed a problem with exercise editor, which wasn't editing or adding
1165
        Store.of(self).remove(self)
1099.1.141 by Nick Chadwick
Updated the exercises to be loaded from the database, not a local file.
1166
1167
class TestSuiteVar(Storm):
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
1168
    """A variable used by an exercise test suite.
1169
1170
    This may represent a function argument or a normal variable.
1171
    """
1172
1099.1.195 by William Grant
Rename problem to exercise in the DB.
1173
    __storm_table__ = "suite_variable"
1099.1.141 by Nick Chadwick
Updated the exercises to be loaded from the database, not a local file.
1174
    __storm_primary__ = "varid"
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
1175
1099.1.141 by Nick Chadwick
Updated the exercises to be loaded from the database, not a local file.
1176
    varid = Int()
1177
    suiteid = Int()
1178
    var_name = Unicode()
1179
    var_value = Unicode()
1180
    var_type = Unicode()
1181
    arg_no = Int()
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
1182
1099.1.141 by Nick Chadwick
Updated the exercises to be loaded from the database, not a local file.
1183
    suite = Reference(suiteid, "TestSuite.suiteid")
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
1184
1099.1.141 by Nick Chadwick
Updated the exercises to be loaded from the database, not a local file.
1185
    __init__ = _kwarg_init
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
1186
1099.1.242 by Nick Chadwick
Fixed a problem with exercise editor, which wasn't editing or adding
1187
    def delete(self):
1188
        Store.of(self).remove(self)
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
1189
1099.1.141 by Nick Chadwick
Updated the exercises to be loaded from the database, not a local file.
1190
class TestCasePart(Storm):
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
1191
    """An actual piece of code to test an exercise solution."""
1192
1099.1.195 by William Grant
Rename problem to exercise in the DB.
1193
    __storm_table__ = "test_case_part"
1099.1.141 by Nick Chadwick
Updated the exercises to be loaded from the database, not a local file.
1194
    __storm_primary__ = "partid"
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
1195
1099.1.141 by Nick Chadwick
Updated the exercises to be loaded from the database, not a local file.
1196
    partid = Int()
1197
    testid = Int()
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
1198
1099.1.141 by Nick Chadwick
Updated the exercises to be loaded from the database, not a local file.
1199
    part_type = Unicode()
1200
    test_type = Unicode()
1201
    data = Unicode()
1202
    filename = Unicode()
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
1203
1099.1.141 by Nick Chadwick
Updated the exercises to be loaded from the database, not a local file.
1204
    test = Reference(testid, "TestCase.testid")
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
1205
1099.1.114 by Nick Chadwick
Modified the database so that exercises are now stored in the database, rather
1206
    __init__ = _kwarg_init
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
1207
1099.1.242 by Nick Chadwick
Fixed a problem with exercise editor, which wasn't editing or adding
1208
    def delete(self):
1209
        Store.of(self).remove(self)