~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
20
"""
21
Database Classes and Utilities for Storm ORM
22
23
This module provides all of the classes which map to database tables.
24
It also provides miscellaneous utility functions for database interaction.
25
"""
26
1080.1.13 by me at id
ivle.database.User: Add an authenticate() method, and a hash_password()
27
import md5
1080.1.15 by me at id
Give ivle.database.User {password,account}_expired attributes, and get
28
import datetime
1080.1.13 by me at id
ivle.database.User: Add an authenticate() method, and a hash_password()
29
1080.1.4 by matt.giuca
ivle.database: Added User class.
30
from storm.locals import create_database, Store, Int, Unicode, DateTime, \
1080.1.27 by me at id
ivle.database.User: Add an 'active_enrolments' property, which returns a list
31
                         Reference, ReferenceSet, Bool, Storm, Desc
1129 by William Grant
Move the group admin view to per-offering.
32
from storm.exceptions import NotOneError
1080.1.2 by matt.giuca
New module: ivle.database. Classes and utilities for Storm ORM.
33
34
import ivle.conf
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',
1080.1.59 by Matt Giuca
ivle.worksheet, ivle.database: Added/updated __all__.
40
            'Exercise', 'Worksheet', 'WorksheetExercise',
1080.1.61 by William Grant
ivle.database: Add an Offering.enrol(user) method, which enrols the user in
41
            'ExerciseSave', 'ExerciseAttempt',
1110 by William Grant
ivle-enrol now allows updating of existing enrolments. It also sets the role.
42
            'TestCase', 'TestSuite', 'TestSuiteVar'
1080.1.39 by Matt Giuca
ivle.database: Added __all__ to the top of the file.
43
        ]
44
1080.1.25 by me at id
ivle.database: Add Subject, Semester and Offering.
45
def _kwarg_init(self, **kwargs):
46
    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,
47
        if k.startswith('_') or not hasattr(self.__class__, k):
1080.1.25 by me at id
ivle.database: Add Subject, Semester and Offering.
48
            raise TypeError("%s got an unexpected keyword argument '%s'"
1080.1.45 by William Grant
ivle.database._kwarg_init: Fix exception throwing.
49
                % (self.__class__.__name__, k))
1080.1.25 by me at id
ivle.database: Add Subject, Semester and Offering.
50
        setattr(self, k, v)
51
1080.1.2 by matt.giuca
New module: ivle.database. Classes and utilities for Storm ORM.
52
def get_conn_string():
53
    """
54
    Returns the Storm connection string, generated from the conf file.
55
    """
1099.1.174 by William Grant
ivle.database.get_conn_string() now defaults to localhost:5432, rather than
56
57
    clusterstr = ''
58
    if ivle.conf.db_user:
59
        clusterstr += ivle.conf.db_user
60
        if ivle.conf.db_password:
61
            clusterstr += ':' + ivle.conf.db_password
62
        clusterstr += '@'
63
64
    host = ivle.conf.db_host or 'localhost'
65
    port = ivle.conf.db_port or 5432
66
67
    clusterstr += '%s:%d' % (host, port)
68
69
    return "postgres://%s/%s" % (clusterstr, ivle.conf.db_dbname)
1080.1.2 by matt.giuca
New module: ivle.database. Classes and utilities for Storm ORM.
70
71
def get_store():
72
    """
73
    Open a database connection and transaction. Return a storm.store.Store
74
    instance connected to the configured IVLE database.
75
    """
76
    return Store(create_database(get_conn_string()))
1080.1.4 by matt.giuca
ivle.database: Added User class.
77
1080.1.39 by Matt Giuca
ivle.database: Added __all__ to the top of the file.
78
# USERS #
79
1080.1.26 by me at id
ivle.database: Add an Enrolment class, and reference(set)s between all of the
80
class User(Storm):
1080.1.4 by matt.giuca
ivle.database: Added User class.
81
    """
82
    Represents an IVLE user.
83
    """
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
119
    def password_expired(self):
1080.1.5 by matt.giuca
ivle.database.User: Add the missing methods from ivle.user.User.
120
        fieldval = self.pass_exp
1080.1.15 by me at id
Give ivle.database.User {password,account}_expired attributes, and get
121
        return fieldval is not None and datetime.datetime.now() > fieldval
122
123
    @property
124
    def account_expired(self):
1080.1.5 by matt.giuca
ivle.database.User: Add the missing methods from ivle.user.User.
125
        fieldval = self.acct_exp
