~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
    url = Unicode()
253
1080.1.26 by me at id
ivle.database: Add an Enrolment class, and reference(set)s between all of the
254
    offerings = ReferenceSet(id, 'Offering.subject_id')
255
1080.1.25 by me at id
ivle.database: Add Subject, Semester and Offering.
256
    __init__ = _kwarg_init
257
258
    def __repr__(self):
259
        return "<%s '%s'>" % (type(self).__name__, self.short_name)
260
1099.1.110 by William Grant
Implement an authorization system in the new framework. This breaks the REST
261
    def get_permissions(self, user):
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
262
        """Determine privileges held by a user over this object.
263
264
        If the user requesting privileges is an admin, they may edit.
265
        Otherwise they may only read.
266
        """
1099.1.110 by William Grant
Implement an authorization system in the new framework. This breaks the REST
267
        perms = set()
268
        if user is not None:
269
            perms.add('view')
1101 by William Grant
Privileges (apart from admin) are now offering-local, not global.
270
            if user.admin:
1099.1.141 by Nick Chadwick
Updated the exercises to be loaded from the database, not a local file.
271
                perms.add('edit')
1099.1.110 by William Grant
Implement an authorization system in the new framework. This breaks the REST
272
        return perms
273
1195.1.1 by Matt Giuca
ivle.database: Added Subject.active_offerings, which can be used by tools
274
    def active_offerings(self):
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
275
        """Find active offerings for this subject.
276
277
        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
278
        (offerings whose semester.state is "current"). There should be 0 or 1
279
        elements in this sequence, but it's possible there are more.
280
        """
1195.1.6 by Matt Giuca
ivle.database: Added Subject.offering_for_semester.
281
        return self.offerings.find(Offering.semester_id == Semester.id,
282
                                   Semester.state == u'current')
283
284
    def offering_for_semester(self, year, semester):
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
285
        """Get the offering for the given year/semester, or None.
286
287
        @param year: A string representation of the year.
288
        @param semester: A string representation of the semester.
289
        """
1195.1.6 by Matt Giuca
ivle.database: Added Subject.offering_for_semester.
290
        return self.offerings.find(Offering.semester_id == Semester.id,
291
                               Semester.year == unicode(year),
292
                               Semester.semester == unicode(semester)).one()
1195.1.1 by Matt Giuca
ivle.database: Added Subject.active_offerings, which can be used by tools
293
1080.1.26 by me at id
ivle.database: Add an Enrolment class, and reference(set)s between all of the
294
class Semester(Storm):
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
295
    """A semester in which subjects can be run."""
296
1080.1.25 by me at id
ivle.database: Add Subject, Semester and Offering.
297
    __storm_table__ = "semester"
298
299
    id = Int(primary=True, name="semesterid")
300
    year = Unicode()
301
    semester = Unicode()
1104 by William Grant
Replace Semester.active with Semester.state, allowing more useful state
302
    state = Unicode()
1080.1.25 by me at id
ivle.database: Add Subject, Semester and Offering.
303
1080.1.26 by me at id
ivle.database: Add an Enrolment class, and reference(set)s between all of the
304
    offerings = ReferenceSet(id, 'Offering.semester_id')
1124 by William Grant
Add Semester.enrolments.
305
    enrolments = ReferenceSet(id,
306
                              'Offering.semester_id',
307
                              'Offering.id',
308
                              'Enrolment.offering_id')
1080.1.26 by me at id
ivle.database: Add an Enrolment class, and reference(set)s between all of the
309
1080.1.25 by me at id
ivle.database: Add Subject, Semester and Offering.
310
    __init__ = _kwarg_init
311
312
    def __repr__(self):
313
        return "<%s %s/%s>" % (type(self).__name__, self.year, self.semester)
314
1080.1.26 by me at id
ivle.database: Add an Enrolment class, and reference(set)s between all of the
315
class Offering(Storm):
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
316
    """An offering of a subject in a particular semester."""
317
1080.1.25 by me at id
ivle.database: Add Subject, Semester and Offering.
318
    __storm_table__ = "offering"
319
320
    id = Int(primary=True, name="offeringid")
321
    subject_id = Int(name="subject")
322
    subject = Reference(subject_id, Subject.id)
323
    semester_id = Int(name="semesterid")
324
    semester = Reference(semester_id, Semester.id)
325
    groups_student_permissions = Unicode()
326
1080.1.26 by me at id
ivle.database: Add an Enrolment class, and reference(set)s between all of the
327
    enrolments = ReferenceSet(id, 'Enrolment.offering_id')
1080.1.79 by William Grant
ivle.database.Offering: Add a members ReferenceSet.
328
    members = ReferenceSet(id,
329
                           'Enrolment.offering_id',
330
                           'Enrolment.user_id',
331
                           'User.id')
1080.1.76 by William Grant
ivle.database.Offering: Add project_sets referenceset.
332
    project_sets = ReferenceSet(id, 'ProjectSet.offering_id')
1080.1.26 by me at id
ivle.database: Add an Enrolment class, and reference(set)s between all of the
333
1099.1.180 by Nick Chadwick
This commit changes the tutorial service, which now almost exclusively
334
    worksheets = ReferenceSet(id, 
335
        'Worksheet.offering_id', 
1099.1.212 by Nick Chadwick
Added a new page to display exercises. This will then be modified to
336
        order_by="seq_no"
1099.1.180 by Nick Chadwick
This commit changes the tutorial service, which now almost exclusively
337
    )
1099.1.114 by Nick Chadwick
Modified the database so that exercises are now stored in the database, rather
338
1080.1.25 by me at id
ivle.database: Add Subject, Semester and Offering.
339
    __init__ = _kwarg_init
340
341
    def __repr__(self):
342
        return "<%s %r in %r>" % (type(self).__name__, self.subject,
343
                                  self.semester)
