~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
1080.1.4 by matt.giuca
ivle.database: Added User class.
34
import ivle.caps
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',
1099.1.114 by Nick Chadwick
Modified the database so that exercises are now stored in the database, rather
42
            'AlreadyEnrolledError', 'TestCase', 'TestSuite'
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
    """
56
    return "postgres://%s:%s@%s:%d/%s" % (ivle.conf.db_user,
57
        ivle.conf.db_password, ivle.conf.db_host, ivle.conf.db_port,
58
        ivle.conf.db_dbname)
59
60
def get_store():
61
    """
62
    Open a database connection and transaction. Return a storm.store.Store
63
    instance connected to the configured IVLE database.
64
    """
65
    return Store(create_database(get_conn_string()))
1080.1.4 by matt.giuca
ivle.database: Added User class.
66
1080.1.39 by Matt Giuca
ivle.database: Added __all__ to the top of the file.
67
# USERS #
68
1080.1.26 by me at id
ivle.database: Add an Enrolment class, and reference(set)s between all of the
69
class User(Storm):
1080.1.4 by matt.giuca
ivle.database: Added User class.
70
    """
71
    Represents an IVLE user.
72
    """
73
    __storm_table__ = "login"
74
75
    id = Int(primary=True, name="loginid")
76
    login = Unicode()
77
    passhash = Unicode()
78
    state = Unicode()
79
    rolenm = Unicode()
80
    unixid = Int()
81
    nick = Unicode()
82
    pass_exp = DateTime()
83
    acct_exp = DateTime()
84
    last_login = DateTime()
85
    svn_pass = Unicode()
86
    email = Unicode()
87
    fullname = Unicode()
88
    studentid = Unicode()
89
    settings = Unicode()
90
91
    def _get_role(self):
92
        if self.rolenm is None:
93
            return None
94
        return ivle.caps.Role(self.rolenm)
95
    def _set_role(self, value):
96
        if not isinstance(value, ivle.caps.Role):
97
            raise TypeError("role must be an ivle.caps.Role")
98
        self.rolenm = unicode(value)
99
    role = property(_get_role, _set_role)
100
1080.1.25 by me at id
ivle.database: Add Subject, Semester and Offering.
101
    __init__ = _kwarg_init
1080.1.4 by matt.giuca
ivle.database: Added User class.
102
103
    def __repr__(self):
104
        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.
105
1080.1.13 by me at id
ivle.database.User: Add an authenticate() method, and a hash_password()
106
    def authenticate(self, password):
107
        """Validate a given password against this user.
108
109
        Returns True if the given password matches the password hash for this
110
        User, False if it doesn't match, and None if there is no hash for the
111
        user.
112
        """
113
        if self.passhash is None:
114
            return None
115
        return self.hash_password(password) == self.passhash
116
1080.1.7 by matt.giuca
The new ivle.database.User class is now used in Request and usrmgt, which
117
    def hasCap(self, capability):
1080.1.5 by matt.giuca
ivle.database.User: Add the missing methods from ivle.user.User.
118
        """Given a capability (which is a Role object), returns True if this
119
        User has that capability, False otherwise.