1080.1.15 by me at id
Give ivle.database.User {password,account}_expired attributes, and get
126
        return fieldval is not None and datetime.datetime.now() > fieldval
1080.1.6 by matt.giuca
ivle.database.User: Added get_by_login method.
127
1099.1.121 by William Grant
Don't set req.user unless the login in the session specifies a valid user.
128
    @property
129
    def valid(self):
130
        return self.state == 'enabled' and not self.account_expired
131
1080.1.29 by me at id
ivle.database.User: Order 'enrolments' the same way as 'active_enrolments'.
132
    def _get_enrolments(self, justactive):
1080.1.27 by me at id
ivle.database.User: Add an 'active_enrolments' property, which returns a list
133
        return Store.of(self).find(Enrolment,
134
            Enrolment.user_id == self.id,
1080.1.29 by me at id
ivle.database.User: Order 'enrolments' the same way as 'active_enrolments'.
135
            (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
136
            Enrolment.offering_id == Offering.id,
137
            Offering.semester_id == Semester.id,
138
            Offering.subject_id == Subject.id).order_by(
139
                Desc(Semester.year),
140
                Desc(Semester.semester),
141
                Desc(Subject.code)
142
            )
143
1080.1.68 by William Grant
ivle.database.User: Add a write-only 'password' attribute. When set, it will
144
    def _set_password(self, password):
145
        if password is None:
146
            self.passhash = None
147
        else:
148
            self.passhash = unicode(User.hash_password(password))
149
    password = property(fset=_set_password)
150
1080.1.29 by me at id
ivle.database.User: Order 'enrolments' the same way as 'active_enrolments'.
151
    @property
1080.1.31 by me at id
ivle.database.User: Add 'subjects', an attribute containing currently
152
    def subjects(self):
153
        return Store.of(self).find(Subject,
154
            Enrolment.user_id == self.id,
155
            Enrolment.active == True,
156
            Offering.id == Enrolment.offering_id,
157
            Subject.id == Offering.subject_id).config(distinct=True)
158
1080.1.36 by William Grant
ivle.database: Add ProjectSet, Project, ProjectGroup, ProjectGroupMembership
159
    # TODO: Invitations should be listed too?
160
    def get_groups(self, offering=None):
161
        preds = [
162
            ProjectGroupMembership.user_id == self.id,
163
            ProjectGroup.id == ProjectGroupMembership.project_group_id,
164
        ]
165
        if offering:
166
            preds.extend([
167
                ProjectSet.offering_id == offering.id,
168
                ProjectGroup.project_set_id == ProjectSet.id,
169
            ])
170
        return Store.of(self).find(ProjectGroup, *preds)
171
172
    @property
173
    def groups(self):
174
        return self.get_groups()
175
1080.1.31 by me at id
ivle.database.User: Add 'subjects', an attribute containing currently
176
    @property
1080.1.29 by me at id
ivle.database.User: Order 'enrolments' the same way as 'active_enrolments'.
177
    def active_enrolments(self):
178
        '''A sanely ordered list of the user's active enrolments.'''
179
        return self._get_enrolments(True)
180
181
    @property
182
    def enrolments(self):
183
        '''A sanely ordered list of all of the user's enrolments.'''
184
        return self._get_enrolments(False) 
1080.1.27 by me at id
ivle.database.User: Add an 'active_enrolments' property, which returns a list
185
1080.1.13 by me at id
ivle.database.User: Add an authenticate() method, and a hash_password()
186
    @staticmethod
187
    def hash_password(password):
188
        return md5.md5(password).hexdigest()
189
1080.1.6 by matt.giuca
ivle.database.User: Added get_by_login method.
190
    @classmethod
191
    def get_by_login(cls, store, login):
192
        """
193
        Get the User from the db associated with a given store and
194
        login.
195
        """
1080.1.7 by matt.giuca
The new ivle.database.User class is now used in Request and usrmgt, which
196
        return store.find(cls, cls.login == unicode(login)).one()
1080.1.25 by me at id
ivle.database: Add Subject, Semester and Offering.
197
1099.1.110 by William Grant
Implement an authorization system in the new framework. This breaks the REST
198
    def get_permissions(self, user):
1101 by William Grant
Privileges (apart from admin) are now offering-local, not global.
199
        if user and user.admin or user is self:
1099.1.110 by William Grant
Implement an authorization system in the new framework. This breaks the REST
200
            return set(['view', 'edit'])
201
        else:
202
            return set()
203
1080.1.39 by Matt Giuca
ivle.database: Added __all__ to the top of the file.
204
# SUBJECTS AND ENROLMENTS #
205
1080.1.26 by me at id
ivle.database: Add an Enrolment class, and reference(set)s between all of the
206
class Subject(Storm):
1080.1.25 by me at id
ivle.database: Add Subject, Semester and Offering.
207
    __storm_table__ = "subject"
208
209
    id = Int(primary=True, name="subjectid")
210
    code = Unicode(name="subj_code")
211
    name = Unicode(name="subj_name")
212
    short_name = Unicode(name="subj_short_name")
213
    url = Unicode()
214
1080.1.26 by me at id
ivle.database: Add an Enrolment class, and reference(set)s between all of the
215
    offerings = ReferenceSet(id, 'Offering.subject_id')
216
1080.1.25 by me at id
ivle.database: Add Subject, Semester and Offering.
217
    __init__ = _kwarg_init
218
219
    def __repr__(self):
220
        return "<%s '%s'>" % (type(self).__name__, self.short_name)
221
1099.1.110 by William Grant
Implement an authorization system in the new framework. This breaks the REST
222
    def get_permissions(self, user):
223
        perms = set()
224
        if user is not None:
225
            perms.add('view')
1101 by William Grant
Privileges (apart from admin) are now offering-local, not global.
226
            if user.admin:
1099.1.141 by Nick Chadwick
Updated the exercises to be loaded from the database, not a local file.
227
                perms.add('edit')
1099.1.110 by William Grant
Implement an authorization system in the new framework. This breaks the REST
228
        return perms
229
1080.1.26 by me at id
ivle.database: Add an Enrolment class, and reference(set)s between all of the
230
class Semester(Storm):
1080.1.25 by me at id
ivle.database: Add Subject, Semester and Offering.
231
    __storm_table__ = "semester"
232
233
    id = Int(primary=True, name="semesterid")
234
    year = Unicode()
235
    semester = Unicode()
1104 by William Grant
Replace Semester.active with Semester.state, allowing more useful state
236
    state = Unicode()
1080.1.25 by me at id
ivle.database: Add Subject, Semester and Offering.
237
1080.1.26 by me at id
ivle.database: Add an Enrolment class, and reference(set)s between all of the
238
    offerings = ReferenceSet(id, 'Offering.semester_id')
1124 by William Grant
Add Semester.enrolments.
239
    enrolments = ReferenceSet(id,
240
                              'Offering.semester_id',
241
                              'Offering.id',
242
                              'Enrolment.offering_id')
1080.1.26 by me at id
ivle.database: Add an Enrolment class, and reference(set)s between all of the
243
1080.1.25 by me at id
ivle.database: Add Subject, Semester and Offering.
244
    __init__ = _kwarg_init
245
246
    def __repr__(self):
247
        return "<%s %s/%s>" % (type(self).__name__, self.year, self.semester)
248
1080.1.26 by me at id
ivle.database: Add an Enrolment class, and reference(set)s between all of the
249
class Offering(Storm):
1080.1.25 by me at id
ivle.database: Add Subject, Semester and Offering.
250
    __storm_table__ = "offering"
251
252
    id = Int(primary=True, name="offeringid")
253
    subject_id = Int(name="subject")
254
    subject = Reference(subject_id, Subject.id)
255
    semester_id = Int(name="semesterid")
256
    semester = Reference(semester_id, Semester.id)
257
    groups_student_permissions = Unicode()
258
1080.1.26 by me at id
ivle.database: Add an Enrolment class, and reference(set)s between all of the
259
    enrolments = ReferenceSet(id, 'Enrolment.offering_id')
1080.1.79 by William Grant
ivle.database.Offering: Add a members ReferenceSet.
260
    members = ReferenceSet(id,
261
                           'Enrolment.offering_id',
262
                           'Enrolment.user_id',
263
                           'User.id')
1080.1.76 by William Grant
ivle.database.Offering: Add project_sets referenceset.
264
    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
265
1099.1.180 by Nick Chadwick
This commit changes the tutorial service, which now almost exclusively
266
    worksheets = ReferenceSet(id, 
267
        'Worksheet.offering_id', 
268
        order_by="Worksheet.seq_no"
269
    )
1099.1.114 by Nick Chadwick
Modified the database so that exercises are now stored in the database, rather
270
1080.1.25 by me at id
ivle.database: Add Subject, Semester and Offering.
271
    __init__ = _kwarg_init
272
273
    def __repr__(self):
274
        return "<%s %r in %r>" % (type(self).__name__, self.subject,
275
                                  self.semester)
1080.1.26 by me at id
ivle.database: Add an Enrolment class, and reference(set)s between all of the
276
1110 by William Grant
ivle-enrol now allows updating of existing enrolments. It also sets the role.
277
    def enrol(self, user, role=u'student'):
1080.1.61 by William Grant
ivle.database: Add an Offering.enrol(user) method, which enrols the user in
278
        '''Enrol a user in this offering.'''
1110 by William Grant
ivle-enrol now allows updating of existing enrolments. It also sets the role.
279
        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
280
                               Enrolment.user_id == user.id,
1110 by William Grant
ivle-enrol now allows updating of existing enrolments. It also sets the role.
281
                               Enrolment.offering_id == self.id).one()