1080.1.26 by me at id
ivle.database: Add an Enrolment class, and reference(set)s between all of the
344
1110 by William Grant
ivle-enrol now allows updating of existing enrolments. It also sets the role.
345
    def enrol(self, user, role=u'student'):
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
346
        """Enrol a user in this offering.
347
348
        Enrolments handle both the staff and student cases. The role controls
349
        the privileges granted by this enrolment.
350
        """
1110 by William Grant
ivle-enrol now allows updating of existing enrolments. It also sets the role.
351
        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
352
                               Enrolment.user_id == user.id,
1110 by William Grant
ivle-enrol now allows updating of existing enrolments. It also sets the role.
353
                               Enrolment.offering_id == self.id).one()
354
355
        if enrolment is None:
356
            enrolment = Enrolment(user=user, offering=self)
357
            self.enrolments.add(enrolment)
358
359
        enrolment.active = True
360
        enrolment.role = role
1080.1.61 by William Grant
ivle.database: Add an Offering.enrol(user) method, which enrols the user in
361
1132 by William Grant
Add Offering.unenrol(), to unenrol a user from an offering.
362
    def unenrol(self, user):
363
        '''Unenrol a user from this offering.'''
364
        enrolment = Store.of(self).find(Enrolment,
365
                               Enrolment.user_id == user.id,
366
                               Enrolment.offering_id == self.id).one()
367
        Store.of(enrolment).remove(enrolment)
368
1099.1.141 by Nick Chadwick
Updated the exercises to be loaded from the database, not a local file.
369
    def get_permissions(self, user):
370
        perms = set()
371
        if user is not None:
1131 by William Grant
Offerings now give 'view' only to user enrolled in them. 'edit' is granted
372
            enrolment = self.get_enrolment(user)
373
            if enrolment or user.admin:
374
                perms.add('view')
375
            if (enrolment and enrolment.role in (u'tutor', u'lecturer')) \
376
               or user.admin:
1099.1.141 by Nick Chadwick
Updated the exercises to be loaded from the database, not a local file.
377
                perms.add('edit')
1377 by Matt Giuca
database: Added finer-grained enrol permissions on offerings.
378
                # XXX Bug #493945 -- should tutors have these permissions?
1376 by Matt Giuca
database: More granular permissions on offerings: Added 'enrol' permission.
379
                # Potentially move into the next category (lecturer & admin)
1377 by Matt Giuca
database: Added finer-grained enrol permissions on offerings.
380
                perms.add('enrol')          # Can see enrolment screen at all
381
                perms.add('enrol_student')  # Can enrol students
382
            if (enrolment and enrolment.role in (u'lecturer')) or user.admin:
383
                perms.add('enrol_tutor')    # Can enrol tutors
384
            if user.admin:
385
                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.
386
        return perms
387
1129 by William Grant
Move the group admin view to per-offering.
388
    def get_enrolment(self, user):
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
389
        """Find the user's enrolment in this offering."""
1129 by William Grant
Move the group admin view to per-offering.
390
        try:
391
            enrolment = self.enrolments.find(user=user).one()
392
        except NotOneError:
393
            enrolment = None
394
395
        return enrolment
396
1165.3.42 by William Grant
Fix ProjectView's total assigned count by dealing with ResultSets only.
397
    def get_members_by_role(self, role):
398
        return Store.of(self).find(User,
399
                Enrolment.user_id == User.id,
400
                Enrolment.offering_id == self.id,
401
                Enrolment.role == role
1364 by Matt Giuca
database: Offering.get_members_by_role now sorts the data.
402
                ).order_by(User.login)
1165.3.42 by William Grant
Fix ProjectView's total assigned count by dealing with ResultSets only.
403
1165.3.43 by William Grant
Turn the new database methods into properties where appropriate.
404
    @property
405
    def students(self):
1165.3.42 by William Grant
Fix ProjectView's total assigned count by dealing with ResultSets only.
406
        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
407
1080.1.26 by me at id
ivle.database: Add an Enrolment class, and reference(set)s between all of the
408
class Enrolment(Storm):
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
409
    """An enrolment of a user in an offering.
410
411
    This represents the roles of both staff and students.
412
    """
413
1080.1.26 by me at id
ivle.database: Add an Enrolment class, and reference(set)s between all of the
414
    __storm_table__ = "enrolment"
415
    __storm_primary__ = "user_id", "offering_id"
416
417
    user_id = Int(name="loginid")
418
    user = Reference(user_id, User.id)
419
    offering_id = Int(name="offeringid")
420
    offering = Reference(offering_id, Offering.id)
1101 by William Grant
Privileges (apart from admin) are now offering-local, not global.
421
    role = Unicode()
1080.1.26 by me at id
ivle.database: Add an Enrolment class, and reference(set)s between all of the
422
    notes = Unicode()
423
    active = Bool()
424
1080.1.81 by William Grant
ivle.database.Enrolment: Add a groups attribute, containing groups of which
425
    @property
426
    def groups(self):
427
        return Store.of(self).find(ProjectGroup,
428
                ProjectSet.offering_id == self.offering.id,
429
                ProjectGroup.project_set_id == ProjectSet.id,
430
                ProjectGroupMembership.project_group_id == ProjectGroup.id,
431
                ProjectGroupMembership.user_id == self.user.id)
432
1080.1.26 by me at id
ivle.database: Add an Enrolment class, and reference(set)s between all of the
433
    __init__ = _kwarg_init
434
435
    def __repr__(self):
436
        return "<%s %r in %r>" % (type(self).__name__, self.user,
437
                                  self.offering)
1080.1.36 by William Grant
ivle.database: Add ProjectSet, Project, ProjectGroup, ProjectGroupMembership
438
1080.1.39 by Matt Giuca
ivle.database: Added __all__ to the top of the file.
439
# PROJECTS #
440
1080.1.36 by William Grant
ivle.database: Add ProjectSet, Project, ProjectGroup, ProjectGroupMembership
441
class ProjectSet(Storm):
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
442
    """A set of projects that share common groups.
443
444
    Each student project group is attached to a project set. The group is
445
    valid for all projects in the group's set.
446
    """