120
        """
121
        return self.role.hasCap(capability)
122
1080.1.15 by me at id
Give ivle.database.User {password,account}_expired attributes, and get
123
    @property
124
    def password_expired(self):
1080.1.5 by matt.giuca
ivle.database.User: Add the missing methods from ivle.user.User.
125
        fieldval = self.pass_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
127
128
    @property
129
    def account_expired(self):
1080.1.5 by matt.giuca
ivle.database.User: Add the missing methods from ivle.user.User.
130
        fieldval = self.acct_exp
1080.1.15 by me at id
Give ivle.database.User {password,account}_expired attributes, and get
131
        return fieldval is not None and datetime.datetime.now() > fieldval
1080.1.6 by matt.giuca
ivle.database.User: Added get_by_login method.
132
1099.1.121 by William Grant
Don't set req.user unless the login in the session specifies a valid user.
133
    @property
134
    def valid(self):
135
        return self.state == 'enabled' and not self.account_expired
136
1080.1.29 by me at id
ivle.database.User: Order 'enrolments' the same way as 'active_enrolments'.
137
    def _get_enrolments(self, justactive):
1080.1.27 by me at id
ivle.database.User: Add an 'active_enrolments' property, which returns a list
138
        return Store.of(self).find(Enrolment,
139
            Enrolment.user_id == self.id,
1080.1.29 by me at id
ivle.database.User: Order 'enrolments' the same way as 'active_enrolments'.
140
            (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
141
            Enrolment.offering_id == Offering.id,
142
            Offering.semester_id == Semester.id,
143
            Offering.subject_id == Subject.id).order_by(
144
                Desc(Semester.year),
145
                Desc(Semester.semester),
146
                Desc(Subject.code)
147
            )
148
1080.1.68 by William Grant
ivle.database.User: Add a write-only 'password' attribute. When set, it will
149
    def _set_password(self, password):
150
        if password is None:
151
            self.passhash = None
152
        else:
153
            self.passhash = unicode(User.hash_password(password))
154
    password = property(fset=_set_password)
155
1080.1.29 by me at id
ivle.database.User: Order 'enrolments' the same way as 'active_enrolments'.
156
    @property
1080.1.31 by me at id
ivle.database.User: Add 'subjects', an attribute containing currently
157
    def subjects(self):
158
        return Store.of(self).find(Subject,
159
            Enrolment.user_id == self.id,
160
            Enrolment.active == True,
161
            Offering.id == Enrolment.offering_id,
162
            Subject.id == Offering.subject_id).config(distinct=True)
163
1080.1.36 by William Grant
ivle.database: Add ProjectSet, Project, ProjectGroup, ProjectGroupMembership
164
    # TODO: Invitations should be listed too?
165
    def get_groups(self, offering=None):
166
        preds = [
167
            ProjectGroupMembership.user_id == self.id,
168
            ProjectGroup.id == ProjectGroupMembership.project_group_id,
169
        ]
170
        if offering:
171
            preds.extend([
172
                ProjectSet.offering_id == offering.id,
173
                ProjectGroup.project_set_id == ProjectSet.id,
174
            ])
175
        return Store.of(self).find(ProjectGroup, *preds)
176
177
    @property
178
    def groups(self):
179
        return self.get_groups()
180
1080.1.31 by me at id
ivle.database.User: Add 'subjects', an attribute containing currently
181
    @property
1080.1.29 by me at id
ivle.database.User: Order 'enrolments' the same way as 'active_enrolments'.
182
    def active_enrolments(self):
183
        '''A sanely ordered list of the user's active enrolments.'''
184
        return self._get_enrolments(True)
185
186
    @property
187
    def enrolments(self):
188
        '''A sanely ordered list of all of the user's enrolments.'''
189
        return self._get_enrolments(False) 
1080.1.27 by me at id
ivle.database.User: Add an 'active_enrolments' property, which returns a list
190
1080.1.13 by me at id
ivle.database.User: Add an authenticate() method, and a hash_password()
191
    @staticmethod
192
    def hash_password(password):
193
        return md5.md5(password).hexdigest()
194
1080.1.6 by matt.giuca
ivle.database.User: Added get_by_login method.
195
    @classmethod
196
    def get_by_login(cls, store, login):
197
        """
198
        Get the User from the db associated with a given store and
199
        login.
