~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
1080.1.2 by matt.giuca
New module: ivle.database. Classes and utilities for Storm ORM.
32
33
import ivle.conf
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',
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')
239
1080.1.25 by me at id
ivle.database: Add Subject, Semester and Offering.
240
    __init__ = _kwarg_init
241
242
    def __repr__(self):
243
        return "<%s %s/%s>" % (type(self).__name__, self.year, self.semester)
244
1080.1.26 by me at id
ivle.database: Add an Enrolment class, and reference(set)s between all of the
245
class Offering(Storm):
1080.1.25 by me at id
ivle.database: Add Subject, Semester and Offering.
246
    __storm_table__ = "offering"
247
248
    id = Int(primary=True, name="offeringid")
249
    subject_id = Int(name="subject")
250
    subject = Reference(subject_id, Subject.id)
251
    semester_id = Int(name="semesterid")
252
    semester = Reference(semester_id, Semester.id)
253
    groups_student_permissions = Unicode()
254
1080.1.26 by me at id
ivle.database: Add an Enrolment class, and reference(set)s between all of the
255
    enrolments = ReferenceSet(id, 'Enrolment.offering_id')
1080.1.79 by William Grant
ivle.database.Offering: Add a members ReferenceSet.
256
    members = ReferenceSet(id,
257
                           'Enrolment.offering_id',
258
                           'Enrolment.user_id',
259
                           'User.id')
1080.1.76 by William Grant
ivle.database.Offering: Add project_sets referenceset.
260
    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
261
1099.1.180 by Nick Chadwick
This commit changes the tutorial service, which now almost exclusively
262
    worksheets = ReferenceSet(id, 
263
        'Worksheet.offering_id', 
1099.1.212 by Nick Chadwick
Added a new page to display exercises. This will then be modified to
264
        order_by="seq_no"
1099.1.180 by Nick Chadwick
This commit changes the tutorial service, which now almost exclusively
265
    )
1099.1.114 by Nick Chadwick
Modified the database so that exercises are now stored in the database, rather
266
1080.1.25 by me at id
ivle.database: Add Subject, Semester and Offering.
267
    __init__ = _kwarg_init
268
269
    def __repr__(self):
270
        return "<%s %r in %r>" % (type(self).__name__, self.subject,
271
                                  self.semester)
1080.1.26 by me at id
ivle.database: Add an Enrolment class, and reference(set)s between all of the
272
1110 by William Grant
ivle-enrol now allows updating of existing enrolments. It also sets the role.
273
    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
274
        '''Enrol a user in this offering.'''
1110 by William Grant
ivle-enrol now allows updating of existing enrolments. It also sets the role.
275
        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
276
                               Enrolment.user_id == user.id,
1110 by William Grant
ivle-enrol now allows updating of existing enrolments. It also sets the role.
277
                               Enrolment.offering_id == self.id).one()
278
279
        if enrolment is None:
280
            enrolment = Enrolment(user=user, offering=self)
281
            self.enrolments.add(enrolment)
282
283
        enrolment.active = True
284
        enrolment.role = role
1080.1.61 by William Grant
ivle.database: Add an Offering.enrol(user) method, which enrols the user in
285
1099.1.141 by Nick Chadwick
Updated the exercises to be loaded from the database, not a local file.
286
    def get_permissions(self, user):
287
        perms = set()
288
        if user is not None:
289
            perms.add('view')
1101 by William Grant
Privileges (apart from admin) are now offering-local, not global.
290
            if user.admin:
1099.1.141 by Nick Chadwick
Updated the exercises to be loaded from the database, not a local file.
291
                perms.add('edit')
292
        return perms
293
1080.1.26 by me at id
ivle.database: Add an Enrolment class, and reference(set)s between all of the
294
class Enrolment(Storm):
295
    __storm_table__ = "enrolment"
296
    __storm_primary__ = "user_id", "offering_id"
297
298
    user_id = Int(name="loginid")
