~azzar1/unity/add-show-desktop-key

« back to all changes in this revision

Viewing changes to ivle/database.py

  • Committer: dilshan_a
  • Date: 2008-01-25 03:09:19 UTC
  • Revision ID: svn-v3-trunk0:2b9c9e99-6f39-0410-b283-7f802c844ae2:trunk:304
Added documentation of output of TestSuite.

Show diffs side-by-side

added added

removed removed

Lines of Context:
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
 
 
27
 
import md5
28
 
import datetime
29
 
 
30
 
from storm.locals import create_database, Store, Int, Unicode, DateTime, \
31
 
                         Reference, ReferenceSet, Bool, Storm, Desc
32
 
from storm.exceptions import NotOneError, IntegrityError
33
 
 
34
 
import ivle.conf
35
 
from ivle.worksheet.rst import rst
36
 
 
37
 
__all__ = ['get_store',
38
 
            'User',
39
 
            'Subject', 'Semester', 'Offering', 'Enrolment',
40
 
            'ProjectSet', 'Project', 'ProjectGroup', 'ProjectGroupMembership',
41
 
            'Assessed', 'ProjectSubmission', 'ProjectExtension',
42
 
            'Exercise', 'Worksheet', 'WorksheetExercise',
43
 
            'ExerciseSave', 'ExerciseAttempt',
44
 
            'TestCase', 'TestSuite', 'TestSuiteVar'
45
 
        ]
46
 
 
47
 
def _kwarg_init(self, **kwargs):
48
 
    for k,v in kwargs.items():
49
 
        if k.startswith('_') or not hasattr(self.__class__, k):
50
 
            raise TypeError("%s got an unexpected keyword argument '%s'"
51
 
                % (self.__class__.__name__, k))
52
 
        setattr(self, k, v)
53
 
 
54
 
def get_conn_string():
55
 
    """
56
 
    Returns the Storm connection string, generated from the conf file.
57
 
    """
58
 
 
59
 
    clusterstr = ''
60
 
    if ivle.conf.db_user:
61
 
        clusterstr += ivle.conf.db_user
62
 
        if ivle.conf.db_password:
63
 
            clusterstr += ':' + ivle.conf.db_password
64
 
        clusterstr += '@'
65
 
 
66
 
    host = ivle.conf.db_host or 'localhost'
67
 
    port = ivle.conf.db_port or 5432
68
 
 
69
 
    clusterstr += '%s:%d' % (host, port)
70
 
 
71
 
    return "postgres://%s/%s" % (clusterstr, ivle.conf.db_dbname)
72
 
 
73
 
def get_store():
74
 
    """
75
 
    Open a database connection and transaction. Return a storm.store.Store
76
 
    instance connected to the configured IVLE database.
77
 
    """
78
 
    return Store(create_database(get_conn_string()))
79
 
 
80
 
# USERS #
81
 
 
82
 
class User(Storm):
83
 
    """
84
 
    Represents an IVLE user.
85
 
    """
86
 
    __storm_table__ = "login"
87
 
 
88
 
    id = Int(primary=True, name="loginid")
89
 
    login = Unicode()
90
 
    passhash = Unicode()
91
 
    state = Unicode()
92
 
    admin = Bool()
93
 
    unixid = Int()
94
 
    nick = Unicode()
95
 
    pass_exp = DateTime()
96
 
    acct_exp = DateTime()
97
 
    last_login = DateTime()
98
 
    svn_pass = Unicode()
99
 
    email = Unicode()
100
 
    fullname = Unicode()
101
 
    studentid = Unicode()
102
 
    settings = Unicode()
103
 
 
104
 
    __init__ = _kwarg_init
105
 
 
106
 
    def __repr__(self):
107
 
        return "<%s '%s'>" % (type(self).__name__, self.login)
108
 
 
109
 
    def authenticate(self, password):
110
 
        """Validate a given password against this user.
111
 
 
112
 
        Returns True if the given password matches the password hash for this
113
 
        User, False if it doesn't match, and None if there is no hash for the
114
 
        user.
115
 
        """
116
 
        if self.passhash is None:
117
 
            return None
118
 
        return self.hash_password(password) == self.passhash
119
 
 
120
 
    @property
121
 
    def display_name(self):
122
 
        return self.fullname
123
 
 
124
 
    @property
125
 
    def password_expired(self):
126
 
        fieldval = self.pass_exp
127
 
        return fieldval is not None and datetime.datetime.now() > fieldval