200
        """
1080.1.7 by matt.giuca
The new ivle.database.User class is now used in Request and usrmgt, which
201
        return store.find(cls, cls.login == unicode(login)).one()
1080.1.25 by me at id
ivle.database: Add Subject, Semester and Offering.
202
1099.1.110 by William Grant
Implement an authorization system in the new framework. This breaks the REST
203
    def get_permissions(self, user):
204
        if user and user.rolenm == 'admin' or user is self:
205
            return set(['view', 'edit'])
206
        else:
207
            return set()
208
1080.1.39 by Matt Giuca
ivle.database: Added __all__ to the top of the file.
209
# SUBJECTS AND ENROLMENTS #
210
1080.1.26 by me at id
ivle.database: Add an Enrolment class, and reference(set)s between all of the
211
class Subject(Storm):
1080.1.25 by me at id
ivle.database: Add Subject, Semester and Offering.
212
    __storm_table__ = "subject"
213
214
    id = Int(primary=True, name="subjectid")
215
    code = Unicode(name="subj_code")
216
    name = Unicode(name="subj_name")
217
    short_name = Unicode(name="subj_short_name")
218
    url = Unicode()
219
1080.1.26 by me at id
ivle.database: Add an Enrolment class, and reference(set)s between all of the
220
    offerings = ReferenceSet(id, 'Offering.subject_id')
221
1080.1.25 by me at id
ivle.database: Add Subject, Semester and Offering.
222
    __init__ = _kwarg_init
223
224
    def __repr__(self):
225
        return "<%s '%s'>" % (type(self).__name__, self.short_name)
226
1099.1.110 by William Grant
Implement an authorization system in the new framework. This breaks the REST
227
    def get_permissions(self, user):
228
        perms = set()
229
        if user is not None:
230
            perms.add('view')
231
        if user.rolenm == 'admin':
232
            perms.add('edit')
233
        return perms
234
1080.1.26 by me at id
ivle.database: Add an Enrolment class, and reference(set)s between all of the
235
class Semester(Storm):
1080.1.25 by me at id
ivle.database: Add Subject, Semester and Offering.
236
    __storm_table__ = "semester"
237
238
    id = Int(primary=True, name="semesterid")
239
    year = Unicode()
240
    semester = Unicode()
241
    active = Bool()
242
1080.1.26 by me at id
ivle.database: Add an Enrolment class, and reference(set)s between all of the
243
    offerings = ReferenceSet(id, 'Offering.semester_id')
244
1080.1.25 by me at id
ivle.database: Add Subject, Semester and Offering.
245
    __init__ = _kwarg_init
246
247
    def __repr__(self):
248
        return "<%s %s/%s>" % (type(self).__name__, self.year, self.semester)
249
1080.1.26 by me at id
ivle.database: Add an Enrolment class, and reference(set)s between all of the
250
class Offering(Storm):
1080.1.25 by me at id
ivle.database: Add Subject, Semester and Offering.
251
    __storm_table__ = "offering"
252
253
    id = Int(primary=True, name="offeringid")
254
    subject_id = Int(name="subject")
255
    subject = Reference(subject_id, Subject.id)
256
    semester_id = Int(name="semesterid")
257
    semester = Reference(semester_id, Semester.id)
258
    groups_student_permissions = Unicode()
259
1080.1.26 by me at id
ivle.database: Add an Enrolment class, and reference(set)s between all of the
260
    enrolments = ReferenceSet(id, 'Enrolment.offering_id')
1080.1.79 by William Grant
ivle.database.Offering: Add a members ReferenceSet.
261
    members = ReferenceSet(id,
262
                           'Enrolment.offering_id',
263
                           'Enrolment.user_id',
264
                           'User.id')
1080.1.76 by William Grant
ivle.database.Offering: Add project_sets referenceset.
265
    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
266
1099.1.114 by Nick Chadwick
Modified the database so that exercises are now stored in the database, rather
267
    worksheets = ReferenceSet(id, 'Worksheet.offering_id')
268
1080.1.25 by me at id
ivle.database: Add Subject, Semester and Offering.
269
    __init__ = _kwarg_init
270
271
    def __repr__(self):
272
        return "<%s %r in %r>" % (type(self).__name__, self.subject,
273
                                  self.semester)
1080.1.26 by me at id
ivle.database: Add an Enrolment class, and reference(set)s between all of the
274
1080.1.61 by William Grant
ivle.database: Add an Offering.enrol(user) method, which enrols the user in
275
    def enrol(self, user):
276
        '''Enrol a user in this offering.'''
277
        # We'll get a horrible database constraint violation error if we try
278
        # to add a second enrolment.
279
        if Store.of(self).find(Enrolment,
280
                               Enrolment.user_id == user.id,
281
                               Enrolment.offering_id == self.id).count() == 1:
282
            raise AlreadyEnrolledError()
283
284
        e = Enrolment(user=user, offering=self, active=True)
285
        self.enrolments.add(e)
286
1080.1.26 by me at id
ivle.database: Add an Enrolment class, and reference(set)s between all of the
287
class Enrolment(Storm):
288
    __storm_table__ = "enrolment"
289
    __storm_primary__ = "user_id", "offering_id"
290
291
    user_id = Int(name="loginid")
292
    user = Reference(user_id, User.id)
293
    offering_id = Int(name="offeringid")
294
    offering = Reference(offering_id, Offering.id)
295
    notes = Unicode()
296
    active = Bool()
297
1080.1.81 by William Grant
ivle.database.Enrolment: Add a groups attribute, containing groups of which
298
    @property
299
    def groups(self):
300
        return Store.of(self).find(ProjectGroup,
301
                ProjectSet.offering_id == self.offering.id,
302
                ProjectGroup.project_set_id == ProjectSet.id,
303
                ProjectGroupMembership.project_group_id == ProjectGroup.id,
304
                ProjectGroupMembership.user_id == self.user.id)
305
1080.1.26 by me at id
ivle.database: Add an Enrolment class, and reference(set)s between all of the
306
    __init__ = _kwarg_init
307
308
    def __repr__(self):
309
        return "<%s %r in %r>" % (type(self).__name__, self.user,
310
                                  self.offering)
1080.1.36 by William Grant
ivle.database: Add ProjectSet, Project, ProjectGroup, ProjectGroupMembership
311
1080.1.61 by William Grant
ivle.database: Add an Offering.enrol(user) method, which enrols the user in
312
class AlreadyEnrolledError(Exception):
313
    pass
314
1080.1.39 by Matt Giuca
ivle.database: Added __all__ to the top of the file.
315
# PROJECTS #
316
1080.1.36 by William Grant
ivle.database: Add ProjectSet, Project, ProjectGroup, ProjectGroupMembership
317
class ProjectSet(Storm):
318
    __storm_table__ = "project_set"
319
320
    id = Int(name="projectsetid", primary=True)
321
    offering_id = Int(name="offeringid")
322
    offering = Reference(offering_id, Offering.id)
323
    max_students_per_group = Int()
324
1080.1.77 by William Grant
ivle.database.ProjectSet: Add projects and project_groups referencesets.
325
    projects = ReferenceSet(id, 'Project.project_set_id')
326
    project_groups = ReferenceSet(id, 'ProjectGroup.project_set_id')
327
1080.1.36 by William Grant
ivle.database: Add ProjectSet, Project, ProjectGroup, ProjectGroupMembership
328
    __init__ = _kwarg_init
329
330
    def __repr__(self):
331
        return "<%s %d in %r>" % (type(self).__name__, self.id,
332
                                  self.offering)
333
334
class Project(Storm):
335
    __storm_table__ = "project"
336
337
    id = Int(name="projectid", primary=True)
338
    synopsis = Unicode()
339
    url = Unicode()
340
    project_set_id = Int(name="projectsetid")
341
    project_set = Reference(project_set_id, ProjectSet.id)
342
    deadline = DateTime()
343
344
    __init__ = _kwarg_init
345
346
    def __repr__(self):
347
        return "<%s '%s' in %r>" % (type(self).__name__, self.synopsis,
348
                                  self.project_set.offering)
349
350
class ProjectGroup(Storm):
351
    __storm_table__ = "project_group"
352
353
    id = Int(name="groupid", primary=True)
354
    name = Unicode(name="groupnm")
355
    project_set_id = Int(name="projectsetid")
356
    project_set = Reference(project_set_id, ProjectSet.id)
357
    nick = Unicode()
358
    created_by_id = Int(name="createdby")
359
    created_by = Reference(created_by_id, User.id)
360
    epoch = DateTime()
361
1080.1.78 by William Grant
ivle.database.ProjectGroup.members: Use a ReferenceSet.
362
    members = ReferenceSet(id,
363
                           "ProjectGroupMembership.project_group_id",
364
                           "ProjectGroupMembership.user_id",
365
                           "User.id")
366
1080.1.36 by William Grant
ivle.database: Add ProjectSet, Project, ProjectGroup, ProjectGroupMembership
367
    __init__ = _kwarg_init
368
369
    def __repr__(self):
370
        return "<%s %s in %r>" % (type(self).__name__, self.name,
371
                                  self.project_set.offering)
372
373
class ProjectGroupMembership(Storm):
374
    __storm_table__ = "group_member"
375
    __storm_primary__ = "user_id", "project_group_id"
376
377
    user_id = Int(name="loginid")
378
    user = Reference(user_id, User.id)
379
    project_group_id = Int(name="groupid")
380
    project_group = Reference(project_group_id, ProjectGroup.id)
381
382
    __init__ = _kwarg_init
383
384
    def __repr__(self):
385
        return "<%s %r in %r>" % (type(self).__name__, self.user,
386
                                  self.project_group)
387
1080.1.40 by Matt Giuca
ivle.database: Added Worksheet and Exercise classes (more to come in this
388
# WORKSHEETS AND EXERCISES #
389
390
class Exercise(Storm):
391
    # Note: Table "problem" is called "Exercise" in the Object layer, since
392
    # it's called that everywhere else.
393
    __storm_table__ = "problem"
1099.1.114 by Nick Chadwick
Modified the database so that exercises are now stored in the database, rather
394
#TODO: Add in a field for the user-friendly identifier
395
    id = Unicode(primary=True, name="identifier")
396
    name = Unicode()
397
    description = Unicode()
398
    partial = Unicode()
399
    solution = Unicode()
400
    include = Unicode()
401
    num_rows = Int()
1080.1.40 by Matt Giuca
ivle.database: Added Worksheet and Exercise classes (more to come in this
402
1080.1.50 by Matt Giuca
ivle.database: Added WorksheetExercise (relates worksheets to exercises), and
403
    worksheets = ReferenceSet(id,
404
        'WorksheetExercise.exercise_id',
405
        'WorksheetExercise.worksheet_id',
406
        'Worksheet.id'
407
    )
1099.1.114 by Nick Chadwick
Modified the database so that exercises are now stored in the database, rather
408
    
409
    test_suites = ReferenceSet(id, 'TestSuite.exercise_id')
1080.1.50 by Matt Giuca
ivle.database: Added WorksheetExercise (relates worksheets to exercises), and
410
1080.1.40 by Matt Giuca
ivle.database: Added Worksheet and Exercise classes (more to come in this
411
    __init__ = _kwarg_init
412
413
    def __repr__(self):
414
        return "<%s %s>" % (type(self).__name__, self.name)
415
1080.1.51 by Matt Giuca
tutorial: Replaced call to ivle.db.create_worksheet with local code (roughly
416
    @classmethod
417
    def get_by_name(cls, store, name):
418
        """
