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