128
 
 
129
 
    @property
130
 
    def account_expired(self):
131
 
        fieldval = self.acct_exp
132
 
        return fieldval is not None and datetime.datetime.now() > fieldval
133
 
 
134
 
    @property
135
 
    def valid(self):
136
 
        return self.state == 'enabled' and not self.account_expired
137
 
 
138
 
    def _get_enrolments(self, justactive):
139
 
        return Store.of(self).find(Enrolment,
140
 
            Enrolment.user_id == self.id,
141
 
            (Enrolment.active == True) if justactive else True,
142
 
            Enrolment.offering_id == Offering.id,
143
 
            Offering.semester_id == Semester.id,
144
 
            Offering.subject_id == Subject.id).order_by(
145
 
                Desc(Semester.year),
146
 
                Desc(Semester.semester),
147
 
                Desc(Subject.code)
148
 
            )
149
 
 
150
 
    def _set_password(self, password):
151
 
        if password is None:
152
 
            self.passhash = None
153
 
        else:
154
 
            self.passhash = unicode(User.hash_password(password))
155
 
    password = property(fset=_set_password)
156
 
 
157
 
    @property
158
 
    def subjects(self):
159
 
        return Store.of(self).find(Subject,
160
 
            Enrolment.user_id == self.id,
161
 
            Enrolment.active == True,
162
 
            Offering.id == Enrolment.offering_id,
163
 
            Subject.id == Offering.subject_id).config(distinct=True)
164
 
 
165
 
    # TODO: Invitations should be listed too?
166
 
    def get_groups(self, offering=None):
167
 
        preds = [
168
 
            ProjectGroupMembership.user_id == self.id,
169
 
            ProjectGroup.id == ProjectGroupMembership.project_group_id,
170
 
        ]
171
 
        if offering:
172
 
            preds.extend([
173
 
                ProjectSet.offering_id == offering.id,
174
 
                ProjectGroup.project_set_id == ProjectSet.id,
175
 
            ])
176
 
        return Store.of(self).find(ProjectGroup, *preds)
177
 
 
178
 
    @property
179
 
    def groups(self):
180
 
        return self.get_groups()
181
 
 
182
 
    @property
183
 
    def active_enrolments(self):
184
 
        '''A sanely ordered list of the user's active enrolments.'''
185
 
        return self._get_enrolments(True)
186
 
 
187
 
    @property
188
 
    def enrolments(self):
189
 
        '''A sanely ordered list of all of the user's enrolments.'''
190
 
        return self._get_enrolments(False) 
191
 
 
192
 
    def get_projects(self, offering=None, active_only=True):
193
 
        '''Return Projects that the user can submit.
194
 
 
195
 
        This will include projects for offerings in which the user is
196
 
        enrolled, as long as the project is not in a project set which has
197
 
        groups (ie. if maximum number of group members is 0).
198
 
 
199
 
        Unless active_only is False, only projects for active offerings will
200
 
        be returned.
201
 
 
202
 
        If an offering is specified, returned projects will be limited to
203
 
        those for that offering.
204
 
        '''
205
 
        return Store.of(self).find(Project,
206
 
            Project.project_set_id == ProjectSet.id,
207
 
            ProjectSet.max_students_per_group == 0,
208
 
            ProjectSet.offering_id == Offering.id,
209
 
            (offering is None) or (Offering.id == offering.id),
210
 
            Semester.id == Offering.semester_id,
211
 
            (not active_only) or (Semester.state == u'current'),
212
 
            Enrolment.offering_id == Offering.id,
213
 
            Enrolment.user_id == self.id)
214
 
 
215
 
    @staticmethod
216
 
    def hash_password(password):
217
 
        return md5.md5(password).hexdigest()
218
 
 
219
 
    @classmethod
220
 
    def get_by_login(cls, store, login):
221
 
        """
222
 
        Get the User from the db associated with a given store and
223
 
        login.
224
 
        """
225
 
        return store.find(cls, cls.login == unicode(login)).one()
226
 
 
227
 
    def get_permissions(self, user):
228
 
        if user and user.admin or user is self:
229
 
            return set(['view', 'edit', 'submit_project'])
230
 
        else:
231
 
            return set()
232
 
 
233
 
# SUBJECTS AND ENROLMENTS #
234
 
 
235
 
class Subject(Storm):
236
 
    __storm_table__ = "subject"
