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

« back to all changes in this revision

Viewing changes to ivle/database.py

  • Committer: me at id
  • Date: 2009-01-15 03:02:36 UTC
  • mto: This revision was merged to the branch mainline in revision 1090.
  • Revision ID: svn-v3-trunk0:2b9c9e99-6f39-0410-b283-7f802c844ae2:branches%2Fstorm:1150
ivle.makeuser.make_jail: Just take an ivle.database.User, rather than some
    attributes.

services/usrmgt-server: Give make_jail a User.

bin/ivle-remakeuser: Rewrite to use ivle.database.User.

Show diffs side-by-side

added added

removed removed

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