282
283
        if enrolment is None:
284
            enrolment = Enrolment(user=user, offering=self)
285
            self.enrolments.add(enrolment)
286
287
        enrolment.active = True
288
        enrolment.role = role
1080.1.61 by William Grant
ivle.database: Add an Offering.enrol(user) method, which enrols the user in
289
1132 by William Grant
Add Offering.unenrol(), to unenrol a user from an offering.
290
    def unenrol(self, user):
291
        '''Unenrol a user from this offering.'''
292
        enrolment = Store.of(self).find(Enrolment,
293
                               Enrolment.user_id == user.id,
294
                               Enrolment.offering_id == self.id).one()
295
        Store.of(enrolment).remove(enrolment)
296
1099.1.141 by Nick Chadwick
Updated the exercises to be loaded from the database, not a local file.
297
    def get_permissions(self, user):
298
        perms = set()
299
        if user is not None:
1131 by William Grant
Offerings now give 'view' only to user enrolled in them. 'edit' is granted
300
            enrolment = self.get_enrolment(user)
301
            if enrolment or user.admin:
302
                perms.add('view')
303
            if (enrolment and enrolment.role in (u'tutor', u'lecturer')) \
304
               or user.admin:
1099.1.141 by Nick Chadwick
Updated the exercises to be loaded from the database, not a local file.
305
                perms.add('edit')