299
    user = Reference(user_id, User.id)
300
    offering_id = Int(name="offeringid")
301
    offering = Reference(offering_id, Offering.id)
1101 by William Grant
Privileges (apart from admin) are now offering-local, not global.
302
    role = Unicode()
1080.1.26 by me at id
ivle.database: Add an Enrolment class, and reference(set)s between all of the
303
    notes = Unicode()
304
    active = Bool()
305
1080.1.81 by William Grant
ivle.database.Enrolment: Add a groups attribute, containing groups of which
306
    @property
307
    def groups(self):
308
        return Store.of(self).find(ProjectGroup,
309
                ProjectSet.offering_id == self.offering.id,
310
                ProjectGroup.project_set_id == ProjectSet.id,
311
                ProjectGroupMembership.project_group_id == ProjectGroup.id,
312
                ProjectGroupMembership.user_id == self.user.id)
313
1080.1.26 by me at id
ivle.database: Add an Enrolment class, and reference(set)s between all of the
314
    __init__ = _kwarg_init
315
316
    def __repr__(self):
317
        return "<%s %r in %r>" % (type(self).__name__, self.user,
318
                                  self.offering)
1080.1.36 by William Grant
ivle.database: Add ProjectSet, Project, ProjectGroup, ProjectGroupMembership
319
1080.1.39 by Matt Giuca
ivle.database: Added __all__ to the top of the file.
320
# PROJECTS #
321
1080.1.36 by William Grant
ivle.database: Add ProjectSet, Project, ProjectGroup, ProjectGroupMembership
322
class ProjectSet(Storm):
323
    __storm_table__ = "project_set"
324
325
    id = Int(name="projectsetid", primary=True)
326
    offering_id = Int(name="offeringid")
327
    offering = Reference(offering_id, Offering.id)
328
    max_students_per_group = Int()
329
1080.1.77 by William Grant
ivle.database.ProjectSet: Add projects and project_groups referencesets.
330
    projects = ReferenceSet(id, 'Project.project_set_id')
331
    project_groups = ReferenceSet(id, 'ProjectGroup.project_set_id')
332
1080.1.36 by William Grant
ivle.database: Add ProjectSet, Project, ProjectGroup, ProjectGroupMembership
333
    __init__ = _kwarg_init
334
335
    def __repr__(self):
336
        return "<%s %d in %r>" % (type(self).__name__, self.id,
337
                                  self.offering)
338
339
class Project(Storm):
340
    __storm_table__ = "project"
341
342
    id = Int(name="projectid", primary=True)
343
    synopsis = Unicode()
344
    url = Unicode()
345
    project_set_id = Int(name="projectsetid")
346
    project_set = Reference(project_set_id, ProjectSet.id)
347
    deadline = DateTime()
348
349
    __init__ = _kwarg_init
350
351
    def __repr__(self):
352
        return "<%s '%s' in %r>" % (type(self).__name__, self.synopsis,
353
                                  self.project_set.offering)
354
355
class ProjectGroup(Storm):
356
    __storm_table__ = "project_group"
357
358
    id = Int(name="groupid", primary=True)
359
    name = Unicode(name="groupnm")
360
    project_set_id = Int(name="projectsetid")
361
    project_set = Reference(project_set_id, ProjectSet.id)
362
    nick = Unicode()
363
    created_by_id = Int(name="createdby")
364
    created_by = Reference(created_by_id, User.id)
365
    epoch = DateTime()
366
1080.1.78 by William Grant
ivle.database.ProjectGroup.members: Use a ReferenceSet.
367
    members = ReferenceSet(id,
368
                           "ProjectGroupMembership.project_group_id",
369
                           "ProjectGroupMembership.user_id",
370
                           "User.id")
371
1080.1.36 by William Grant
ivle.database: Add ProjectSet, Project, ProjectGroup, ProjectGroupMembership
372
    __init__ = _kwarg_init
373
374
    def __repr__(self):