447
1080.1.36 by William Grant
ivle.database: Add ProjectSet, Project, ProjectGroup, ProjectGroupMembership
448
    __storm_table__ = "project_set"
449
450
    id = Int(name="projectsetid", primary=True)
451
    offering_id = Int(name="offeringid")
452
    offering = Reference(offering_id, Offering.id)
453
    max_students_per_group = Int()
454
1080.1.77 by William Grant
ivle.database.ProjectSet: Add projects and project_groups referencesets.
455
    projects = ReferenceSet(id, 'Project.project_set_id')
456
    project_groups = ReferenceSet(id, 'ProjectGroup.project_set_id')
457
1080.1.36 by William Grant
ivle.database: Add ProjectSet, Project, ProjectGroup, ProjectGroupMembership
458
    __init__ = _kwarg_init
459
460
    def __repr__(self):
461
        return "<%s %d in %r>" % (type(self).__name__, self.id,
462
                                  self.offering)
463
1165.3.2 by Nick Chadwick
Created a new view for IVLE, allowing lecturers and tutors to
464
    def get_permissions(self, user):
465
        return self.offering.get_permissions(user)
466
1165.3.43 by William Grant
Turn the new database methods into properties where appropriate.
467
    @property
1375.1.3 by William Grant
Add ProjectSet.is_group, a property determining whether it is a group or solo set.
468
    def is_group(self):
469
        return self.max_students_per_group is not None
470
471
    @property
1165.3.43 by William Grant
Turn the new database methods into properties where appropriate.
472
    def assigned(self):
473
        """Get the entities (groups or users) assigned to submit this project.
474
475
        This will be a Storm ResultSet.
476
        """
477
        #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.
478
        if self.is_group:
479
            return self.project_groups
480
        else:
1165.3.43 by William Grant
Turn the new database methods into properties where appropriate.
481
            return self.offering.students
1165.4.2 by Nick Chadwick
Added a get_assigned method to project sets, to find out which objects
482
1080.1.36 by William Grant
ivle.database: Add ProjectSet, Project, ProjectGroup, ProjectGroupMembership
483
class Project(Storm):
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
484
    """A student project for which submissions can be made."""
485
1080.1.36 by William Grant
ivle.database: Add ProjectSet, Project, ProjectGroup, ProjectGroupMembership
486
    __storm_table__ = "project"
487
488
    id = Int(name="projectid", primary=True)
1165.1.4 by William Grant
Add database classes for assessed, project_extension and project_submission.
489
    name = Unicode()
490
    short_name = Unicode()
1080.1.36 by William Grant
ivle.database: Add ProjectSet, Project, ProjectGroup, ProjectGroupMembership
491
    synopsis = Unicode()
492
    url = Unicode()
493
    project_set_id = Int(name="projectsetid")
494
    project_set = Reference(project_set_id, ProjectSet.id)
495
    deadline = DateTime()
496
1165.1.5 by William Grant
Add relevant ReferenceSets to Project and Assessed.
497
    assesseds = ReferenceSet(id, 'Assessed.project_id')
498
    submissions = ReferenceSet(id,
499
                               'Assessed.project_id',
500
                               'Assessed.id',
501
                               'ProjectSubmission.assessed_id')
502
1080.1.36 by William Grant
ivle.database: Add ProjectSet, Project, ProjectGroup, ProjectGroupMembership
503
    __init__ = _kwarg_init
504
505
    def __repr__(self):
1165.1.4 by William Grant
Add database classes for assessed, project_extension and project_submission.
506
        return "<%s '%s' in %r>" % (type(self).__name__, self.short_name,
1080.1.36 by William Grant
ivle.database: Add ProjectSet, Project, ProjectGroup, ProjectGroupMembership
507
                                  self.project_set.offering)
508
1165.1.19 by William Grant
Add Project.submit(), to create a submission for the principal, path and rev.
509
    def can_submit(self, principal):
510
        return (self in principal.get_projects() and
511
                self.deadline > datetime.datetime.now())
512
1165.1.42 by William Grant
Record who submitted each submission.
513
    def submit(self, principal, path, revision, who):
514
        """Submit a Subversion path and revision to a project.
515
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
516
        @param principal: The owner of the Subversion repository, and the
517
                          entity on behalf of whom the submission is being made
518
        @param path: A path within that repository to submit.
519
        @param revision: The revision of that path to submit.
520
        @param who: The user who is actually making the submission.
1165.1.42 by William Grant
Record who submitted each submission.
521
        """
522
1165.1.19 by William Grant
Add Project.submit(), to create a submission for the principal, path and rev.
523
        if not self.can_submit(principal):
524
            raise Exception('cannot submit')
525
526
        a = Assessed.get(Store.of(self), principal, self)
527
        ps = ProjectSubmission()
528
        ps.path = path
529
        ps.revision = revision
530
        ps.date_submitted = datetime.datetime.now()
531
        ps.assessed = a
1165.1.42 by William Grant
Record who submitted each submission.
532
        ps.submitter = who
1165.1.19 by William Grant
Add Project.submit(), to create a submission for the principal, path and rev.
533
534
        return ps
535
1165.3.2 by Nick Chadwick
Created a new view for IVLE, allowing lecturers and tutors to
536
    def get_permissions(self, user):
537
        return self.project_set.offering.get_permissions(user)
538
1165.3.45 by William Grant
Add Project.latest_submissions, which excludes superseded submissions.
539
    @property
540
    def latest_submissions(self):
541
        """Return the latest submission for each Assessed."""
542
        return Store.of(self).find(ProjectSubmission,
543
            Assessed.project_id == self.id,
544
            ProjectSubmission.assessed_id == Assessed.id,
545
            ProjectSubmission.date_submitted == Select(
546
                    Max(ProjectSubmission.date_submitted),
547
                    ProjectSubmission.assessed_id == Assessed.id,
548
                    tables=ProjectSubmission
549
            )
550
        )