237
 
 
238
 
    id = Int(primary=True, name="subjectid")
239
 
    code = Unicode(name="subj_code")
240
 
    name = Unicode(name="subj_name")
241
 
    short_name = Unicode(name="subj_short_name")
242
 
    url = Unicode()
243
 
 
244
 
    offerings = ReferenceSet(id, 'Offering.subject_id')
245
 
 
246
 
    __init__ = _kwarg_init
247
 
 
248
 
    def __repr__(self):
249
 
        return "<%s '%s'>" % (type(self).__name__, self.short_name)
250
 
 
251
 
    def get_permissions(self, user):
252
 
        perms = set()
253
 
        if user is not None:
254
 
            perms.add('view')
255
 
            if user.admin:
256
 
                perms.add('edit')
257
 
        return perms
258
 
 
259
 
class Semester(Storm):
260
 
    __storm_table__ = "semester"
261
 
 
262
 
    id = Int(primary=True, name="semesterid")
263
 
    year = Unicode()
264
 
    semester = Unicode()
265
 
    state = Unicode()
266
 
 
267
 
    offerings = ReferenceSet(id, 'Offering.semester_id')
268
 
    enrolments = ReferenceSet(id,
269
 
                              'Offering.semester_id',
270
 
                              'Offering.id',
271
 
                              'Enrolment.offering_id')
272
 
 
273
 
    __init__ = _kwarg_init
274
 
 
275
 
    def __repr__(self):
276
 
        return "<%s %s/%s>" % (type(self).__name__, self.year, self.semester)
277
 
 
278
 
class Offering(Storm):
279
 
    __storm_table__ = "offering"
280
 
 
281
 
    id = Int(primary=True, name="offeringid")
282
 
    subject_id = Int(name="subject")
283
 
    subject = Reference(subject_id, Subject.id)
284
 
    semester_id = Int(name="semesterid")
285
 
    semester = Reference(semester_id, Semester.id)
286
 
    groups_student_permissions = Unicode()
287
 
 
288
 
    enrolments = ReferenceSet(id, 'Enrolment.offering_id')
289
 
    members = ReferenceSet(id,
290
 
                           'Enrolment.offering_id',
291
 
                           'Enrolment.user_id',
292
 
                           'User.id')
293
 
    project_sets = ReferenceSet(id, 'ProjectSet.offering_id')
294
 
 
295
 
    worksheets = ReferenceSet(id, 
296
 
        'Worksheet.offering_id', 
297
 
        order_by="seq_no"
298
 
    )
299
 
 
300
 
    __init__ = _kwarg_init
301
 
 
302
 
    def __repr__(self):
303
 
        return "<%s %r in %r>" % (type(self).__name__, self.subject,
304
 
                                  self.semester)
305
 
 
306
 
    def enrol(self, user, role=u'student'):
307
 
        '''Enrol a user in this offering.'''
308
 
        enrolment = Store.of(self).find(Enrolment,
309
 
                               Enrolment.user_id == user.id,
310
 
                               Enrolment.offering_id == self.id).one()
311
 
 
312
 
        if enrolment is None:
313
 
            enrolment = Enrolment(user=user, offering=self)
314
 
            self.enrolments.add(enrolment)
315
 
 
316
 
        enrolment.active = True
317
 
        enrolment.role = role
318
 
 
319
 
    def unenrol(self, user):
320
 
        '''Unenrol a user from this offering.'''
321
 
        enrolment = Store.of(self).find(Enrolment,
322
 
                               Enrolment.user_id == user.id,
323
 
                               Enrolment.offering_id == self.id).one()
324
 
        Store.of(enrolment).remove(enrolment)
325
 
 
326
 
    def get_permissions(self, user):
327
 
        perms = set()
328
 
        if user is not None:
329
 
            enrolment = self.get_enrolment(user)
330
 
            if enrolment or user.admin:
331
 
                perms.add('view')
332
 
            if (enrolment and enrolment.role in (u'tutor', u'lecturer')) \
333
 
               or user.admin:
334
 
                perms.add('edit')
335
 
        return perms
336
 
 
337
 
    def get_enrolment(self, user):
338
 
        try:
339
 
            enrolment = self.enrolments.find(user=user).one()
340
 
        except NotOneError:
341
 
            enrolment = None
342
 
 
343
 
        return enrolment
344
 
 
345
 
class Enrolment(Storm):
346
 
    __storm_table__ = "enrolment"
347
 
    __storm_primary__ = "user_id", "offering_id"