375
        return "<%s %s in %r>" % (type(self).__name__, self.name,
376
                                  self.project_set.offering)
377
378
class ProjectGroupMembership(Storm):
379
    __storm_table__ = "group_member"
380
    __storm_primary__ = "user_id", "project_group_id"
381
382
    user_id = Int(name="loginid")
383
    user = Reference(user_id, User.id)
384
    project_group_id = Int(name="groupid")
385
    project_group = Reference(project_group_id, ProjectGroup.id)
386
387
    __init__ = _kwarg_init
388
389
    def __repr__(self):
390
        return "<%s %r in %r>" % (type(self).__name__, self.user,
391
                                  self.project_group)
392
1080.1.40 by Matt Giuca
ivle.database: Added Worksheet and Exercise classes (more to come in this
393
# WORKSHEETS AND EXERCISES #
394
395
class Exercise(Storm):
1099.1.195 by William Grant
Rename problem to exercise in the DB.
396
    __storm_table__ = "exercise"
1099.1.114 by Nick Chadwick
Modified the database so that exercises are now stored in the database, rather
397
    id = Unicode(primary=True, name="identifier")
398
    name = Unicode()
399
    description = Unicode()
400
    partial = Unicode()
401
    solution = Unicode()
402
    include = Unicode()
403
    num_rows = Int()
1080.1.40 by Matt Giuca
ivle.database: Added Worksheet and Exercise classes (more to come in this
404
1080.1.50 by Matt Giuca
ivle.database: Added WorksheetExercise (relates worksheets to exercises), and
405
    worksheets = ReferenceSet(id,
406
        'WorksheetExercise.exercise_id',
407
        'WorksheetExercise.worksheet_id',
408
        'Worksheet.id'
409
    )
1099.1.114 by Nick Chadwick
Modified the database so that exercises are now stored in the database, rather
410
    
1099.1.212 by Nick Chadwick
Added a new page to display exercises. This will then be modified to
411
    test_suites = ReferenceSet(id, 
412
        'TestSuite.exercise_id',
413
        order_by='seq_no')
1080.1.50 by Matt Giuca
ivle.database: Added WorksheetExercise (relates worksheets to exercises), and
414
1080.1.40 by Matt Giuca
ivle.database: Added Worksheet and Exercise classes (more to come in this
415
    __init__ = _kwarg_init
416
417
    def __repr__(self):
418
        return "<%s %s>" % (type(self).__name__, self.name)
419
1099.1.210 by Nick Chadwick
Modified the database layer so that exercises have a get_permissions
420
    def get_permissions(self, user):
421
        perms = set()
422
        if user is not None:
1101 by William Grant
Privileges (apart from admin) are now offering-local, not global.
423
            if user.admin:
1099.1.210 by Nick Chadwick
Modified the database layer so that exercises have a get_permissions
424
                perms.add('edit')
425
                perms.add('view')
426
        return perms
1080.1.51 by Matt Giuca
tutorial: Replaced call to ivle.db.create_worksheet with local code (roughly
427
1080.1.40 by Matt Giuca
ivle.database: Added Worksheet and Exercise classes (more to come in this
428
class Worksheet(Storm):
429
    __storm_table__ = "worksheet"
430
431
    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
432
    offering_id = Int(name="offeringid")
1099.4.1 by Nick Chadwick
Working on putting worksheets into the database.
433
    identifier = Unicode()
434
    name = Unicode()
1080.1.40 by Matt Giuca
ivle.database: Added Worksheet and Exercise classes (more to come in this
435
    assessable = Bool()
1099.4.1 by Nick Chadwick
Working on putting worksheets into the database.
436
    data = Unicode()
1099.1.180 by Nick Chadwick
This commit changes the tutorial service, which now almost exclusively
437
    seq_no = Int()
438
    format = Unicode()
1080.1.40 by Matt Giuca
ivle.database: Added Worksheet and Exercise classes (more to come in this
439
1099.1.141 by Nick Chadwick
Updated the exercises to be loaded from the database, not a local file.
440
    attempts = ReferenceSet(id, "ExerciseAttempt.worksheetid")
1099.1.118 by William Grant
Fix a bad reference introduced with the worksheet changes.
441
    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
442
1103 by William Grant
Worksheet.worksheet_exercises now only contains active ones.
443
    all_worksheet_exercises = ReferenceSet(id,
444
        'WorksheetExercise.worksheet_id')
445
446
    # Use worksheet_exercises to get access to the *active* WorksheetExercise
447
    # 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
448
    # "optional" field.
1099.1.220 by Nick Chadwick
Merged from trunk
449
1103 by William Grant
Worksheet.worksheet_exercises now only contains active ones.
450
    @property
451
    def worksheet_exercises(self):
452
        return self.all_worksheet_exercises.find(active=True)
1080.1.50 by Matt Giuca
ivle.database: Added WorksheetExercise (relates worksheets to exercises), and
453
1080.1.40 by Matt Giuca
ivle.database: Added Worksheet and Exercise classes (more to come in this
454
    __init__ = _kwarg_init
455
456
    def __repr__(self):
457
        return "<%s %s>" % (type(self).__name__, self.name)
1080.1.47 by Matt Giuca
ivle.database: Added Worksheet.get_by_name method.
458
459
    # XXX Refactor this - make it an instance method of Subject rather than a
460
    # class method of Worksheet. Can't do that now because Subject isn't
461
    # linked referentially to the Worksheet.
462
    @classmethod
463
    def get_by_name(cls, store, subjectname, worksheetname):
464
        """
465
        Get the Worksheet from the db associated with a given store, subject
466
        name and worksheet name.
467
        """
468
        return store.find(cls, cls.subject == unicode(subjectname),
469
            cls.name == unicode(worksheetname)).one()
1080.1.50 by Matt Giuca
ivle.database: Added WorksheetExercise (relates worksheets to exercises), and
470
1080.1.51 by Matt Giuca
tutorial: Replaced call to ivle.db.create_worksheet with local code (roughly
471
    def remove_all_exercises(self, store):
472
        """
473
        Remove all exercises from this worksheet.
474
        This does not delete the exercises themselves. It just removes them
475
        from the worksheet.
476
        """
477
        store.find(WorksheetExercise,
478
            WorksheetExercise.worksheet == self).remove()
1099.1.141 by Nick Chadwick
Updated the exercises to be loaded from the database, not a local file.
479
            
480
    def get_permissions(self, user):
481
        return self.offering.get_permissions(user)
1099.1.220 by Nick Chadwick
Merged from trunk
482
    
483
    def get_xml(self):
484
        """Returns the xml of this worksheet, converts from rst if required."""
485
        if self.format == u'rst':
486
            ws_xml = '<worksheet>' + rst(self.data) + '</worksheet>'
487
            return ws_xml
488
        else:
489
            return self.data
1080.1.51 by Matt Giuca
tutorial: Replaced call to ivle.db.create_worksheet with local code (roughly
490
1080.1.50 by Matt Giuca
ivle.database: Added WorksheetExercise (relates worksheets to exercises), and
491
class WorksheetExercise(Storm):
1099.1.195 by William Grant
Rename problem to exercise in the DB.
492
    __storm_table__ = "worksheet_exercise"
1099.4.4 by Nick Chadwick
Made what should (hopefully) be the last changes to the database schema.
493
    
1099.1.195 by William Grant
Rename problem to exercise in the DB.
494
    id = Int(primary=True, name="ws_ex_id")
1080.1.50 by Matt Giuca
ivle.database: Added WorksheetExercise (relates worksheets to exercises), and
495
496
    worksheet_id = Int(name="worksheetid")
497
    worksheet = Reference(worksheet_id, Worksheet.id)
1099.1.195 by William Grant
Rename problem to exercise in the DB.
498
    exercise_id = Unicode(name="exerciseid")
1080.1.50 by Matt Giuca
ivle.database: Added WorksheetExercise (relates worksheets to exercises), and
499
    exercise = Reference(exercise_id, Exercise.id)
500
    optional = Bool()
1099.4.3 by Nick Chadwick
Updated the tutorial service, to now allow users to edit worksheets
501
    active = Bool()
502
    seq_no = Int()
1099.1.180 by Nick Chadwick
This commit changes the tutorial service, which now almost exclusively
503
    
504
    saves = ReferenceSet(id, "ExerciseSave.ws_ex_id")
1099.1.183 by William Grant
Fix a reference typo in ivle.database.
505
    attempts = ReferenceSet(id, "ExerciseAttempt.ws_ex_id")
1080.1.50 by Matt Giuca
ivle.database: Added WorksheetExercise (relates worksheets to exercises), and
506
507
    __init__ = _kwarg_init
508
509
    def __repr__(self):
510
        return "<%s %s in %s>" % (type(self).__name__, self.exercise.name,
1099.4.1 by Nick Chadwick
Working on putting worksheets into the database.
511
                                  self.worksheet.identifier)
1080.1.55 by Matt Giuca
ivle.database: Added ExerciseAttempt and ExerciseSave classes.
512
513
class ExerciseSave(Storm):
514
    """
515
    Represents a potential solution to an exercise that a user has submitted
516
    to the server for storage.
517
    A basic ExerciseSave is just the current saved text for this exercise for
518
    this user (doesn't count towards their attempts).
519
    ExerciseSave may be extended with additional semantics (such as
520
    ExerciseAttempt).
521
    """
1099.1.195 by William Grant
Rename problem to exercise in the DB.
522
    __storm_table__ = "exercise_save"
1099.1.180 by Nick Chadwick
This commit changes the tutorial service, which now almost exclusively
523
    __storm_primary__ = "ws_ex_id", "user_id"
524
1099.1.195 by William Grant
Rename problem to exercise in the DB.
525
    ws_ex_id = Int(name="ws_ex_id")
1099.1.180 by Nick Chadwick
This commit changes the tutorial service, which now almost exclusively
526
    worksheet_exercise = Reference(ws_ex_id, "WorksheetExercise.id")
527
1080.1.55 by Matt Giuca
ivle.database: Added ExerciseAttempt and ExerciseSave classes.
528
    user_id = Int(name="loginid")
529
    user = Reference(user_id, User.id)
530
    date = DateTime()
531
    text = Unicode()
532
533
    __init__ = _kwarg_init
534
535
    def __repr__(self):
536
        return "<%s %s by %s at %s>" % (type(self).__name__,
537
            self.exercise.name, self.user.login, self.date.strftime("%c"))
538
539
class ExerciseAttempt(ExerciseSave):
540
    """
541
    An ExerciseAttempt is a special case of an ExerciseSave. Like an
542
    ExerciseSave, it constitutes exercise solution data that the user has
543
    submitted to the server for storage.
544
    In addition, it contains additional information about the submission.
545
    complete - True if this submission was successful, rendering this exercise
546
        complete for this user.
547
    active - True if this submission is "active" (usually true). Submissions
548
        may be de-activated by privileged users for special reasons, and then
549
        they won't count (either as a penalty or success), but will still be
550
        stored.
551
    """
1099.1.195 by William Grant
Rename problem to exercise in the DB.
552
    __storm_table__ = "exercise_attempt"
1099.1.180 by Nick Chadwick
This commit changes the tutorial service, which now almost exclusively
553
    __storm_primary__ = "ws_ex_id", "user_id", "date"
1080.1.55 by Matt Giuca
ivle.database: Added ExerciseAttempt and ExerciseSave classes.
554
555
    # The "text" field is the same but has a different name in the DB table
556
    # for some reason.
557
    text = Unicode(name="attempt")
558
    complete = Bool()
559
    active = Bool()
1099.1.114 by Nick Chadwick
Modified the database so that exercises are now stored in the database, rather
560
    
1099.1.113 by William Grant
Give console and tutorial services security declarations.
561
    def get_permissions(self, user):
562
        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
563
  
564
class TestSuite(Storm):
565
    """A Testsuite acts as a container for the test cases of an exercise."""
566
    __storm_table__ = "test_suite"
567
    __storm_primary__ = "exercise_id", "suiteid"
568
    
569
    suiteid = Int()
1099.1.195 by William Grant
Rename problem to exercise in the DB.
570
    exercise_id = Unicode(name="exerciseid")
1099.1.141 by Nick Chadwick
Updated the exercises to be loaded from the database, not a local file.
571
    description = Unicode()
572
    seq_no = Int()
573
    function = Unicode()
574
    stdin = Unicode()
1099.1.114 by Nick Chadwick
Modified the database so that exercises are now stored in the database, rather
575
    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
576
    test_cases = ReferenceSet(suiteid, 'TestCase.suiteid', order_by="seq_no")
577
    variables = ReferenceSet(suiteid, 'TestSuiteVar.suiteid', order_by='arg_no')
1099.1.114 by Nick Chadwick
Modified the database so that exercises are now stored in the database, rather
578
579
class TestCase(Storm):
580
    """A TestCase is a member of a TestSuite.
581
    
582
    It contains the data necessary to check if an exercise is correct"""
583
    __storm_table__ = "test_case"
584
    __storm_primary__ = "testid", "suiteid"
585
    
586
    testid = Int()
587
    suiteid = Int()
1099.1.141 by Nick Chadwick
Updated the exercises to be loaded from the database, not a local file.
588
    suite = Reference(suiteid, "TestSuite.suiteid")
1099.1.114 by Nick Chadwick
Modified the database so that exercises are now stored in the database, rather
589
    passmsg = Unicode()
590
    failmsg = Unicode()
1099.1.141 by Nick Chadwick
Updated the exercises to be loaded from the database, not a local file.
591
    test_default = Unicode()
1099.1.114 by Nick Chadwick
Modified the database so that exercises are now stored in the database, rather
592
    seq_no = Int()
593
    
1099.1.141 by Nick Chadwick
Updated the exercises to be loaded from the database, not a local file.
594
    parts = ReferenceSet(testid, "TestCasePart.testid")
595
    
596
    __init__ = _kwarg_init
597
598
class TestSuiteVar(Storm):
599
    """A container for the arguments of a Test Suite"""
1099.1.195 by William Grant
Rename problem to exercise in the DB.
600
    __storm_table__ = "suite_variable"
1099.1.141 by Nick Chadwick
Updated the exercises to be loaded from the database, not a local file.
601
    __storm_primary__ = "varid"
602
    
603
    varid = Int()
604
    suiteid = Int()
605
    var_name = Unicode()
606
    var_value = Unicode()
607
    var_type = Unicode()
608
    arg_no = Int()
609
    
610
    suite = Reference(suiteid, "TestSuite.suiteid")
611
    
612
    __init__ = _kwarg_init
613
    
614
class TestCasePart(Storm):
615
    """A container for the test elements of a Test Case"""
1099.1.195 by William Grant
Rename problem to exercise in the DB.
616
    __storm_table__ = "test_case_part"
1099.1.141 by Nick Chadwick
Updated the exercises to be loaded from the database, not a local file.
617
    __storm_primary__ = "partid"
618
    
619
    partid = Int()
620
    testid = Int()
621
    
622
    part_type = Unicode()
623
    test_type = Unicode()
624
    data = Unicode()
625
    filename = Unicode()
626
    
627
    test = Reference(testid, "TestCase.testid")
628
    
1099.1.114 by Nick Chadwick
Modified the database so that exercises are now stored in the database, rather
629
    __init__ = _kwarg_init