551
1165.1.19 by William Grant
Add Project.submit(), to create a submission for the principal, path and rev.
552
1080.1.36 by William Grant
ivle.database: Add ProjectSet, Project, ProjectGroup, ProjectGroupMembership
553
class ProjectGroup(Storm):
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
554
    """A group of students working together on a project."""
555
1080.1.36 by William Grant
ivle.database: Add ProjectSet, Project, ProjectGroup, ProjectGroupMembership
556
    __storm_table__ = "project_group"
557
558
    id = Int(name="groupid", primary=True)
559
    name = Unicode(name="groupnm")
560
    project_set_id = Int(name="projectsetid")
561
    project_set = Reference(project_set_id, ProjectSet.id)
562
    nick = Unicode()
563
    created_by_id = Int(name="createdby")
564
    created_by = Reference(created_by_id, User.id)
565
    epoch = DateTime()
566
1080.1.78 by William Grant
ivle.database.ProjectGroup.members: Use a ReferenceSet.
567
    members = ReferenceSet(id,
568
                           "ProjectGroupMembership.project_group_id",
569
                           "ProjectGroupMembership.user_id",
570
                           "User.id")
571
1080.1.36 by William Grant
ivle.database: Add ProjectSet, Project, ProjectGroup, ProjectGroupMembership
572
    __init__ = _kwarg_init
573
574
    def __repr__(self):
575
        return "<%s %s in %r>" % (type(self).__name__, self.name,
576
                                  self.project_set.offering)
577
1165.1.26 by William Grant
Add display_name properties to users and groups.
578
    @property
579
    def display_name(self):
1165.3.69 by Matt Giuca
ivle.database: User/ProjectGroup: Added 'short_name' methods so we have a
580
        """Returns the "nice name" of the user or group."""
581
        return self.nick
582
583
    @property
584
    def short_name(self):
585
        """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.
586
        return self.name
1165.1.26 by William Grant
Add display_name properties to users and groups.
587
1165.1.12 by William Grant
Implement ProjectGroup.get_projects(), with identical interface.
588
    def get_projects(self, offering=None, active_only=True):
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
589
        '''Find projects that the group can submit.
1165.1.12 by William Grant
Implement ProjectGroup.get_projects(), with identical interface.
590
591
        This will include projects in the project set which owns this group,
592
        unless the project set disallows groups (in which case none will be
593
        returned).
594
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
595
        @param active_only: Whether to only search active offerings.
596
        @param offering: An optional offering to restrict the search to.
1165.1.12 by William Grant
Implement ProjectGroup.get_projects(), with identical interface.
597
        '''
598
        return Store.of(self).find(Project,
599
            Project.project_set_id == ProjectSet.id,
600
            ProjectSet.id == self.project_set.id,
1165.1.46 by William Grant
Respect the new max_students_per_group semantics in Python.
601
            ProjectSet.max_students_per_group != None,
1165.1.12 by William Grant
Implement ProjectGroup.get_projects(), with identical interface.
602
            ProjectSet.offering_id == Offering.id,
603
            (offering is None) or (Offering.id == offering.id),
604
            Semester.id == Offering.semester_id,
605
            (not active_only) or (Semester.state == u'current'))
606
607
1165.1.7 by William Grant
Grant submit_project on users to themselves, and on groups to their members.
608
    def get_permissions(self, user):
609
        if user.admin or user in self.members:
610
            return set(['submit_project'])
611
        else:
612
            return set()
613
1080.1.36 by William Grant
ivle.database: Add ProjectSet, Project, ProjectGroup, ProjectGroupMembership
614
class ProjectGroupMembership(Storm):
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
615
    """A student's membership in a project group."""
616
1080.1.36 by William Grant
ivle.database: Add ProjectSet, Project, ProjectGroup, ProjectGroupMembership
617
    __storm_table__ = "group_member"
618
    __storm_primary__ = "user_id", "project_group_id"
619
620
    user_id = Int(name="loginid")
621
    user = Reference(user_id, User.id)
622
    project_group_id = Int(name="groupid")
623
    project_group = Reference(project_group_id, ProjectGroup.id)
624
625
    __init__ = _kwarg_init
626
627
    def __repr__(self):
628
        return "<%s %r in %r>" % (type(self).__name__, self.user,
629
                                  self.project_group)
630
1165.1.4 by William Grant
Add database classes for assessed, project_extension and project_submission.
631
class Assessed(Storm):
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
632
    """A composite of a user or group combined with a project.
633
634
    Each project submission and extension refers to an Assessed. It is the
635
    sole specifier of the repository and project.
636
    """
637
1165.1.4 by William Grant
Add database classes for assessed, project_extension and project_submission.
638
    __storm_table__ = "assessed"
639
640
    id = Int(name="assessedid", primary=True)
641
    user_id = Int(name="loginid")
642
    user = Reference(user_id, User.id)
643
    project_group_id = Int(name="groupid")
644
    project_group = Reference(project_group_id, ProjectGroup.id)
645
646
    project_id = Int(name="projectid")
647
    project = Reference(project_id, Project.id)
648
1165.1.5 by William Grant
Add relevant ReferenceSets to Project and Assessed.
649
    extensions = ReferenceSet(id, 'ProjectExtension.assessed_id')
650
    submissions = ReferenceSet(id, 'ProjectSubmission.assessed_id')
651
1165.1.4 by William Grant
Add database classes for assessed, project_extension and project_submission.
652
    def __repr__(self):
653
        return "<%s %r in %r>" % (type(self).__name__,
654
            self.user or self.project_group, self.project)
655
1165.3.12 by William Grant
Add an Assessed.principal property.
656
    @property
1165.3.49 by Matt Giuca
ivle.database.Assessed: Added property is_group.
657
    def is_group(self):
658
        """True if the Assessed is a group, False if it is a user."""
659
        return self.project_group is not None
660
661
    @property
1165.3.12 by William Grant
Add an Assessed.principal property.
662
    def principal(self):
663
        return self.project_group or self.user