306
        return perms
307
1129 by William Grant
Move the group admin view to per-offering.
308
    def get_enrolment(self, user):
309
        try:
310
            enrolment = self.enrolments.find(user=user).one()
311
        except NotOneError:
312
            enrolment = None
313
314
        return enrolment
315
1080.1.26 by me at id
ivle.database: Add an Enrolment class, and reference(set)s between all of the
316
class Enrolment(Storm):
317
    __storm_table__ = "enrolment"
318
    __storm_primary__ = "user_id", "offering_id"
319
320
    user_id = Int(name="loginid")
321
    user = Reference(user_id, User.id)
322
    offering_id = Int(name="offeringid")
323
    offering = Reference(offering_id, Offering.id)
1101 by William Grant
Privileges (apart from admin) are now offering-local, not global.
324
    role = Unicode()
1080.1.26 by me at id
ivle.database: Add an Enrolment class, and reference(set)s between all of the
325
    notes = Unicode()
326
    active = Bool()
327
1080.1.81 by William Grant
ivle.database.Enrolment: Add a groups attribute, containing groups of which
328
    @property
329
    def groups(self):
330
        return Store.of(self).find(ProjectGroup,
331
                ProjectSet.offering_id == self.offering.id,
332
                ProjectGroup.project_set_id == ProjectSet.id,
333
                ProjectGroupMembership.project_group_id == ProjectGroup.id,
334
                ProjectGroupMembership.user_id == self.user.id)
335
1080.1.26 by me at id
ivle.database: Add an Enrolment class, and reference(set)s between all of the
336
    __init__ = _kwarg_init
337
338
    def __repr__(self):
339
        return "<%s %r in %r>" % (type(self).__name__, self.user,
340
                                  self.offering)
1080.1.36 by William Grant
ivle.database: Add ProjectSet, Project, ProjectGroup, ProjectGroupMembership
341
1080.1.39 by Matt Giuca
ivle.database: Added __all__ to the top of the file.
342
# PROJECTS #
343
1080.1.36 by William Grant
ivle.database: Add ProjectSet, Project, ProjectGroup, ProjectGroupMembership
344
class ProjectSet(Storm):
345
    __storm_table__ = "project_set"
346
347
    id = Int(name="projectsetid", primary=True)