348
 
 
349
 
    user_id = Int(name="loginid")
350
 
    user = Reference(user_id, User.id)
351
 
    offering_id = Int(name="offeringid")
352
 
    offering = Reference(offering_id, Offering.id)
353
 
    role = Unicode()
354
 
    notes = Unicode()
355
 
    active = Bool()
356
 
 
357
 
    @property
358
 
    def groups(self):
359
 
        return Store.of(self).find(ProjectGroup,
360
 
                ProjectSet.offering_id == self.offering.id,
361
 
                ProjectGroup.project_set_id == ProjectSet.id,
362
 
                ProjectGroupMembership.project_group_id == ProjectGroup.id,
363
 
                ProjectGroupMembership.user_id == self.user.id)
364
 
 
365
 
    __init__ = _kwarg_init
366
 
 
367
 
    def __repr__(self):
368
 
        return "<%s %r in %r>" % (type(self).__name__, self.user,
369
 
                                  self.offering)
370
 
 
371
 
# PROJECTS #
372
 
 
373
 
class ProjectSet(Storm):
374
 
    __storm_table__ = "project_set"
375
 
 
376
 
    id = Int(name="projectsetid", primary=True)
377
 
    offering_id = Int(name="offeringid")
378
 
    offering = Reference(offering_id, Offering.id)
379
 
    max_students_per_group = Int()
380
 
 
381
 
    projects = ReferenceSet(id, 'Project.project_set_id')
382
 
    project_groups = ReferenceSet(id, 'ProjectGroup.project_set_id')
383
 
 
384
 
    __init__ = _kwarg_init
385
 
 
386
 
    def __repr__(self):
387
 
        return "<%s %d in %r>" % (type(self).__name__, self.id,
388
 
                                  self.offering)
389
 
 
390
 
    def get_permissions(self, user):
391
 
        return self.offering.get_permissions(user)
392
 
 
393
 
class Project(Storm):
394
 
    __storm_table__ = "project"
395
 
 
396
 
    id = Int(name="projectid", primary=True)
397
 
    name = Unicode()
398
 
    short_name = Unicode()
399
 
    synopsis = Unicode()
400
 
    url = Unicode()
401
 
    project_set_id = Int(name="projectsetid")
402
 
    project_set = Reference(project_set_id, ProjectSet.id)
403
 
    deadline = DateTime()
404
 
 
405
 
    assesseds = ReferenceSet(id, 'Assessed.project_id')
406
 
    submissions = ReferenceSet(id,
407
 
                               'Assessed.project_id',
408
 
                               'Assessed.id',
409
 
                               'ProjectSubmission.assessed_id')
410
 
 
411
 
    __init__ = _kwarg_init
412
 
 
413
 
    def __repr__(self):
414
 
        return "<%s '%s' in %r>" % (type(self).__name__, self.short_name,
415
 
                                  self.project_set.offering)
416
 
 
417
 
    def can_submit(self, principal):
418
 
        return (self in principal.get_projects() and
419
 
                self.deadline > datetime.datetime.now())
420
 
 
421
 
    def submit(self, principal, path, revision, who):
422
 
        """Submit a Subversion path and revision to a project.
423
 
 
424
 
        'principal' is the owner of the Subversion repository, and the
425
 
        entity on behalf of whom the submission is being made. 'path' is
426
 
        a path within that repository, and 'revision' specifies which
427
 
        revision of that path. 'who' is the person making the submission.
428
 
        """
429
 
 
430
 
        if not self.can_submit(principal):
431
 
            raise Exception('cannot submit')
432
 
 
433
 
        a = Assessed.get(Store.of(self), principal, self)
434
 
        ps = ProjectSubmission()
435
 
        ps.path = path
436
 
        ps.revision = revision
437
 
        ps.date_submitted = datetime.datetime.now()
438
 
        ps.assessed = a
439
 
        ps.submitter = who
440
 
 
441
 
        return ps
442
 
 
443
 
    def get_permissions(self, user):
444
 
        return self.project_set.offering.get_permissions(user)
445
 
 
446
 
 
447
 
class ProjectGroup(Storm):
448
 
    __storm_table__ = "project_group"
449
 
 
450
 
    id = Int(name="groupid", primary=True)
451
 
    name = Unicode(name="groupnm")
452
 
    project_set_id = Int(name="projectsetid")
453
 
    project_set = Reference(project_set_id, ProjectSet.id)