664
1165.1.18 by William Grant
Add a method to retrieve or create an Assessed given a principal and project.
665
    @classmethod
666
    def get(cls, store, principal, project):
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
667
        """Find or create an Assessed for the given user or group and project.
668
669
        @param principal: The user or group.
670
        @param project: The project.
671
        """
1165.1.18 by William Grant
Add a method to retrieve or create an Assessed given a principal and project.
672
        t = type(principal)
673
        if t not in (User, ProjectGroup):
674
            raise AssertionError('principal must be User or ProjectGroup')
675
676
        a = store.find(cls,
677
            (t is User) or (cls.project_group_id == principal.id),
678
            (t is ProjectGroup) or (cls.user_id == principal.id),
679
            Project.id == project.id).one()
680
681
        if a is None:
682
            a = cls()
683
            if t is User:
684
                a.user = principal
685
            else:
686
                a.project_group = principal
687
            a.project = project
688
            store.add(a)
689
690
        return a
691
692
1165.1.4 by William Grant
Add database classes for assessed, project_extension and project_submission.
693
class ProjectExtension(Storm):
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
694
    """An extension granted to a user or group on a particular project.
695
696
    The user or group and project are specified by the Assessed.
697
    """
698
1165.1.4 by William Grant
Add database classes for assessed, project_extension and project_submission.
699
    __storm_table__ = "project_extension"
700
701
    id = Int(name="extensionid", primary=True)
702
    assessed_id = Int(name="assessedid")
703
    assessed = Reference(assessed_id, Assessed.id)
704
    deadline = DateTime()
705
    approver_id = Int(name="approver")
706
    approver = Reference(approver_id, User.id)
707
    notes = Unicode()
708
709
class ProjectSubmission(Storm):
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
710
    """A submission from a user or group repository to a particular project.
711
712
    The content of a submission is a single path and revision inside a
713
    repository. The repository is that owned by the submission's user and
714
    group, while the path and revision are explicit.
715
716
    The user or group and project are specified by the Assessed.
717
    """
718
1165.1.4 by William Grant
Add database classes for assessed, project_extension and project_submission.
719
    __storm_table__ = "project_submission"
720
721
    id = Int(name="submissionid", primary=True)
722
    assessed_id = Int(name="assessedid")
723
    assessed = Reference(assessed_id, Assessed.id)
724
    path = Unicode()
725
    revision = Int()
1165.1.42 by William Grant
Record who submitted each submission.
726
    submitter_id = Int(name="submitter")
727
    submitter = Reference(submitter_id, User.id)
1165.1.4 by William Grant
Add database classes for assessed, project_extension and project_submission.
728
    date_submitted = DateTime()