348
    offering_id = Int(name="offeringid")
349
    offering = Reference(offering_id, Offering.id)
350
    max_students_per_group = Int()
351
1080.1.77 by William Grant
ivle.database.ProjectSet: Add projects and project_groups referencesets.
352
    projects = ReferenceSet(id, 'Project.project_set_id')
353
    project_groups = ReferenceSet(id, 'ProjectGroup.project_set_id')
354
1080.1.36 by William Grant
ivle.database: Add ProjectSet, Project, ProjectGroup, ProjectGroupMembership
355
    __init__ = _kwarg_init
356
357
    def __repr__(self):
358
        return "<%s %d in %r>" % (type(self).__name__, self.id,
359
                                  self.offering)
360
361
class Project(Storm):
362
    __storm_table__ = "project"
363
364
    id = Int(name="projectid", primary=True)
365
    synopsis = Unicode()
366
    url = Unicode()
367
    project_set_id = Int(name="projectsetid")
368
    project_set = Reference(project_set_id, ProjectSet.id)
369
    deadline = DateTime()
370
371
    __init__ = _kwarg_init
372
373
    def __repr__(self):
374
        return "<%s '%s' in %r>" % (type(self).__name__, self.synopsis,
375
                                  self.project_set.offering)
376
377
class ProjectGroup(Storm):
378
    __storm_table__ = "project_group"
379
380
    id = Int(name="groupid", primary=True)
381
    name = Unicode(name="groupnm")
382
    project_set_id = Int(name="projectsetid")
383
    project_set = Reference(project_set_id, ProjectSet.id)
384
    nick = Unicode()
385
    created_by_id = Int(name="createdby")
386
    created_by = Reference(created_by_id, User.id)
387
    epoch = DateTime()
388
1080.1.78 by William Grant
ivle.database.ProjectGroup.members: Use a ReferenceSet.
389
    members = ReferenceSet(id,
390
                           "ProjectGroupMembership.project_group_id",
391
                           "ProjectGroupMembership.user_id",
392
                           "User.id")
393
1080.1.36 by William Grant
ivle.database: Add ProjectSet, Project, ProjectGroup, ProjectGroupMembership
394
    __init__ = _kwarg_init
395
396
    def __repr__(self):
397
        return "<%s %s in %r>" % (type(self).__name__, self.name,
398
                                  self.project_set.offering)
399
400
class ProjectGroupMembership(Storm):
401
    __storm_table__ = "group_member"
402
    __storm_primary__ = "user_id", "project_group_id"
403
404
    user_id = Int(name="loginid")
405
    user = Reference(user_id, User.id)
406
    project_group_id = Int(name="groupid")
407
    project_group = Reference(project_group_id, ProjectGroup.id)
408
409
    __init__ = _kwarg_init
410
411
    def __repr__(self):
412
        return "<%s %r in %r>" % (type(self).__name__, self.user,
413
                                  self.project_group)