454
 
    nick = Unicode()
455
 
    created_by_id = Int(name="createdby")
456
 
    created_by = Reference(created_by_id, User.id)
457
 
    epoch = DateTime()
458
 
 
459
 
    members = ReferenceSet(id,
460
 
                           "ProjectGroupMembership.project_group_id",
461
 
                           "ProjectGroupMembership.user_id",
462
 
                           "User.id")
463
 
 
464
 
    __init__ = _kwarg_init
465
 
 
466
 
    def __repr__(self):
467
 
        return "<%s %s in %r>" % (type(self).__name__, self.name,
468
 
                                  self.project_set.offering)
469
 
 
470
 
    @property
471
 
    def display_name(self):
472
 
        return '%s (%s)' % (self.nick, self.name)
473
 
 
474
 
    def get_projects(self, offering=None, active_only=True):
475
 
        '''Return Projects that the group can submit.
476
 
 
477
 
        This will include projects in the project set which owns this group,
478
 
        unless the project set disallows groups (in which case none will be
479
 
        returned).
480
 
 
481
 
        Unless active_only is False, projects will only be returned if the
482
 
        group's offering is active.
483
 
 
484
 
        If an offering is specified, projects will only be returned if it
485
 
        matches the group's.
486
 
        '''
487
 
        return Store.of(self).find(Project,
488
 
            Project.project_set_id == ProjectSet.id,
489
 
            ProjectSet.id == self.project_set.id,
490
 
            ProjectSet.max_students_per_group > 0,
491
 
            ProjectSet.offering_id == Offering.id,
492
 
            (offering is None) or (Offering.id == offering.id),
493
 
            Semester.id == Offering.semester_id,
494
 
            (not active_only) or (Semester.state == u'current'))
495
 
 
496
 
 
497
 
    def get_permissions(self, user):
498
 
        if user.admin or user in self.members:
499
 
            return set(['submit_project'])
500
 
        else:
501
 
            return set()
502
 
 
503
 
class ProjectGroupMembership(Storm):
504
 
    __storm_table__ = "group_member"
505
 
    __storm_primary__ = "user_id", "project_group_id"
506
 
 
507
 
    user_id = Int(name="loginid")
508
 
    user = Reference(user_id, User.id)
509
 
    project_group_id = Int(name="groupid")
510
 
    project_group = Reference(project_group_id, ProjectGroup.id)
511
 
 
512
 
    __init__ = _kwarg_init
513
 
 
514
 
    def __repr__(self):
515
 
        return "<%s %r in %r>" % (type(self).__name__, self.user,
516
 
                                  self.project_group)
517
 
 
518
 
class Assessed(Storm):
519
 
    __storm_table__ = "assessed"
520
 
 
521
 
    id = Int(name="assessedid", primary=True)
522
 
    user_id = Int(name="loginid")
523
 
    user = Reference(user_id, User.id)
524
 
    project_group_id = Int(name="groupid")
525
 
    project_group = Reference(project_group_id, ProjectGroup.id)
526
 
 
527
 
    project_id = Int(name="projectid")
528
 
    project = Reference(project_id, Project.id)
529
 
 
530
 
    extensions = ReferenceSet(id, 'ProjectExtension.assessed_id')
531
 
    submissions = ReferenceSet(id, 'ProjectSubmission.assessed_id')
532
 
 
533
 
    def __repr__(self):
534
 
        return "<%s %r in %r>" % (type(self).__name__,
535
 
            self.user or self.project_group, self.project)
536
 
 
537
 
    @classmethod
538
 
    def get(cls, store, principal, project):
539
 
        t = type(principal)
540
 
        if t not in (User, ProjectGroup):
541
 
            raise AssertionError('principal must be User or ProjectGroup')
542
 
 
543
 
        a = store.find(cls,
544
 
            (t is User) or (cls.project_group_id == principal.id),
545
 
            (t is ProjectGroup) or (cls.user_id == principal.id),
546
 
            Project.id == project.id).one()
547
 
 
548
 
        if a is None:
549
 
            a = cls()
550
 
            if t is User:
551
 
                a.user = principal
552
 
            else:
553
 
                a.project_group = principal
554
 
            a.project = project
555
 
            store.add(a)
556
 
 
557
 
        return a
558
 
 
559
 
 
560
 
class ProjectExtension(Storm):
561
 
    __storm_table__ = "project_extension"
562
 
 
563
 
    id = Int(name="extensionid", primary=True)