729
730
1080.1.40 by Matt Giuca
ivle.database: Added Worksheet and Exercise classes (more to come in this
731
# WORKSHEETS AND EXERCISES #
732
733
class Exercise(Storm):
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
734
    """An exercise for students to complete in a worksheet.
735
736
    An exercise may be present in any number of worksheets.
737
    """
738
1099.1.195 by William Grant
Rename problem to exercise in the DB.
739
    __storm_table__ = "exercise"
1099.1.114 by Nick Chadwick
Modified the database so that exercises are now stored in the database, rather
740
    id = Unicode(primary=True, name="identifier")
741
    name = Unicode()
742
    description = Unicode()
743
    partial = Unicode()
744
    solution = Unicode()
745
    include = Unicode()
746
    num_rows = Int()
1080.1.40 by Matt Giuca
ivle.database: Added Worksheet and Exercise classes (more to come in this
747
1099.6.2 by Nick Chadwick
Added a listing of all exercises
748
    worksheet_exercises =  ReferenceSet(id,
749
        'WorksheetExercise.exercise_id')
750
1080.1.50 by Matt Giuca
ivle.database: Added WorksheetExercise (relates worksheets to exercises), and
751
    worksheets = ReferenceSet(id,
752
        'WorksheetExercise.exercise_id',
753
        'WorksheetExercise.worksheet_id',
754
        'Worksheet.id'
755
    )
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
756
1099.1.212 by Nick Chadwick
Added a new page to display exercises. This will then be modified to
757
    test_suites = ReferenceSet(id, 
758
        'TestSuite.exercise_id',
759
        order_by='seq_no')
1080.1.50 by Matt Giuca
ivle.database: Added WorksheetExercise (relates worksheets to exercises), and
760
1080.1.40 by Matt Giuca
ivle.database: Added Worksheet and Exercise classes (more to come in this
761
    __init__ = _kwarg_init
762
763
    def __repr__(self):
764
        return "<%s %s>" % (type(self).__name__, self.name)
765
1099.1.210 by Nick Chadwick
Modified the database layer so that exercises have a get_permissions
766
    def get_permissions(self, user):
767
        perms = set()
1099.1.234 by Nick Chadwick
Permissions for editing and deleting exercises now come from the
768
        roles = set()
1099.1.210 by Nick Chadwick
Modified the database layer so that exercises have a get_permissions
769
        if user is not None:
1101 by William Grant
Privileges (apart from admin) are now offering-local, not global.
770
            if user.admin:
1099.1.210 by Nick Chadwick
Modified the database layer so that exercises have a get_permissions
771
                perms.add('edit')
772
                perms.add('view')
1165.2.2 by Nick Chadwick
fixed a bug in which tutors weren't able to edit the exercises of
773
            elif u'lecturer' in set((e.role for e in user.active_enrolments)):
774
                perms.add('edit')
775
                perms.add('view')
776
            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
777
                perms.add('edit')
778
                perms.add('view')
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
779
1099.1.210 by Nick Chadwick
Modified the database layer so that exercises have a get_permissions
780
        return perms
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
781
1099.6.3 by Nick Chadwick
Edited the exercise service to delete individual parts of an exercise.
782
    def get_description(self):
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
783
        """Return the description interpreted as reStructuredText."""
1099.1.232 by Nick Chadwick
Removed XML from database. RST now generates a full xml document, not
784
        return rst(self.description)
1080.1.51 by Matt Giuca
tutorial: Replaced call to ivle.db.create_worksheet with local code (roughly
785
1099.1.242 by Nick Chadwick
Fixed a problem with exercise editor, which wasn't editing or adding
786
    def delete(self):
787
        """Deletes the exercise, providing it has no associated worksheets."""
788
        if (self.worksheet_exercises.count() > 0):
789
            raise IntegrityError()
1099.1.233 by Nick Chadwick
Exercise objects in the database module, along with their test cases,
790
        for suite in self.test_suites:
791
            suite.delete()
1099.1.242 by Nick Chadwick
Fixed a problem with exercise editor, which wasn't editing or adding
792
        Store.of(self).remove(self)
1099.1.233 by Nick Chadwick
Exercise objects in the database module, along with their test cases,
793
1080.1.40 by Matt Giuca
ivle.database: Added Worksheet and Exercise classes (more to come in this
794
class Worksheet(Storm):
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
795
    """A worksheet with exercises for students to complete.
796
797
    Worksheets are owned by offerings.
798
    """
799
1080.1.40 by Matt Giuca
ivle.database: Added Worksheet and Exercise classes (more to come in this
800
    __storm_table__ = "worksheet"
801
802
    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
803
    offering_id = Int(name="offeringid")
1099.4.1 by Nick Chadwick
Working on putting worksheets into the database.
804
    identifier = Unicode()
805
    name = Unicode()
1080.1.40 by Matt Giuca
ivle.database: Added Worksheet and Exercise classes (more to come in this
806
    assessable = Bool()
1099.4.1 by Nick Chadwick
Working on putting worksheets into the database.
807
    data = Unicode()
1099.1.180 by Nick Chadwick
This commit changes the tutorial service, which now almost exclusively
808
    seq_no = Int()
809
    format = Unicode()
1080.1.40 by Matt Giuca
ivle.database: Added Worksheet and Exercise classes (more to come in this
810
1099.1.141 by Nick Chadwick
Updated the exercises to be loaded from the database, not a local file.
811
    attempts = ReferenceSet(id, "ExerciseAttempt.worksheetid")
1099.1.118 by William Grant
Fix a bad reference introduced with the worksheet changes.
812
    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
813
1103 by William Grant
Worksheet.worksheet_exercises now only contains active ones.
814
    all_worksheet_exercises = ReferenceSet(id,
815
        'WorksheetExercise.worksheet_id')
816
817
    # Use worksheet_exercises to get access to the *active* WorksheetExercise
818
    # 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
819
    # "optional" field.
1099.1.220 by Nick Chadwick
Merged from trunk
820
1103 by William Grant
Worksheet.worksheet_exercises now only contains active ones.
821
    @property
822
    def worksheet_exercises(self):
823
        return self.all_worksheet_exercises.find(active=True)
1080.1.50 by Matt Giuca
ivle.database: Added WorksheetExercise (relates worksheets to exercises), and
824
1080.1.40 by Matt Giuca
ivle.database: Added Worksheet and Exercise classes (more to come in this
825
    __init__ = _kwarg_init
826
827
    def __repr__(self):
828
        return "<%s %s>" % (type(self).__name__, self.name)
1080.1.47 by Matt Giuca
ivle.database: Added Worksheet.get_by_name method.
829
1099.1.242 by Nick Chadwick
Fixed a problem with exercise editor, which wasn't editing or adding
830
    def remove_all_exercises(self):
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
831
        """Remove all exercises from this worksheet.
832
1080.1.51 by Matt Giuca
tutorial: Replaced call to ivle.db.create_worksheet with local code (roughly
833
        This does not delete the exercises themselves. It just removes them
834
        from the worksheet.
835
        """
1099.1.242 by Nick Chadwick
Fixed a problem with exercise editor, which wasn't editing or adding
836
        store = Store.of(self)
837
        for ws_ex in self.all_worksheet_exercises:
838
            if ws_ex.saves.count() > 0 or ws_ex.attempts.count() > 0:
839
                raise IntegrityError()
1080.1.51 by Matt Giuca
tutorial: Replaced call to ivle.db.create_worksheet with local code (roughly
840
        store.find(WorksheetExercise,
841
            WorksheetExercise.worksheet == self).remove()
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
842
1099.1.141 by Nick Chadwick
Updated the exercises to be loaded from the database, not a local file.
843
    def get_permissions(self, user):
844
        return self.offering.get_permissions(user)
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
845
1099.1.220 by Nick Chadwick
Merged from trunk
846
    def get_xml(self):
847
        """Returns the xml of this worksheet, converts from rst if required."""
848
        if self.format == u'rst':
1099.1.232 by Nick Chadwick
Removed XML from database. RST now generates a full xml document, not
849
            ws_xml = rst(self.data)
1099.1.220 by Nick Chadwick
Merged from trunk
850
            return ws_xml
851
        else:
852
            return self.data
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
853
1099.1.242 by Nick Chadwick
Fixed a problem with exercise editor, which wasn't editing or adding
854
    def delete(self):
1099.1.233 by Nick Chadwick
Exercise objects in the database module, along with their test cases,
855
        """Deletes the worksheet, provided it has no attempts on any exercises.
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
856
1099.1.233 by Nick Chadwick
Exercise objects in the database module, along with their test cases,
857
        Returns True if delete succeeded, or False if this worksheet has
858
        attempts attached."""
859
        for ws_ex in self.all_worksheet_exercises:
860
            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
861
                raise IntegrityError()
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
862
1099.1.233 by Nick Chadwick
Exercise objects in the database module, along with their test cases,
863
        self.remove_all_exercises()
1099.1.242 by Nick Chadwick
Fixed a problem with exercise editor, which wasn't editing or adding
864
        Store.of(self).remove(self)
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
865
1080.1.50 by Matt Giuca
ivle.database: Added WorksheetExercise (relates worksheets to exercises), and
866
class WorksheetExercise(Storm):
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
867
    """A link between a worksheet and one of its exercises.
868
869
    These may be marked optional, in which case the exercise does not count
870
    for marking purposes. The sequence number is used to order the worksheet
871
    ToC.
872
    """
873
1099.1.195 by William Grant
Rename problem to exercise in the DB.
874
    __storm_table__ = "worksheet_exercise"
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
875
1099.1.195 by William Grant
Rename problem to exercise in the DB.
876
    id = Int(primary=True, name="ws_ex_id")
1080.1.50 by Matt Giuca
ivle.database: Added WorksheetExercise (relates worksheets to exercises), and
877
878
    worksheet_id = Int(name="worksheetid")
879
    worksheet = Reference(worksheet_id, Worksheet.id)
1099.1.195 by William Grant
Rename problem to exercise in the DB.
880
    exercise_id = Unicode(name="exerciseid")
1080.1.50 by Matt Giuca
ivle.database: Added WorksheetExercise (relates worksheets to exercises), and
881
    exercise = Reference(exercise_id, Exercise.id)
882
    optional = Bool()
1099.4.3 by Nick Chadwick
Updated the tutorial service, to now allow users to edit worksheets
883
    active = Bool()
884
    seq_no = Int()
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
885
1099.1.180 by Nick Chadwick
This commit changes the tutorial service, which now almost exclusively
886
    saves = ReferenceSet(id, "ExerciseSave.ws_ex_id")
1099.1.183 by William Grant
Fix a reference typo in ivle.database.
887
    attempts = ReferenceSet(id, "ExerciseAttempt.ws_ex_id")
1080.1.50 by Matt Giuca
ivle.database: Added WorksheetExercise (relates worksheets to exercises), and
888
889
    __init__ = _kwarg_init
890
891
    def __repr__(self):
892
        return "<%s %s in %s>" % (type(self).__name__, self.exercise.name,
1099.4.1 by Nick Chadwick
Working on putting worksheets into the database.
893
                                  self.worksheet.identifier)
1080.1.55 by Matt Giuca
ivle.database: Added ExerciseAttempt and ExerciseSave classes.
894
1131 by William Grant
Offerings now give 'view' only to user enrolled in them. 'edit' is granted
895
    def get_permissions(self, user):
896
        return self.worksheet.get_permissions(user)
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
897
1131 by William Grant
Offerings now give 'view' only to user enrolled in them. 'edit' is granted
898
1080.1.55 by Matt Giuca
ivle.database: Added ExerciseAttempt and ExerciseSave classes.
899
class ExerciseSave(Storm):
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
900
    """A potential exercise solution submitted by a user for storage.
901
902
    This is not an actual tested attempt at an exercise, it's just a save of
903
    the editing session.
904
    """
905
1099.1.195 by William Grant
Rename problem to exercise in the DB.
906
    __storm_table__ = "exercise_save"
1099.1.180 by Nick Chadwick
This commit changes the tutorial service, which now almost exclusively
907
    __storm_primary__ = "ws_ex_id", "user_id"
908
1099.1.195 by William Grant
Rename problem to exercise in the DB.
909
    ws_ex_id = Int(name="ws_ex_id")
1099.1.180 by Nick Chadwick
This commit changes the tutorial service, which now almost exclusively
910
    worksheet_exercise = Reference(ws_ex_id, "WorksheetExercise.id")
911
1080.1.55 by Matt Giuca
ivle.database: Added ExerciseAttempt and ExerciseSave classes.
912
    user_id = Int(name="loginid")
913
    user = Reference(user_id, User.id)
914
    date = DateTime()
915
    text = Unicode()
916
917
    __init__ = _kwarg_init
918
919
    def __repr__(self):
920
        return "<%s %s by %s at %s>" % (type(self).__name__,
921
            self.exercise.name, self.user.login, self.date.strftime("%c"))
922
923
class ExerciseAttempt(ExerciseSave):
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
924
    """An attempt at solving an exercise.
925
926
    This is a special case of ExerciseSave, used when the user submits a
927
    candidate solution. Like an ExerciseSave, it constitutes exercise solution
928
    data.
929
930
    In addition, it contains information about the result of the submission:
931
932
     - complete - True if this submission was successful, rendering this
933
                  exercise complete for this user in this worksheet.
934
     - active   - True if this submission is "active" (usually true).
935
                  Submissions may be de-activated by privileged users for
936
                  special reasons, and then they won't count (either as a
937
                  penalty or success), but will still be stored.
938
    """
939
1099.1.195 by William Grant
Rename problem to exercise in the DB.
940
    __storm_table__ = "exercise_attempt"
1099.1.180 by Nick Chadwick
This commit changes the tutorial service, which now almost exclusively
941
    __storm_primary__ = "ws_ex_id", "user_id", "date"
1080.1.55 by Matt Giuca
ivle.database: Added ExerciseAttempt and ExerciseSave classes.
942
943
    # The "text" field is the same but has a different name in the DB table
944
    # for some reason.
945
    text = Unicode(name="attempt")
946
    complete = Bool()
947
    active = Bool()
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
948
1099.1.113 by William Grant
Give console and tutorial services security declarations.
949
    def get_permissions(self, user):
950
        return set(['view']) if user is self.user else set()
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
951
1099.1.114 by Nick Chadwick
Modified the database so that exercises are now stored in the database, rather
952
class TestSuite(Storm):
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
953
    """A container to group an exercise's test cases.
954
955
    The test suite contains some information on how to test. The function to
956
    test, variables to set and stdin data are stored here.
957
    """
958
1099.1.114 by Nick Chadwick
Modified the database so that exercises are now stored in the database, rather
959
    __storm_table__ = "test_suite"
960
    __storm_primary__ = "exercise_id", "suiteid"
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
961
1099.1.114 by Nick Chadwick
Modified the database so that exercises are now stored in the database, rather
962
    suiteid = Int()
1099.1.195 by William Grant
Rename problem to exercise in the DB.
963
    exercise_id = Unicode(name="exerciseid")
1099.1.141 by Nick Chadwick
Updated the exercises to be loaded from the database, not a local file.
964
    description = Unicode()
965
    seq_no = Int()
966
    function = Unicode()
967
    stdin = Unicode()
1099.1.114 by Nick Chadwick
Modified the database so that exercises are now stored in the database, rather
968
    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
969
    test_cases = ReferenceSet(suiteid, 'TestCase.suiteid', order_by="seq_no")
970
    variables = ReferenceSet(suiteid, 'TestSuiteVar.suiteid', order_by='arg_no')
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
971
1099.1.242 by Nick Chadwick
Fixed a problem with exercise editor, which wasn't editing or adding
972
    def delete(self):
1099.1.233 by Nick Chadwick
Exercise objects in the database module, along with their test cases,
973
        """Delete this suite, without asking questions."""
1427 by William Grant
Fix deletion of test suites with variables.
974
        for variable in self.variables:
1099.1.233 by Nick Chadwick
Exercise objects in the database module, along with their test cases,
975
            variable.delete()
976
        for test_case in self.test_cases:
977
            test_case.delete()
1099.1.242 by Nick Chadwick
Fixed a problem with exercise editor, which wasn't editing or adding
978
        Store.of(self).remove(self)
1099.1.114 by Nick Chadwick
Modified the database so that exercises are now stored in the database, rather
979
980
class TestCase(Storm):
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
981
    """A container for actual tests (see TestCasePart), inside a test suite.
982
983
    It is the lowest level shown to students on their pass/fail status."""
984
1099.1.114 by Nick Chadwick
Modified the database so that exercises are now stored in the database, rather
985
    __storm_table__ = "test_case"
986
    __storm_primary__ = "testid", "suiteid"
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
987
1099.1.114 by Nick Chadwick
Modified the database so that exercises are now stored in the database, rather
988
    testid = Int()
989
    suiteid = Int()
1099.1.141 by Nick Chadwick
Updated the exercises to be loaded from the database, not a local file.
990
    suite = Reference(suiteid, "TestSuite.suiteid")
1099.1.114 by Nick Chadwick
Modified the database so that exercises are now stored in the database, rather
991
    passmsg = Unicode()
992
    failmsg = Unicode()
1394.1.5 by William Grant
Drop TestSuite file match default from the UI -- we don't support file tests any more.
993
    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
994
    seq_no = Int()
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
995
1099.1.141 by Nick Chadwick
Updated the exercises to be loaded from the database, not a local file.
996
    parts = ReferenceSet(testid, "TestCasePart.testid")
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
997
1099.1.141 by Nick Chadwick
Updated the exercises to be loaded from the database, not a local file.
998
    __init__ = _kwarg_init
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
999
1099.1.242 by Nick Chadwick
Fixed a problem with exercise editor, which wasn't editing or adding
1000
    def delete(self):
1099.1.233 by Nick Chadwick
Exercise objects in the database module, along with their test cases,
1001
        for part in self.parts:
1002
            part.delete()
1099.1.242 by Nick Chadwick
Fixed a problem with exercise editor, which wasn't editing or adding
1003
        Store.of(self).remove(self)
1099.1.141 by Nick Chadwick
Updated the exercises to be loaded from the database, not a local file.
1004
1005
class TestSuiteVar(Storm):
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
1006
    """A variable used by an exercise test suite.
1007
1008
    This may represent a function argument or a normal variable.
1009
    """
1010
1099.1.195 by William Grant
Rename problem to exercise in the DB.
1011
    __storm_table__ = "suite_variable"
1099.1.141 by Nick Chadwick
Updated the exercises to be loaded from the database, not a local file.
1012
    __storm_primary__ = "varid"
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
1013
1099.1.141 by Nick Chadwick
Updated the exercises to be loaded from the database, not a local file.
1014
    varid = Int()
1015
    suiteid = Int()
1016
    var_name = Unicode()
1017
    var_value = Unicode()
1018
    var_type = Unicode()
1019
    arg_no = Int()
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
1020
1099.1.141 by Nick Chadwick
Updated the exercises to be loaded from the database, not a local file.
1021
    suite = Reference(suiteid, "TestSuite.suiteid")
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
1022
1099.1.141 by Nick Chadwick
Updated the exercises to be loaded from the database, not a local file.
1023
    __init__ = _kwarg_init
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
1024
1099.1.242 by Nick Chadwick
Fixed a problem with exercise editor, which wasn't editing or adding
1025
    def delete(self):
1026
        Store.of(self).remove(self)
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
1027
1099.1.141 by Nick Chadwick
Updated the exercises to be loaded from the database, not a local file.
1028
class TestCasePart(Storm):
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
1029
    """An actual piece of code to test an exercise solution."""
1030
1099.1.195 by William Grant
Rename problem to exercise in the DB.
1031
    __storm_table__ = "test_case_part"
1099.1.141 by Nick Chadwick
Updated the exercises to be loaded from the database, not a local file.
1032
    __storm_primary__ = "partid"
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
1033
1099.1.141 by Nick Chadwick
Updated the exercises to be loaded from the database, not a local file.
1034
    partid = Int()
1035
    testid = Int()
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
1036
1099.1.141 by Nick Chadwick
Updated the exercises to be loaded from the database, not a local file.
1037
    part_type = Unicode()
1038
    test_type = Unicode()
1039
    data = Unicode()
1040
    filename = Unicode()
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
1041
1099.1.141 by Nick Chadwick
Updated the exercises to be loaded from the database, not a local file.
1042
    test = Reference(testid, "TestCase.testid")
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
1043
1099.1.114 by Nick Chadwick
Modified the database so that exercises are now stored in the database, rather
1044
    __init__ = _kwarg_init
1241 by William Grant
Vastly improve docstrings throughout ivle.database.
1045
1099.1.242 by Nick Chadwick
Fixed a problem with exercise editor, which wasn't editing or adding
1046
    def delete(self):
1047
        Store.of(self).remove(self)