414
1080.1.40 by Matt Giuca
ivle.database: Added Worksheet and Exercise classes (more to come in this
415
# WORKSHEETS AND EXERCISES #
416
417
class Exercise(Storm):
1099.1.195 by William Grant
Rename problem to exercise in the DB.
418
    __storm_table__ = "exercise"
1099.1.114 by Nick Chadwick
Modified the database so that exercises are now stored in the database, rather
419
    id = Unicode(primary=True, name="identifier")
420
    name = Unicode()
421
    description = Unicode()
422
    partial = Unicode()
423
    solution = Unicode()
424
    include = Unicode()
425
    num_rows = Int()
1080.1.40 by Matt Giuca
ivle.database: Added Worksheet and Exercise classes (more to come in this
426
1080.1.50 by Matt Giuca
ivle.database: Added WorksheetExercise (relates worksheets to exercises), and
427
    worksheets = ReferenceSet(id,
428
        'WorksheetExercise.exercise_id',
429
        'WorksheetExercise.worksheet_id',
430
        'Worksheet.id'
431
    )
1099.1.114 by Nick Chadwick
Modified the database so that exercises are now stored in the database, rather
432
    
433
    test_suites = ReferenceSet(id, 'TestSuite.exercise_id')
1080.1.50 by Matt Giuca
ivle.database: Added WorksheetExercise (relates worksheets to exercises), and
434
1080.1.40 by Matt Giuca
ivle.database: Added Worksheet and Exercise classes (more to come in this
435
    __init__ = _kwarg_init
436
437
    def __repr__(self):
438
        return "<%s %s>" % (type(self).__name__, self.name)
439
1099.1.210 by Nick Chadwick
Modified the database layer so that exercises have a get_permissions
440
    def get_permissions(self, user):
441
        perms = set()
442
        if user is not None:
1101 by William Grant
Privileges (apart from admin) are now offering-local, not global.
443
            if user.admin:
1099.1.210 by Nick Chadwick
Modified the database layer so that exercises have a get_permissions
444
                perms.add('edit')
445
                perms.add('view')
446
        return perms
1080.1.51 by Matt Giuca
tutorial: Replaced call to ivle.db.create_worksheet with local code (roughly
447
1080.1.40 by Matt Giuca
ivle.database: Added Worksheet and Exercise classes (more to come in this
448
class Worksheet(Storm):
449
    __storm_table__ = "worksheet"
450
451
    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
452
    offering_id = Int(name="offeringid")
1099.4.1 by Nick Chadwick
Working on putting worksheets into the database.
453
    identifier = Unicode()
454
    name = Unicode()
1080.1.40 by Matt Giuca
ivle.database: Added Worksheet and Exercise classes (more to come in this
455
    assessable = Bool()
1099.4.1 by Nick Chadwick
Working on putting worksheets into the database.
456
    data = Unicode()
1099.1.180 by Nick Chadwick
This commit changes the tutorial service, which now almost exclusively
457
    seq_no = Int()
458
    format = Unicode()
1080.1.40 by Matt Giuca
ivle.database: Added Worksheet and Exercise classes (more to come in this
459
1099.1.141 by Nick Chadwick
Updated the exercises to be loaded from the database, not a local file.
460
    attempts = ReferenceSet(id, "ExerciseAttempt.worksheetid")
1099.1.118 by William Grant
Fix a bad reference introduced with the worksheet changes.
461
    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
462
1103 by William Grant
Worksheet.worksheet_exercises now only contains active ones.
463
    all_worksheet_exercises = ReferenceSet(id,
464
        'WorksheetExercise.worksheet_id')
465
466
    # Use worksheet_exercises to get access to the *active* WorksheetExercise
467
    # 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
468
    # "optional" field.
1103 by William Grant
Worksheet.worksheet_exercises now only contains active ones.
469
    @property
470
    def worksheet_exercises(self):
471
        return self.all_worksheet_exercises.find(active=True)
1080.1.50 by Matt Giuca
ivle.database: Added WorksheetExercise (relates worksheets to exercises), and
472
1080.1.40 by Matt Giuca
ivle.database: Added Worksheet and Exercise classes (more to come in this
473
    __init__ = _kwarg_init
474
475
    def __repr__(self):
476
        return "<%s %s>" % (type(self).__name__, self.name)
1080.1.47 by Matt Giuca
ivle.database: Added Worksheet.get_by_name method.
477
478
    # XXX Refactor this - make it an instance method of Subject rather than a
479
    # class method of Worksheet. Can't do that now because Subject isn't
480
    # linked referentially to the Worksheet.
481
    @classmethod
482
    def get_by_name(cls, store, subjectname, worksheetname):
483
        """
484
        Get the Worksheet from the db associated with a given store, subject
485
        name and worksheet name.
486
        """
487
        return store.find(cls, cls.subject == unicode(subjectname),
488
            cls.name == unicode(worksheetname)).one()
1080.1.50 by Matt Giuca
ivle.database: Added WorksheetExercise (relates worksheets to exercises), and
489
1080.1.51 by Matt Giuca
tutorial: Replaced call to ivle.db.create_worksheet with local code (roughly
490
    def remove_all_exercises(self, store):
491
        """
492
        Remove all exercises from this worksheet.
493
        This does not delete the exercises themselves. It just removes them
494
        from the worksheet.
495
        """
496
        store.find(WorksheetExercise,
497
            WorksheetExercise.worksheet == self).remove()
1099.1.141 by Nick Chadwick
Updated the exercises to be loaded from the database, not a local file.
498
            
499
    def get_permissions(self, user):
500
        return self.offering.get_permissions(user)
1080.1.51 by Matt Giuca
tutorial: Replaced call to ivle.db.create_worksheet with local code (roughly
501
1080.1.50 by Matt Giuca
ivle.database: Added WorksheetExercise (relates worksheets to exercises), and
502
class WorksheetExercise(Storm):
1099.1.195 by William Grant
Rename problem to exercise in the DB.
503
    __storm_table__ = "worksheet_exercise"
1099.4.4 by Nick Chadwick
Made what should (hopefully) be the last changes to the database schema.
504
    
1099.1.195 by William Grant
Rename problem to exercise in the DB.
505
    id = Int(primary=True, name="ws_ex_id")
1080.1.50 by Matt Giuca
ivle.database: Added WorksheetExercise (relates worksheets to exercises), and
506
507
    worksheet_id = Int(name="worksheetid")
508
    worksheet = Reference(worksheet_id, Worksheet.id)
1099.1.195 by William Grant
Rename problem to exercise in the DB.
509
    exercise_id = Unicode(name="exerciseid")
1080.1.50 by Matt Giuca
ivle.database: Added WorksheetExercise (relates worksheets to exercises), and
510
    exercise = Reference(exercise_id, Exercise.id)
511
    optional = Bool()
1099.4.3 by Nick Chadwick
Updated the tutorial service, to now allow users to edit worksheets
512
    active = Bool()
513
    seq_no = Int()
1099.1.180 by Nick Chadwick
This commit changes the tutorial service, which now almost exclusively
514
    
515
    saves = ReferenceSet(id, "ExerciseSave.ws_ex_id")
1099.1.183 by William Grant
Fix a reference typo in ivle.database.
516
    attempts = ReferenceSet(id, "ExerciseAttempt.ws_ex_id")
1080.1.50 by Matt Giuca
ivle.database: Added WorksheetExercise (relates worksheets to exercises), and
517
518
    __init__ = _kwarg_init
519
520
    def __repr__(self):
521
        return "<%s %s in %s>" % (type(self).__name__, self.exercise.name,
1099.4.1 by Nick Chadwick
Working on putting worksheets into the database.
522
                                  self.worksheet.identifier)
1080.1.55 by Matt Giuca
ivle.database: Added ExerciseAttempt and ExerciseSave classes.
523
1131 by William Grant
Offerings now give 'view' only to user enrolled in them. 'edit' is granted
524
    def get_permissions(self, user):
525
        return self.worksheet.get_permissions(user)
526
1080.1.55 by Matt Giuca
ivle.database: Added ExerciseAttempt and ExerciseSave classes.
527
class ExerciseSave(Storm):
528
    """
529
    Represents a potential solution to an exercise that a user has submitted
530
    to the server for storage.
531
    A basic ExerciseSave is just the current saved text for this exercise for
532
    this user (doesn't count towards their attempts).
533
    ExerciseSave may be extended with additional semantics (such as
534
    ExerciseAttempt).
535
    """
1099.1.195 by William Grant
Rename problem to exercise in the DB.
536
    __storm_table__ = "exercise_save"
1099.1.180 by Nick Chadwick
This commit changes the tutorial service, which now almost exclusively
537
    __storm_primary__ = "ws_ex_id", "user_id"
538
1099.1.195 by William Grant
Rename problem to exercise in the DB.
539
    ws_ex_id = Int(name="ws_ex_id")
1099.1.180 by Nick Chadwick
This commit changes the tutorial service, which now almost exclusively
540
    worksheet_exercise = Reference(ws_ex_id, "WorksheetExercise.id")
541
1080.1.55 by Matt Giuca
ivle.database: Added ExerciseAttempt and ExerciseSave classes.
542
    user_id = Int(name="loginid")
543
    user = Reference(user_id, User.id)
544
    date = DateTime()
545
    text = Unicode()
546
547
    __init__ = _kwarg_init
548
549
    def __repr__(self):
550
        return "<%s %s by %s at %s>" % (type(self).__name__,
551
            self.exercise.name, self.user.login, self.date.strftime("%c"))
552
553
class ExerciseAttempt(ExerciseSave):
554
    """
555
    An ExerciseAttempt is a special case of an ExerciseSave. Like an
556
    ExerciseSave, it constitutes exercise solution data that the user has
557
    submitted to the server for storage.
558
    In addition, it contains additional information about the submission.
559
    complete - True if this submission was successful, rendering this exercise
560
        complete for this user.
561
    active - True if this submission is "active" (usually true). Submissions
562
        may be de-activated by privileged users for special reasons, and then
563
        they won't count (either as a penalty or success), but will still be
564
        stored.
565
    """
1099.1.195 by William Grant
Rename problem to exercise in the DB.
566
    __storm_table__ = "exercise_attempt"
1099.1.180 by Nick Chadwick
This commit changes the tutorial service, which now almost exclusively
567
    __storm_primary__ = "ws_ex_id", "user_id", "date"
1080.1.55 by Matt Giuca
ivle.database: Added ExerciseAttempt and ExerciseSave classes.
568
569
    # The "text" field is the same but has a different name in the DB table
570
    # for some reason.
571
    text = Unicode(name="attempt")
572
    complete = Bool()
573
    active = Bool()
1099.1.114 by Nick Chadwick
Modified the database so that exercises are now stored in the database, rather
574
    
1099.1.113 by William Grant
Give console and tutorial services security declarations.
575
    def get_permissions(self, user):
576
        return set(['view']) if user is self.user else set()
1099.1.114 by Nick Chadwick
Modified the database so that exercises are now stored in the database, rather
577
  
578
class TestSuite(Storm):
579
    """A Testsuite acts as a container for the test cases of an exercise."""
580
    __storm_table__ = "test_suite"
581
    __storm_primary__ = "exercise_id", "suiteid"
582
    
583
    suiteid = Int()
1099.1.195 by William Grant
Rename problem to exercise in the DB.
584
    exercise_id = Unicode(name="exerciseid")
1099.1.141 by Nick Chadwick
Updated the exercises to be loaded from the database, not a local file.
585
    description = Unicode()
586
    seq_no = Int()
587
    function = Unicode()
588
    stdin = Unicode()
1099.1.114 by Nick Chadwick
Modified the database so that exercises are now stored in the database, rather
589
    exercise = Reference(exercise_id, Exercise.id)
590
    test_cases = ReferenceSet(suiteid, 'TestCase.suiteid')
1099.1.141 by Nick Chadwick
Updated the exercises to be loaded from the database, not a local file.
591
    variables = ReferenceSet(suiteid, 'TestSuiteVar.suiteid')
1099.1.114 by Nick Chadwick
Modified the database so that exercises are now stored in the database, rather
592
593
class TestCase(Storm):
594
    """A TestCase is a member of a TestSuite.
595
    
596
    It contains the data necessary to check if an exercise is correct"""
597
    __storm_table__ = "test_case"
598
    __storm_primary__ = "testid", "suiteid"
599
    
600
    testid = Int()
601
    suiteid = Int()
1099.1.141 by Nick Chadwick
Updated the exercises to be loaded from the database, not a local file.
602
    suite = Reference(suiteid, "TestSuite.suiteid")
1099.1.114 by Nick Chadwick
Modified the database so that exercises are now stored in the database, rather
603
    passmsg = Unicode()
604
    failmsg = Unicode()
1099.1.141 by Nick Chadwick
Updated the exercises to be loaded from the database, not a local file.
605
    test_default = Unicode()
1099.1.114 by Nick Chadwick
Modified the database so that exercises are now stored in the database, rather
606
    seq_no = Int()
607
    
1099.1.141 by Nick Chadwick
Updated the exercises to be loaded from the database, not a local file.
608
    parts = ReferenceSet(testid, "TestCasePart.testid")
609
    
610
    __init__ = _kwarg_init
611
612
class TestSuiteVar(Storm):
613
    """A container for the arguments of a Test Suite"""
1099.1.195 by William Grant
Rename problem to exercise in the DB.
614
    __storm_table__ = "suite_variable"
1099.1.141 by Nick Chadwick
Updated the exercises to be loaded from the database, not a local file.
615
    __storm_primary__ = "varid"
616
    
617
    varid = Int()
618
    suiteid = Int()
619
    var_name = Unicode()
620
    var_value = Unicode()
621
    var_type = Unicode()
622
    arg_no = Int()
623
    
624
    suite = Reference(suiteid, "TestSuite.suiteid")
625
    
626
    __init__ = _kwarg_init
627
    
628
class TestCasePart(Storm):
629
    """A container for the test elements of a Test Case"""
1099.1.195 by William Grant
Rename problem to exercise in the DB.
630
    __storm_table__ = "test_case_part"
1099.1.141 by Nick Chadwick
Updated the exercises to be loaded from the database, not a local file.
631
    __storm_primary__ = "partid"
632
    
633
    partid = Int()
634
    testid = Int()
635
    
636
    part_type = Unicode()
637
    test_type = Unicode()
638
    data = Unicode()
639
    filename = Unicode()
640
    
641
    test = Reference(testid, "TestCase.testid")
642
    
1099.1.114 by Nick Chadwick
Modified the database so that exercises are now stored in the database, rather
643
    __init__ = _kwarg_init