564
 
    assessed_id = Int(name="assessedid")
565
 
    assessed = Reference(assessed_id, Assessed.id)
566
 
    deadline = DateTime()
567
 
    approver_id = Int(name="approver")
568
 
    approver = Reference(approver_id, User.id)
569
 
    notes = Unicode()
570
 
 
571
 
class ProjectSubmission(Storm):
572
 
    __storm_table__ = "project_submission"
573
 
 
574
 
    id = Int(name="submissionid", primary=True)
575
 
    assessed_id = Int(name="assessedid")
576
 
    assessed = Reference(assessed_id, Assessed.id)
577
 
    path = Unicode()
578
 
    revision = Int()
579
 
    submitter_id = Int(name="submitter")
580
 
    submitter = Reference(submitter_id, User.id)
581
 
    date_submitted = DateTime()
582
 
 
583
 
 
584
 
# WORKSHEETS AND EXERCISES #
585
 
 
586
 
class Exercise(Storm):
587
 
    __storm_table__ = "exercise"
588
 
    id = Unicode(primary=True, name="identifier")
589
 
    name = Unicode()
590
 
    description = Unicode()
591
 
    partial = Unicode()
592
 
    solution = Unicode()
593
 
    include = Unicode()
594
 
    num_rows = Int()
595
 
 
596
 
    worksheet_exercises =  ReferenceSet(id,
597
 
        'WorksheetExercise.exercise_id')
598
 
 
599
 
    worksheets = ReferenceSet(id,
600
 
        'WorksheetExercise.exercise_id',
601
 
        'WorksheetExercise.worksheet_id',
602
 
        'Worksheet.id'
603
 
    )
604
 
    
605
 
    test_suites = ReferenceSet(id, 
606
 
        'TestSuite.exercise_id',
607
 
        order_by='seq_no')
608
 
 
609
 
    __init__ = _kwarg_init
610
 
 
611
 
    def __repr__(self):
612
 
        return "<%s %s>" % (type(self).__name__, self.name)
613
 
 
614
 
    def get_permissions(self, user):
615
 
        perms = set()
616
 
        roles = set()
617
 
        if user is not None:
618
 
            if user.admin:
619
 
                perms.add('edit')
620
 
                perms.add('view')
621
 
            elif u'lecturer' in set((e.role for e in user.active_enrolments)):
622
 
                perms.add('edit')
623
 
                perms.add('view')
624
 
            elif u'tutor' in set((e.role for e in user.active_enrolments)):
625
 
                perms.add('edit')
626
 
                perms.add('view')
627
 
            
628
 
        return perms
629
 
    
630
 
    def get_description(self):
631
 
        return rst(self.description)
632
 
 
633
 
    def delete(self):
634
 
        """Deletes the exercise, providing it has no associated worksheets."""
635
 
        if (self.worksheet_exercises.count() > 0):
636
 
            raise IntegrityError()
637
 
        for suite in self.test_suites:
638
 
            suite.delete()
639
 
        Store.of(self).remove(self)
640
 
 
641
 
class Worksheet(Storm):
642
 
    __storm_table__ = "worksheet"
643
 
 
644
 
    id = Int(primary=True, name="worksheetid")
645
 
    offering_id = Int(name="offeringid")
646
 
    identifier = Unicode()
647
 
    name = Unicode()
648
 
    assessable = Bool()
649
 
    data = Unicode()
650
 
    seq_no = Int()
651
 
    format = Unicode()
652
 
 
653
 
    attempts = ReferenceSet(id, "ExerciseAttempt.worksheetid")
654
 
    offering = Reference(offering_id, 'Offering.id')
655
 
 
656
 
    all_worksheet_exercises = ReferenceSet(id,
657
 
        'WorksheetExercise.worksheet_id')
658
 
 
659
 
    # Use worksheet_exercises to get access to the *active* WorksheetExercise
660
 
    # objects binding worksheets to exercises. This is required to access the
661
 
    # "optional" field.
662
 
 
663
 
    @property
664
 
    def worksheet_exercises(self):
665
 
        return self.all_worksheet_exercises.find(active=True)
666
 
 
667
 
    __init__ = _kwarg_init
668
 
 
669
 
    def __repr__(self):
670
 
        return "<%s %s>" % (type(self).__name__, self.name)
671
 
 
672
 
    # XXX Refactor this - make it an instance method of Subject rather than a
