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