419
        Get the Exercise from the db associated with a given store and name.
1080.1.52 by Matt Giuca
ivle.database: Exercise.get_by_name, now auto-inserts and returns a new
420
        If the exercise is not in the database, creates it and inserts it
421
        automatically.
1080.1.51 by Matt Giuca
tutorial: Replaced call to ivle.db.create_worksheet with local code (roughly
422
        """
1080.1.52 by Matt Giuca
ivle.database: Exercise.get_by_name, now auto-inserts and returns a new
423
        ex = store.find(cls, cls.name == unicode(name)).one()
424
        if ex is not None:
425
            return ex
426
        ex = Exercise(name=unicode(name))
427
        store.add(ex)
428
        store.commit()
429
        return ex
1080.1.51 by Matt Giuca
tutorial: Replaced call to ivle.db.create_worksheet with local code (roughly
430
1080.1.40 by Matt Giuca
ivle.database: Added Worksheet and Exercise classes (more to come in this
431
class Worksheet(Storm):
432
    __storm_table__ = "worksheet"
433
434
    id = Int(primary=True, name="worksheetid")
435
    # XXX subject is not linked to a Subject object. This is a property of
436
    # the database, and will be refactored.
437
    subject = Unicode()
1099.1.114 by Nick Chadwick
Modified the database so that exercises are now stored in the database, rather
438
    offering_id = Int(name="offeringid")
1080.1.40 by Matt Giuca
ivle.database: Added Worksheet and Exercise classes (more to come in this
439
    name = Unicode(name="identifier")
440
    assessable = Bool()
441
    mtime = DateTime()
442
1099.1.118 by William Grant
Fix a bad reference introduced with the worksheet changes.
443
    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
444
1080.1.50 by Matt Giuca
ivle.database: Added WorksheetExercise (relates worksheets to exercises), and
445
    exercises = ReferenceSet(id,
446
        'WorksheetExercise.worksheet_id',
447
        'WorksheetExercise.exercise_id',
448
        Exercise.id)
449
    # Use worksheet_exercises to get access to the WorksheetExercise objects
450
    # binding worksheets to exercises. This is required to access the
451
    # "optional" field.
452
    worksheet_exercises = ReferenceSet(id,
453
        'WorksheetExercise.worksheet_id')
1099.1.114 by Nick Chadwick
Modified the database so that exercises are now stored in the database, rather
454
        
1080.1.50 by Matt Giuca
ivle.database: Added WorksheetExercise (relates worksheets to exercises), and
455
1080.1.40 by Matt Giuca
ivle.database: Added Worksheet and Exercise classes (more to come in this
456
    __init__ = _kwarg_init
457
458
    def __repr__(self):
459
        return "<%s %s>" % (type(self).__name__, self.name)
1080.1.47 by Matt Giuca
ivle.database: Added Worksheet.get_by_name method.
460
461
    # XXX Refactor this - make it an instance method of Subject rather than a
462
    # class method of Worksheet. Can't do that now because Subject isn't
463
    # linked referentially to the Worksheet.
464
    @classmethod
465
    def get_by_name(cls, store, subjectname, worksheetname):
466
        """
467
        Get the Worksheet from the db associated with a given store, subject
468
        name and worksheet name.
469
        """
470
        return store.find(cls, cls.subject == unicode(subjectname),
471
            cls.name == unicode(worksheetname)).one()
1080.1.50 by Matt Giuca
ivle.database: Added WorksheetExercise (relates worksheets to exercises), and
472
1080.1.51 by Matt Giuca
tutorial: Replaced call to ivle.db.create_worksheet with local code (roughly
473
    def remove_all_exercises(self, store):
474
        """
475
        Remove all exercises from this worksheet.
476
        This does not delete the exercises themselves. It just removes them
477
        from the worksheet.
478
        """
479
        store.find(WorksheetExercise,
480
            WorksheetExercise.worksheet == self).remove()
481
1080.1.50 by Matt Giuca
ivle.database: Added WorksheetExercise (relates worksheets to exercises), and
482
class WorksheetExercise(Storm):
483
    __storm_table__ = "worksheet_problem"
484
    __storm_primary__ = "worksheet_id", "exercise_id"
485
486
    worksheet_id = Int(name="worksheetid")
487
    worksheet = Reference(worksheet_id, Worksheet.id)
1099.1.114 by Nick Chadwick
Modified the database so that exercises are now stored in the database, rather
488
    exercise_id = Unicode(name="problemid")
1080.1.50 by Matt Giuca
ivle.database: Added WorksheetExercise (relates worksheets to exercises), and
489
    exercise = Reference(exercise_id, Exercise.id)
490
    optional = Bool()
491
492
    __init__ = _kwarg_init
493
494
    def __repr__(self):
495
        return "<%s %s in %s>" % (type(self).__name__, self.exercise.name,
496
                                  self.worksheet.name)
1080.1.55 by Matt Giuca
ivle.database: Added ExerciseAttempt and ExerciseSave classes.
497
498
class ExerciseSave(Storm):
499
    """
500
    Represents a potential solution to an exercise that a user has submitted
501
    to the server for storage.
502
    A basic ExerciseSave is just the current saved text for this exercise for
503
    this user (doesn't count towards their attempts).
504
    ExerciseSave may be extended with additional semantics (such as
505
    ExerciseAttempt).
506
    """
507
    __storm_table__ = "problem_save"
508
    __storm_primary__ = "exercise_id", "user_id", "date"
509
1099.1.114 by Nick Chadwick
Modified the database so that exercises are now stored in the database, rather
510
    exercise_id = Unicode(name="problemid")
1080.1.55 by Matt Giuca
ivle.database: Added ExerciseAttempt and ExerciseSave classes.
511
    exercise = Reference(exercise_id, Exercise.id)
512
    user_id = Int(name="loginid")
513
    user = Reference(user_id, User.id)
514
    date = DateTime()
515
    text = Unicode()
1099.1.114 by Nick Chadwick
Modified the database so that exercises are now stored in the database, rather
516
    worksheetid = Int()
517
    worksheet = Reference(worksheetid, Worksheet.id)
1080.1.55 by Matt Giuca
ivle.database: Added ExerciseAttempt and ExerciseSave classes.
518
519
    __init__ = _kwarg_init
520
521
    def __repr__(self):
522
        return "<%s %s by %s at %s>" % (type(self).__name__,
523
            self.exercise.name, self.user.login, self.date.strftime("%c"))
524
525
class ExerciseAttempt(ExerciseSave):
526
    """
527
    An ExerciseAttempt is a special case of an ExerciseSave. Like an
528
    ExerciseSave, it constitutes exercise solution data that the user has
529
    submitted to the server for storage.
530
    In addition, it contains additional information about the submission.
531
    complete - True if this submission was successful, rendering this exercise
532
        complete for this user.
533
    active - True if this submission is "active" (usually true). Submissions
534
        may be de-activated by privileged users for special reasons, and then
535
        they won't count (either as a penalty or success), but will still be
536
        stored.
537
    """
538
    __storm_table__ = "problem_attempt"
539
    __storm_primary__ = "exercise_id", "user_id", "date"
540
541
    # The "text" field is the same but has a different name in the DB table
542
    # for some reason.
543
    text = Unicode(name="attempt")
544
    complete = Bool()
545
    active = Bool()
1099.1.114 by Nick Chadwick
Modified the database so that exercises are now stored in the database, rather
546
    
1099.1.113 by William Grant
Give console and tutorial services security declarations.
547
    def get_permissions(self, user):
548
        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
549
  
550
class TestSuite(Storm):
551
    """A Testsuite acts as a container for the test cases of an exercise."""
552
    __storm_table__ = "test_suite"
553
    __storm_primary__ = "exercise_id", "suiteid"
554
    
555
    suiteid = Int()
556
    exercise_id = Unicode(name="problemid")
557
    exercise = Reference(exercise_id, Exercise.id)
558
    test_cases = ReferenceSet(suiteid, 'TestCase.suiteid')
559
    description = Unicode()
560
    seq_no = Int()
561
562
class TestCase(Storm):
563
    """A TestCase is a member of a TestSuite.
564
    
565
    It contains the data necessary to check if an exercise is correct"""
566
    __storm_table__ = "test_case"
567
    __storm_primary__ = "testid", "suiteid"
568
    
569
    testid = Int()
570
    suiteid = Int()
571
    suite = Reference(suiteid, TestSuite.suiteid)
572
    passmsg = Unicode()
573
    failmsg = Unicode()
574
    init = Unicode()
575
    code_type = Unicode()
576
    code = Unicode()
577
    testtype = Unicode()
578
    seq_no = Int()
579
    
580
    __init__ = _kwarg_init