673
 
    # class method of Worksheet. Can't do that now because Subject isn't
674
 
    # linked referentially to the Worksheet.
675
 
    @classmethod
676
 
    def get_by_name(cls, store, subjectname, worksheetname):
677
 
        """
678
 
        Get the Worksheet from the db associated with a given store, subject
679
 
        name and worksheet name.
680
 
        """
681
 
        return store.find(cls, cls.subject == unicode(subjectname),
682
 
            cls.name == unicode(worksheetname)).one()
683
 
 
684
 
    def remove_all_exercises(self):
685
 
        """
686
 
        Remove all exercises from this worksheet.
687
 
        This does not delete the exercises themselves. It just removes them
688
 
        from the worksheet.
689
 
        """
690
 
        store = Store.of(self)
691
 
        for ws_ex in self.all_worksheet_exercises:
692
 
            if ws_ex.saves.count() > 0 or ws_ex.attempts.count() > 0:
693
 
                raise IntegrityError()
694
 
        store.find(WorksheetExercise,
695
 
            WorksheetExercise.worksheet == self).remove()
696
 
            
697
 
    def get_permissions(self, user):
698
 
        return self.offering.get_permissions(user)
699
 
    
700
 
    def get_xml(self):
701
 
        """Returns the xml of this worksheet, converts from rst if required."""
702
 
        if self.format == u'rst':
703
 
            ws_xml = rst(self.data)
704
 
            return ws_xml
705
 
        else:
706
 
            return self.data
707
 
    
708
 
    def delete(self):
709
 
        """Deletes the worksheet, provided it has no attempts on any exercises.
710
 
        
711
 
        Returns True if delete succeeded, or False if this worksheet has
712
 
        attempts attached."""
713
 
        for ws_ex in self.all_worksheet_exercises:
714
 
            if ws_ex.saves.count() > 0 or ws_ex.attempts.count() > 0:
715
 
                raise IntegrityError()
716
 
        
717
 
        self.remove_all_exercises()
718
 
        Store.of(self).remove(self)
719
 
        
720
 
class WorksheetExercise(Storm):
721
 
    __storm_table__ = "worksheet_exercise"
722
 
    
723
 
    id = Int(primary=True, name="ws_ex_id")
724
 
 
725
 
    worksheet_id = Int(name="worksheetid")
726
 
    worksheet = Reference(worksheet_id, Worksheet.id)
727
 
    exercise_id = Unicode(name="exerciseid")
728
 
    exercise = Reference(exercise_id, Exercise.id)
729
 
    optional = Bool()
730
 
    active = Bool()
731
 
    seq_no = Int()
732
 
    
733
 
    saves = ReferenceSet(id, "ExerciseSave.ws_ex_id")
734
 
    attempts = ReferenceSet(id, "ExerciseAttempt.ws_ex_id")
735
 
 
736
 
    __init__ = _kwarg_init
737
 
 
738
 
    def __repr__(self):
739
 
        return "<%s %s in %s>" % (type(self).__name__, self.exercise.name,
740
 
                                  self.worksheet.identifier)
741
 
 
742
 
    def get_permissions(self, user):
743
 
        return self.worksheet.get_permissions(user)
744
 
    
745
 
 
746
 
class ExerciseSave(Storm):
747
 
    """
748
 
    Represents a potential solution to an exercise that a user has submitted
749
 
    to the server for storage.
750
 
    A basic ExerciseSave is just the current saved text for this exercise for
751
 
    this user (doesn't count towards their attempts).
752
 
    ExerciseSave may be extended with additional semantics (such as
753
 
    ExerciseAttempt).
754
 
    """
755
 
    __storm_table__ = "exercise_save"
756
 
    __storm_primary__ = "ws_ex_id", "user_id"
757
 
 
758
 
    ws_ex_id = Int(name="ws_ex_id")
759
 
    worksheet_exercise = Reference(ws_ex_id, "WorksheetExercise.id")
760
 
 
761
 
    user_id = Int(name="loginid")
762
 
    user = Reference(user_id, User.id)
763
 
    date = DateTime()
764
 
    text = Unicode()
765
 
 
766
 
    __init__ = _kwarg_init
767
 
 
768
 
    def __repr__(self):
769
 
        return "<%s %s by %s at %s>" % (type(self).__name__,
770
 
            self.exercise.name, self.user.login, self.date.strftime("%c"))
771
 
 
772
 
class ExerciseAttempt(ExerciseSave):
773
 
    """
774
 
    An ExerciseAttempt is a special case of an ExerciseSave. Like an
775
 
    ExerciseSave, it constitutes exercise solution data that the user has
776
 
    submitted to the server for storage.
777
 
    In addition, it contains additional information about the submission.
778
 
    complete - True if this submission was successful, rendering this exercise
779
 
        complete for this user.
780
 
    active - True if this submission is "active" (usually true). Submissions
781
 
        may be de-activated by privileged users for special reasons, and then
782
 
        they won't count (either as a penalty or success), but will still be
783
 
        stored.
784
 
    """
785
 
    __storm_table__ = "exercise_attempt"
786
 
    __storm_primary__ = "ws_ex_id", "user_id", "date"
787
 
 
788
 
    # The "text" field is the same but has a different name in the DB table
789
 
    # for some reason.
790
 
    text = Unicode(name="attempt")
791
 
    complete = Bool()
792
 
    active = Bool()
793
 
    
794
 
    def get_permissions(self, user):
795
 
        return set(['view']) if user is self.user else set()
796
 
  
797
 
class TestSuite(Storm):
798
 
    """A Testsuite acts as a container for the test cases of an exercise."""
799
 
    __storm_table__ = "test_suite"
800
 
    __storm_primary__ = "exercise_id", "suiteid"
801
 
    
802
 
    suiteid = Int()
803
 
    exercise_id = Unicode(name="exerciseid")
804
 
    description = Unicode()
805
 
    seq_no = Int()
806
 
    function = Unicode()
807
 
    stdin = Unicode()
808
 
    exercise = Reference(exercise_id, Exercise.id)
809
 
    test_cases = ReferenceSet(suiteid, 'TestCase.suiteid', order_by="seq_no")
810
 
    variables = ReferenceSet(suiteid, 'TestSuiteVar.suiteid', order_by='arg_no')
811
 
    
812
 
    def delete(self):
813
 
        """Delete this suite, without asking questions."""
814
 
        for vaariable in self.variables:
815
 
            variable.delete()
816
 
        for test_case in self.test_cases:
817
 
            test_case.delete()
818
 
        Store.of(self).remove(self)
819
 
 
820
 
class TestCase(Storm):
821
 
    """A TestCase is a member of a TestSuite.
822
 
    
823
 
    It contains the data necessary to check if an exercise is correct"""
824
 
    __storm_table__ = "test_case"
825
 
    __storm_primary__ = "testid", "suiteid"
826
 
    
827
 
    testid = Int()
828
 
    suiteid = Int()
829
 
    suite = Reference(suiteid, "TestSuite.suiteid")
830
 
    passmsg = Unicode()
831
 
    failmsg = Unicode()
832
 
    test_default = Unicode()
833
 
    seq_no = Int()
834
 
    
835
 
    parts = ReferenceSet(testid, "TestCasePart.testid")
836
 
    
837
 
    __init__ = _kwarg_init
838
 
    
839
 
    def delete(self):
840
 
        for part in self.parts:
841
 
            part.delete()
842
 
        Store.of(self).remove(self)
843
 
 
844
 
class TestSuiteVar(Storm):
845
 
    """A container for the arguments of a Test Suite"""
846
 
    __storm_table__ = "suite_variable"
847
 
    __storm_primary__ = "varid"
848
 
    
849
 
    varid = Int()
850
 
    suiteid = Int()
851
 
    var_name = Unicode()
852
 
    var_value = Unicode()
853
 
    var_type = Unicode()
854
 
    arg_no = Int()
855
 
    
856
 
    suite = Reference(suiteid, "TestSuite.suiteid")
857
 
    
858
 
    __init__ = _kwarg_init
859
 
    
860
 
    def delete(self):
861
 
        Store.of(self).remove(self)
862
 
    
863
 
class TestCasePart(Storm):
864
 
    """A container for the test elements of a Test Case"""
865
 
    __storm_table__ = "test_case_part"
866
 
    __storm_primary__ = "partid"
867
 
    
868
 
    partid = Int()
869
 
    testid = Int()
870
 
    
871
 
    part_type = Unicode()
872
 
    test_type = Unicode()
873
 
    data = Unicode()
874
 
    filename = Unicode()
875
 
    
876
 
    test = Reference(testid, "TestCase.testid")
877
 
    
878
 
    __init__ = _kwarg_init
879
 
    
880
 
    def delete(self):
881
 
        Store.of(self).remove(self)