~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
1099.1.242 by Nick Chadwick
Fixed a problem with exercise editor, which wasn't editing or adding
32
from storm.exceptions import NotOneError, IntegrityError
1080.1.2 by matt.giuca
New module: ivle.database. Classes and utilities for Storm ORM.
33
34
import ivle.conf
1099.1.220 by Nick Chadwick
Merged from trunk
35
from ivle.worksheet.rst import rst
1080.1.2 by matt.giuca
New module: ivle.database. Classes and utilities for Storm ORM.
36
1080.1.39 by Matt Giuca
ivle.database: Added __all__ to the top of the file.
37
__all__ = ['get_store',
38
            'User',
39
            'Subject', 'Semester', 'Offering', 'Enrolment',
40
            'ProjectSet', 'Project', 'ProjectGroup', 'ProjectGroupMembership',
1165.1.4 by William Grant
Add database classes for assessed, project_extension and project_submission.
41
            'Assessed', 'ProjectSubmission', 'ProjectExtension',
1080.1.59 by Matt Giuca
ivle.worksheet, ivle.database: Added/updated __all__.
42
            'Exercise', 'Worksheet', 'WorksheetExercise',
1080.1.61 by William Grant
ivle.database: Add an Offering.enrol(user) method, which enrols the user in
43
            'ExerciseSave', 'ExerciseAttempt',
1110 by William Grant
ivle-enrol now allows updating of existing enrolments. It also sets the role.
44
            'TestCase', 'TestSuite', 'TestSuiteVar'
1080.1.39 by Matt Giuca
ivle.database: Added __all__ to the top of the file.
45
        ]
46
1080.1.25 by me at id
ivle.database: Add Subject, Semester and Offering.
47
def _kwarg_init(self, **kwargs):
48
    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,
49
        if k.startswith('_') or not hasattr(self.__class__, k):
1080.1.25 by me at id
ivle.database: Add Subject, Semester and Offering.
50
            raise TypeError("%s got an unexpected keyword argument '%s'"
1080.1.45 by William Grant
ivle.database._kwarg_init: Fix exception throwing.
51
                % (self.__class__.__name__, k))
1080.1.25 by me at id
ivle.database: Add Subject, Semester and Offering.
52
        setattr(self, k, v)
53
1080.1.2 by matt.giuca
New module: ivle.database. Classes and utilities for Storm ORM.
54
def get_conn_string():
55
    """
56
    Returns the Storm connection string, generated from the conf file.
57
    """
1099.1.174 by William Grant
ivle.database.get_conn_string() now defaults to localhost:5432, rather than
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)
1080.1.2 by matt.giuca
New module: ivle.database. Classes and utilities for Storm ORM.
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()))
1080.1.4 by matt.giuca
ivle.database: Added User class.
79
1080.1.39 by Matt Giuca
ivle.database: Added __all__ to the top of the file.
80
# USERS #
81
1080.1.26 by me at id
ivle.database: Add an Enrolment class, and reference(set)s between all of the
82
class User(Storm):
1080.1.4 by matt.giuca
ivle.database: Added User class.
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()
1101 by William Grant
Privileges (apart from admin) are now offering-local, not global.
92
    admin = Bool()
1080.1.4 by matt.giuca
ivle.database: Added User class.
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
1080.1.25 by me at id
ivle.database: Add Subject, Semester and Offering.
104
    __init__ = _kwarg_init
1080.1.4 by matt.giuca
ivle.database: Added User class.
105
106
    def __repr__(self):
107
        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.
108
1080.1.13 by me at id
ivle.database.User: Add an authenticate() method, and a hash_password()
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
1080.1.15 by me at id
Give ivle.database.User {password,account}_expired attributes, and get
120
    @property
121
    def password_expired(self):
1080.1.5 by matt.giuca
ivle.database.User: Add the missing methods from ivle.user.User.
122
        fieldval = self.pass_exp
1080.1.15 by me at id
Give ivle.database.User {password,account}_expired attributes, and get
123
        return fieldval is not None and datetime.datetime.now() > fieldval
124
125
    @property
126
    def account_expired(self):
1080.1.5 by matt.giuca
ivle.database.User: Add the missing methods from ivle.user.User.
127
        fieldval = self.acct_exp
1080.1.15 by me at id
Give ivle.database.User {password,account}_expired attributes, and get
128
        return fieldval is not None and datetime.datetime.now() > fieldval
1080.1.6 by matt.giuca
ivle.database.User: Added get_by_login method.
129
1099.1.121 by William Grant
Don't set req.user unless the login in the session specifies a valid user.
130
    @property
131
    def valid(self):
132
        return self.state == 'enabled' and not self.account_expired
133
1080.1.29 by me at id
ivle.database.User: Order 'enrolments' the same way as 'active_enrolments'.
134
    def _get_enrolments(self, justactive):
1080.1.27 by me at id
ivle.database.User: Add an 'active_enrolments' property, which returns a list
135
        return Store.of(self).find(Enrolment,
136
            Enrolment.user_id == self.id,
1080.1.29 by me at id
ivle.database.User: Order 'enrolments' the same way as 'active_enrolments'.
137
            (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
138
            Enrolment.offering_id == Offering.id,
139
            Offering.semester_id == Semester.id,
140
            Offering.subject_id == Subject.id).order_by(
141
                Desc(Semester.year),
142
                Desc(Semester.semester),
143
                Desc(Subject.code)
144
            )
145
1080.1.68 by William Grant
ivle.database.User: Add a write-only 'password' attribute. When set, it will
146
    def _set_password(self, password):
147
        if password is None:
148
            self.passhash = None
149
        else:
150
            self.passhash = unicode(User.hash_password(password))
151
    password = property(fset=_set_password)
152
1080.1.29 by me at id
ivle.database.User: Order 'enrolments' the same way as 'active_enrolments'.
153
    @property
1080.1.31 by me at id
ivle.database.User: Add 'subjects', an attribute containing currently
154
    def subjects(self):
155
        return Store.of(self).find(Subject,
156
            Enrolment.user_id == self.id,
157
            Enrolment.active == True,
158
            Offering.id == Enrolment.offering_id,
159
            Subject.id == Offering.subject_id).config(distinct=True)
160
1080.1.36 by William Grant
ivle.database: Add ProjectSet, Project, ProjectGroup, ProjectGroupMembership
161
    # TODO: Invitations should be listed too?
162
    def get_groups(self, offering=None):
163
        preds = [
164
            ProjectGroupMembership.user_id == self.id,
165
            ProjectGroup.id == ProjectGroupMembership.project_group_id,
166
        ]
167
        if offering:
168
            preds.extend([
169
                ProjectSet.offering_id == offering.id,
170
                ProjectGroup.project_set_id == ProjectSet.id,
171
            ])
172
        return Store.of(self).find(ProjectGroup, *preds)
173
174
    @property
175
    def groups(self):
176
        return self.get_groups()
177
1080.1.31 by me at id
ivle.database.User: Add 'subjects', an attribute containing currently
178
    @property
1080.1.29 by me at id
ivle.database.User: Order 'enrolments' the same way as 'active_enrolments'.
179
    def active_enrolments(self):
180
        '''A sanely ordered list of the user's active enrolments.'''
181
        return self._get_enrolments(True)
182
183
    @property
184
    def enrolments(self):
185
        '''A sanely ordered list of all of the user's enrolments.'''
186
        return self._get_enrolments(False) 
1080.1.27 by me at id
ivle.database.User: Add an 'active_enrolments' property, which returns a list
187
1165.1.11 by William Grant
Let callsites ask User.get_projects() to show inactive offerings too.
188
    def get_projects(self, offering=None, active_only=True):
1165.1.10 by William Grant
Add User.get_projects(), returning a list of submission targets.
189
        '''Return Projects that the user can submit.
190
1165.1.11 by William Grant
Let callsites ask User.get_projects() to show inactive offerings too.
191
        This will include projects for offerings in which the user is
1165.1.10 by William Grant
Add User.get_projects(), returning a list of submission targets.
192
        enrolled, as long as the project is not in a project set which has
193
        groups (ie. if maximum number of group members is 0).
194
1165.1.11 by William Grant
Let callsites ask User.get_projects() to show inactive offerings too.
195
        Unless active_only is False, only projects for active offerings will
196
        be returned.
197
1165.1.10 by William Grant
Add User.get_projects(), returning a list of submission targets.
198
        If an offering is specified, returned projects will be limited to
199
        those for that offering.
200
        '''
201
        return Store.of(self).find(Project,
202
            Project.project_set_id == ProjectSet.id,
203
            ProjectSet.max_students_per_group == 0,
204
            ProjectSet.offering_id == Offering.id,
1165.1.11 by William Grant
Let callsites ask User.get_projects() to show inactive offerings too.
205
            (offering is None) or (Offering.id == offering.id),
1165.1.10 by William Grant
Add User.get_projects(), returning a list of submission targets.
206
            Semester.id == Offering.semester_id,
1165.1.11 by William Grant
Let callsites ask User.get_projects() to show inactive offerings too.
207
            (not active_only) or (Semester.state == u'current'),
1165.1.10 by William Grant
Add User.get_projects(), returning a list of submission targets.
208
            Enrolment.offering_id == Offering.id,
209
            Enrolment.user_id == self.id)
210
1080.1.13 by me at id
ivle.database.User: Add an authenticate() method, and a hash_password()
211
    @staticmethod
212
    def hash_password(password):
213
        return md5.md5(password).hexdigest()
214
1080.1.6 by matt.giuca
ivle.database.User: Added get_by_login method.
215
    @classmethod
216
    def get_by_login(cls, store, login):
217
        """
218
        Get the User from the db associated with a given store and
219
        login.
220
        """
1080.1.7 by matt.giuca
The new ivle.database.User class is now used in Request and usrmgt, which
221
        return store.find(cls, cls.login == unicode(login)).one()
1080.1.25 by me at id
ivle.database: Add Subject, Semester and Offering.
222
1099.1.110 by William Grant
Implement an authorization system in the new framework. This breaks the REST
223
    def get_permissions(self, user):
1101 by William Grant
Privileges (apart from admin) are now offering-local, not global.
224
        if user and user.admin or user is self:
1165.1.7 by William Grant
Grant submit_project on users to themselves, and on groups to their members.
225
            return set(['view', 'edit', 'submit_project'])
1099.1.110 by William Grant
Implement an authorization system in the new framework. This breaks the REST
226
        else:
227
            return set()
228
1080.1.39 by Matt Giuca
ivle.database: Added __all__ to the top of the file.
229
# SUBJECTS AND ENROLMENTS #
230
1080.1.26 by me at id
ivle.database: Add an Enrolment class, and reference(set)s between all of the
231
class Subject(Storm):
1080.1.25 by me at id
ivle.database: Add Subject, Semester and Offering.
232
    __storm_table__ = "subject"
233
234
    id = Int(primary=True, name="subjectid")
235
    code = Unicode(name="subj_code")
236
    name = Unicode(name="subj_name")
237
    short_name = Unicode(name="subj_short_name")
238
    url = Unicode()
239
1080.1.26 by me at id
ivle.database: Add an Enrolment class, and reference(set)s between all of the
240
    offerings = ReferenceSet(id, 'Offering.subject_id')
241
1080.1.25 by me at id
ivle.database: Add Subject, Semester and Offering.
242
    __init__ = _kwarg_init
243
244
    def __repr__(self):
245
        return "<%s '%s'>" % (type(self).__name__, self.short_name)
246
1099.1.110 by William Grant
Implement an authorization system in the new framework. This breaks the REST
247
    def get_permissions(self, user):
248
        perms = set()
249
        if user is not None:
250
            perms.add('view')
1101 by William Grant
Privileges (apart from admin) are now offering-local, not global.
251
            if user.admin:
1099.1.141 by Nick Chadwick
Updated the exercises to be loaded from the database, not a local file.
252
                perms.add('edit')
1099.1.110 by William Grant
Implement an authorization system in the new framework. This breaks the REST
253
        return perms
254
1080.1.26 by me at id
ivle.database: Add an Enrolment class, and reference(set)s between all of the
255
class Semester(Storm):
1080.1.25 by me at id
ivle.database: Add Subject, Semester and Offering.
256
    __storm_table__ = "semester"
257
258
    id = Int(primary=True, name="semesterid")
259
    year = Unicode()
260
    semester = Unicode()
1104 by William Grant
Replace Semester.active with Semester.state, allowing more useful state
261
    state = Unicode()
1080.1.25 by me at id
ivle.database: Add Subject, Semester and Offering.
262
1080.1.26 by me at id
ivle.database: Add an Enrolment class, and reference(set)s between all of the
263
    offerings = ReferenceSet(id, 'Offering.semester_id')
1124 by William Grant
Add Semester.enrolments.
264
    enrolments = ReferenceSet(id,
265
                              'Offering.semester_id',
266
                              'Offering.id',
267
                              'Enrolment.offering_id')
1080.1.26 by me at id
ivle.database: Add an Enrolment class, and reference(set)s between all of the
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 %s/%s>" % (type(self).__name__, self.year, self.semester)
273
1080.1.26 by me at id
ivle.database: Add an Enrolment class, and reference(set)s between all of the
274
class Offering(Storm):
1080.1.25 by me at id
ivle.database: Add Subject, Semester and Offering.
275
    __storm_table__ = "offering"
276
277
    id = Int(primary=True, name="offeringid")
278
    subject_id = Int(name="subject")
279
    subject = Reference(subject_id, Subject.id)
280
    semester_id = Int(name="semesterid")
281
    semester = Reference(semester_id, Semester.id)
282
    groups_student_permissions = Unicode()
283
1080.1.26 by me at id
ivle.database: Add an Enrolment class, and reference(set)s between all of the
284
    enrolments = ReferenceSet(id, 'Enrolment.offering_id')
1080.1.79 by William Grant
ivle.database.Offering: Add a members ReferenceSet.
285
    members = ReferenceSet(id,
286
                           'Enrolment.offering_id',
287
                           'Enrolment.user_id',
288
                           'User.id')
1080.1.76 by William Grant
ivle.database.Offering: Add project_sets referenceset.
289
    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
290
1099.1.180 by Nick Chadwick
This commit changes the tutorial service, which now almost exclusively
291
    worksheets = ReferenceSet(id, 
292
        'Worksheet.offering_id', 
1099.1.212 by Nick Chadwick
Added a new page to display exercises. This will then be modified to
293
        order_by="seq_no"
1099.1.180 by Nick Chadwick
This commit changes the tutorial service, which now almost exclusively
294
    )
1099.1.114 by Nick Chadwick
Modified the database so that exercises are now stored in the database, rather
295
1080.1.25 by me at id
ivle.database: Add Subject, Semester and Offering.
296
    __init__ = _kwarg_init
297
298
    def __repr__(self):
299
        return "<%s %r in %r>" % (type(self).__name__, self.subject,
300
                                  self.semester)
1080.1.26 by me at id
ivle.database: Add an Enrolment class, and reference(set)s between all of the
301
1110 by William Grant
ivle-enrol now allows updating of existing enrolments. It also sets the role.
302
    def enrol(self, user, role=u'student'):
1080.1.61 by William Grant
ivle.database: Add an Offering.enrol(user) method, which enrols the user in
303
        '''Enrol a user in this offering.'''
1110 by William Grant
ivle-enrol now allows updating of existing enrolments. It also sets the role.
304
        enrolment = Store.of(self).find(Enrolment,
1080.1.61 by William Grant
ivle.database: Add an Offering.enrol(user) method, which enrols the user in
305
                               Enrolment.user_id == user.id,
1110 by William Grant
ivle-enrol now allows updating of existing enrolments. It also sets the role.
306
                               Enrolment.offering_id == self.id).one()
307
308
        if enrolment is None:
309
            enrolment = Enrolment(user=user, offering=self)
310
            self.enrolments.add(enrolment)
311
312
        enrolment.active = True
313
        enrolment.role = role
1080.1.61 by William Grant
ivle.database: Add an Offering.enrol(user) method, which enrols the user in
314
1132 by William Grant
Add Offering.unenrol(), to unenrol a user from an offering.
315
    def unenrol(self, user):
316
        '''Unenrol a user from this offering.'''
317
        enrolment = Store.of(self).find(Enrolment,
318
                               Enrolment.user_id == user.id,
319
                               Enrolment.offering_id == self.id).one()
320
        Store.of(enrolment).remove(enrolment)
321
1099.1.141 by Nick Chadwick
Updated the exercises to be loaded from the database, not a local file.
322
    def get_permissions(self, user):
323
        perms = set()
324
        if user is not None:
1131 by William Grant
Offerings now give 'view' only to user enrolled in them. 'edit' is granted
325
            enrolment = self.get_enrolment(user)
326
            if enrolment or user.admin:
327
                perms.add('view')
328
            if (enrolment and enrolment.role in (u'tutor', u'lecturer')) \
329
               or user.admin:
1099.1.141 by Nick Chadwick
Updated the exercises to be loaded from the database, not a local file.
330
                perms.add('edit')
331
        return perms
332
1129 by William Grant
Move the group admin view to per-offering.
333
    def get_enrolment(self, user):
334
        try:
335
            enrolment = self.enrolments.find(user=user).one()
336
        except NotOneError:
337
            enrolment = None
338
339
        return enrolment
340
1080.1.26 by me at id
ivle.database: Add an Enrolment class, and reference(set)s between all of the
341
class Enrolment(Storm):
342
    __storm_table__ = "enrolment"
343
    __storm_primary__ = "user_id", "offering_id"
344
345
    user_id = Int(name="loginid")
346
    user = Reference(user_id, User.id)
347
    offering_id = Int(name="offeringid")
348
    offering = Reference(offering_id, Offering.id)
1101 by William Grant
Privileges (apart from admin) are now offering-local, not global.
349
    role = Unicode()
1080.1.26 by me at id
ivle.database: Add an Enrolment class, and reference(set)s between all of the
350
    notes = Unicode()
351
    active = Bool()
352
1080.1.81 by William Grant
ivle.database.Enrolment: Add a groups attribute, containing groups of which
353
    @property
354
    def groups(self):
355
        return Store.of(self).find(ProjectGroup,
356
                ProjectSet.offering_id == self.offering.id,
357
                ProjectGroup.project_set_id == ProjectSet.id,
358
                ProjectGroupMembership.project_group_id == ProjectGroup.id,
359
                ProjectGroupMembership.user_id == self.user.id)
360
1080.1.26 by me at id
ivle.database: Add an Enrolment class, and reference(set)s between all of the
361
    __init__ = _kwarg_init
362
363
    def __repr__(self):
364
        return "<%s %r in %r>" % (type(self).__name__, self.user,
365
                                  self.offering)
1080.1.36 by William Grant
ivle.database: Add ProjectSet, Project, ProjectGroup, ProjectGroupMembership
366
1080.1.39 by Matt Giuca
ivle.database: Added __all__ to the top of the file.
367
# PROJECTS #
368
1080.1.36 by William Grant
ivle.database: Add ProjectSet, Project, ProjectGroup, ProjectGroupMembership
369
class ProjectSet(Storm):
370
    __storm_table__ = "project_set"
371
372
    id = Int(name="projectsetid", primary=True)
373
    offering_id = Int(name="offeringid")
374
    offering = Reference(offering_id, Offering.id)
375
    max_students_per_group = Int()
376
1080.1.77 by William Grant
ivle.database.ProjectSet: Add projects and project_groups referencesets.
377
    projects = ReferenceSet(id, 'Project.project_set_id')
378
    project_groups = ReferenceSet(id, 'ProjectGroup.project_set_id')
379
1080.1.36 by William Grant
ivle.database: Add ProjectSet, Project, ProjectGroup, ProjectGroupMembership
380
    __init__ = _kwarg_init
381
382
    def __repr__(self):
383
        return "<%s %d in %r>" % (type(self).__name__, self.id,
384
                                  self.offering)
385
386
class Project(Storm):
387
    __storm_table__ = "project"
388
389
    id = Int(name="projectid", primary=True)
1165.1.4 by William Grant
Add database classes for assessed, project_extension and project_submission.
390
    name = Unicode()
391
    short_name = Unicode()
1080.1.36 by William Grant
ivle.database: Add ProjectSet, Project, ProjectGroup, ProjectGroupMembership
392
    synopsis = Unicode()
393
    url = Unicode()
394
    project_set_id = Int(name="projectsetid")
395
    project_set = Reference(project_set_id, ProjectSet.id)
396
    deadline = DateTime()
397
1165.1.5 by William Grant
Add relevant ReferenceSets to Project and Assessed.
398
    assesseds = ReferenceSet(id, 'Assessed.project_id')
399
    submissions = ReferenceSet(id,
400
                               'Assessed.project_id',
401
                               'Assessed.id',
402
                               'ProjectSubmission.assessed_id')
403
1080.1.36 by William Grant
ivle.database: Add ProjectSet, Project, ProjectGroup, ProjectGroupMembership
404
    __init__ = _kwarg_init
405
406
    def __repr__(self):
1165.1.4 by William Grant
Add database classes for assessed, project_extension and project_submission.
407
        return "<%s '%s' in %r>" % (type(self).__name__, self.short_name,
1080.1.36 by William Grant
ivle.database: Add ProjectSet, Project, ProjectGroup, ProjectGroupMembership
408
                                  self.project_set.offering)
409
410
class ProjectGroup(Storm):
411
    __storm_table__ = "project_group"
412
413
    id = Int(name="groupid", primary=True)
414
    name = Unicode(name="groupnm")
415
    project_set_id = Int(name="projectsetid")
416
    project_set = Reference(project_set_id, ProjectSet.id)
417
    nick = Unicode()
418
    created_by_id = Int(name="createdby")
419
    created_by = Reference(created_by_id, User.id)
420
    epoch = DateTime()
421
1080.1.78 by William Grant
ivle.database.ProjectGroup.members: Use a ReferenceSet.
422
    members = ReferenceSet(id,
423
                           "ProjectGroupMembership.project_group_id",
424
                           "ProjectGroupMembership.user_id",
425
                           "User.id")
426
1080.1.36 by William Grant
ivle.database: Add ProjectSet, Project, ProjectGroup, ProjectGroupMembership
427
    __init__ = _kwarg_init
428
429
    def __repr__(self):
430
        return "<%s %s in %r>" % (type(self).__name__, self.name,
431
                                  self.project_set.offering)
432
1165.1.7 by William Grant
Grant submit_project on users to themselves, and on groups to their members.
433
    def get_permissions(self, user):
434
        if user.admin or user in self.members:
435
            return set(['submit_project'])
436
        else:
437
            return set()
438
1080.1.36 by William Grant
ivle.database: Add ProjectSet, Project, ProjectGroup, ProjectGroupMembership
439
class ProjectGroupMembership(Storm):
440
    __storm_table__ = "group_member"
441
    __storm_primary__ = "user_id", "project_group_id"
442
443
    user_id = Int(name="loginid")
444
    user = Reference(user_id, User.id)
445
    project_group_id = Int(name="groupid")
446
    project_group = Reference(project_group_id, ProjectGroup.id)
447
448
    __init__ = _kwarg_init
449
450
    def __repr__(self):
451
        return "<%s %r in %r>" % (type(self).__name__, self.user,
452
                                  self.project_group)
453
1165.1.4 by William Grant
Add database classes for assessed, project_extension and project_submission.
454
class Assessed(Storm):
455
    __storm_table__ = "assessed"
456
457
    id = Int(name="assessedid", primary=True)
458
    user_id = Int(name="loginid")
459
    user = Reference(user_id, User.id)
460
    project_group_id = Int(name="groupid")
461
    project_group = Reference(project_group_id, ProjectGroup.id)
462
463
    project_id = Int(name="projectid")
464
    project = Reference(project_id, Project.id)
465
1165.1.5 by William Grant
Add relevant ReferenceSets to Project and Assessed.
466
    extensions = ReferenceSet(id, 'ProjectExtension.assessed_id')
467
    submissions = ReferenceSet(id, 'ProjectSubmission.assessed_id')
468
1165.1.4 by William Grant
Add database classes for assessed, project_extension and project_submission.
469
    def __repr__(self):
470
        return "<%s %r in %r>" % (type(self).__name__,
471
            self.user or self.project_group, self.project)
472
473
class ProjectExtension(Storm):
474
    __storm_table__ = "project_extension"
475
476
    id = Int(name="extensionid", primary=True)
477
    assessed_id = Int(name="assessedid")
478
    assessed = Reference(assessed_id, Assessed.id)
479
    deadline = DateTime()
480
    approver_id = Int(name="approver")
481
    approver = Reference(approver_id, User.id)
482
    notes = Unicode()
483
484
class ProjectSubmission(Storm):
485
    __storm_table__ = "project_submission"
486
487
    id = Int(name="submissionid", primary=True)
488
    assessed_id = Int(name="assessedid")
489
    assessed = Reference(assessed_id, Assessed.id)
490
    path = Unicode()
491
    revision = Int()
492
    date_submitted = DateTime()
493
494
1080.1.40 by Matt Giuca
ivle.database: Added Worksheet and Exercise classes (more to come in this
495
# WORKSHEETS AND EXERCISES #
496
497
class Exercise(Storm):
1099.1.195 by William Grant
Rename problem to exercise in the DB.
498
    __storm_table__ = "exercise"
1099.1.114 by Nick Chadwick
Modified the database so that exercises are now stored in the database, rather
499
    id = Unicode(primary=True, name="identifier")
500
    name = Unicode()
501
    description = Unicode()
502
    partial = Unicode()
503
    solution = Unicode()
504
    include = Unicode()
505
    num_rows = Int()
1080.1.40 by Matt Giuca
ivle.database: Added Worksheet and Exercise classes (more to come in this
506
1099.6.2 by Nick Chadwick
Added a listing of all exercises
507
    worksheet_exercises =  ReferenceSet(id,
508
        'WorksheetExercise.exercise_id')
509
1080.1.50 by Matt Giuca
ivle.database: Added WorksheetExercise (relates worksheets to exercises), and
510
    worksheets = ReferenceSet(id,
511
        'WorksheetExercise.exercise_id',
512
        'WorksheetExercise.worksheet_id',
513
        'Worksheet.id'
514
    )
1099.1.114 by Nick Chadwick
Modified the database so that exercises are now stored in the database, rather
515
    
1099.1.212 by Nick Chadwick
Added a new page to display exercises. This will then be modified to
516
    test_suites = ReferenceSet(id, 
517
        'TestSuite.exercise_id',
518
        order_by='seq_no')
1080.1.50 by Matt Giuca
ivle.database: Added WorksheetExercise (relates worksheets to exercises), and
519
1080.1.40 by Matt Giuca
ivle.database: Added Worksheet and Exercise classes (more to come in this
520
    __init__ = _kwarg_init
521
522
    def __repr__(self):
523
        return "<%s %s>" % (type(self).__name__, self.name)
524
1099.1.210 by Nick Chadwick
Modified the database layer so that exercises have a get_permissions
525
    def get_permissions(self, user):
526
        perms = set()
1099.1.234 by Nick Chadwick
Permissions for editing and deleting exercises now come from the
527
        roles = set()
1099.1.210 by Nick Chadwick
Modified the database layer so that exercises have a get_permissions
528
        if user is not None:
1101 by William Grant
Privileges (apart from admin) are now offering-local, not global.
529
            if user.admin:
1099.1.210 by Nick Chadwick
Modified the database layer so that exercises have a get_permissions
530
                perms.add('edit')
531
                perms.add('view')
1099.1.236 by Nick Chadwick
Fixed a syntax error.
532
            elif 'lecturer' in set((e.role for e in user.active_enrolments)):
1099.1.234 by Nick Chadwick
Permissions for editing and deleting exercises now come from the
533
                perms.add('edit')
534
                perms.add('view')
1099.1.235 by Nick Chadwick
Made checking if a user is a lecturer in exercise get_permissions
535
            
1099.1.210 by Nick Chadwick
Modified the database layer so that exercises have a get_permissions
536
        return perms
1099.6.3 by Nick Chadwick
Edited the exercise service to delete individual parts of an exercise.
537
    
538
    def get_description(self):
1099.1.232 by Nick Chadwick
Removed XML from database. RST now generates a full xml document, not
539
        return rst(self.description)
1080.1.51 by Matt Giuca
tutorial: Replaced call to ivle.db.create_worksheet with local code (roughly
540
1099.1.242 by Nick Chadwick
Fixed a problem with exercise editor, which wasn't editing or adding
541
    def delete(self):
542
        """Deletes the exercise, providing it has no associated worksheets."""
543
        if (self.worksheet_exercises.count() > 0):
544
            raise IntegrityError()
1099.1.233 by Nick Chadwick
Exercise objects in the database module, along with their test cases,
545
        for suite in self.test_suites:
546
            suite.delete()
1099.1.242 by Nick Chadwick
Fixed a problem with exercise editor, which wasn't editing or adding
547
        Store.of(self).remove(self)
1099.1.233 by Nick Chadwick
Exercise objects in the database module, along with their test cases,
548
1080.1.40 by Matt Giuca
ivle.database: Added Worksheet and Exercise classes (more to come in this
549
class Worksheet(Storm):
550
    __storm_table__ = "worksheet"
551
552
    id = Int(primary=True, name="worksheetid")
1099.1.114 by Nick Chadwick
Modified the database so that exercises are now stored in the database, rather
553
    offering_id = Int(name="offeringid")
1099.4.1 by Nick Chadwick
Working on putting worksheets into the database.
554
    identifier = Unicode()
555
    name = Unicode()
1080.1.40 by Matt Giuca
ivle.database: Added Worksheet and Exercise classes (more to come in this
556
    assessable = Bool()
1099.4.1 by Nick Chadwick
Working on putting worksheets into the database.
557
    data = Unicode()
1099.1.180 by Nick Chadwick
This commit changes the tutorial service, which now almost exclusively
558
    seq_no = Int()
559
    format = Unicode()
1080.1.40 by Matt Giuca
ivle.database: Added Worksheet and Exercise classes (more to come in this
560
1099.1.141 by Nick Chadwick
Updated the exercises to be loaded from the database, not a local file.
561
    attempts = ReferenceSet(id, "ExerciseAttempt.worksheetid")
1099.1.118 by William Grant
Fix a bad reference introduced with the worksheet changes.
562
    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
563
1103 by William Grant
Worksheet.worksheet_exercises now only contains active ones.
564
    all_worksheet_exercises = ReferenceSet(id,
565
        'WorksheetExercise.worksheet_id')
566
567
    # Use worksheet_exercises to get access to the *active* WorksheetExercise
568
    # objects binding worksheets to exercises. This is required to access the
1080.1.50 by Matt Giuca
ivle.database: Added WorksheetExercise (relates worksheets to exercises), and
569
    # "optional" field.
1099.1.220 by Nick Chadwick
Merged from trunk
570
1103 by William Grant
Worksheet.worksheet_exercises now only contains active ones.
571
    @property
572
    def worksheet_exercises(self):
573
        return self.all_worksheet_exercises.find(active=True)
1080.1.50 by Matt Giuca
ivle.database: Added WorksheetExercise (relates worksheets to exercises), and
574
1080.1.40 by Matt Giuca
ivle.database: Added Worksheet and Exercise classes (more to come in this
575
    __init__ = _kwarg_init
576
577
    def __repr__(self):
578
        return "<%s %s>" % (type(self).__name__, self.name)
1080.1.47 by Matt Giuca
ivle.database: Added Worksheet.get_by_name method.
579
580
    # XXX Refactor this - make it an instance method of Subject rather than a
581
    # class method of Worksheet. Can't do that now because Subject isn't
582
    # linked referentially to the Worksheet.
583
    @classmethod
584
    def get_by_name(cls, store, subjectname, worksheetname):
585
        """
586
        Get the Worksheet from the db associated with a given store, subject
587
        name and worksheet name.
588
        """
589
        return store.find(cls, cls.subject == unicode(subjectname),
590
            cls.name == unicode(worksheetname)).one()
1080.1.50 by Matt Giuca
ivle.database: Added WorksheetExercise (relates worksheets to exercises), and
591
1099.1.242 by Nick Chadwick
Fixed a problem with exercise editor, which wasn't editing or adding
592
    def remove_all_exercises(self):
1080.1.51 by Matt Giuca
tutorial: Replaced call to ivle.db.create_worksheet with local code (roughly
593
        """
594
        Remove all exercises from this worksheet.
595
        This does not delete the exercises themselves. It just removes them
596
        from the worksheet.
597
        """
1099.1.242 by Nick Chadwick
Fixed a problem with exercise editor, which wasn't editing or adding
598
        store = Store.of(self)
599
        for ws_ex in self.all_worksheet_exercises:
600
            if ws_ex.saves.count() > 0 or ws_ex.attempts.count() > 0:
601
                raise IntegrityError()
1080.1.51 by Matt Giuca
tutorial: Replaced call to ivle.db.create_worksheet with local code (roughly
602
        store.find(WorksheetExercise,
603
            WorksheetExercise.worksheet == self).remove()
1099.1.141 by Nick Chadwick
Updated the exercises to be loaded from the database, not a local file.
604
            
605
    def get_permissions(self, user):
606
        return self.offering.get_permissions(user)
1099.1.220 by Nick Chadwick
Merged from trunk
607
    
608
    def get_xml(self):
609
        """Returns the xml of this worksheet, converts from rst if required."""
610
        if self.format == u'rst':
1099.1.232 by Nick Chadwick
Removed XML from database. RST now generates a full xml document, not
611
            ws_xml = rst(self.data)
1099.1.220 by Nick Chadwick
Merged from trunk
612
            return ws_xml
613
        else:
614
            return self.data
1099.1.233 by Nick Chadwick
Exercise objects in the database module, along with their test cases,
615
    
1099.1.242 by Nick Chadwick
Fixed a problem with exercise editor, which wasn't editing or adding
616
    def delete(self):
1099.1.233 by Nick Chadwick
Exercise objects in the database module, along with their test cases,
617
        """Deletes the worksheet, provided it has no attempts on any exercises.
618
        
619
        Returns True if delete succeeded, or False if this worksheet has
620
        attempts attached."""
621
        for ws_ex in self.all_worksheet_exercises:
622
            if ws_ex.saves.count() > 0 or ws_ex.attempts.count() > 0:
1099.1.242 by Nick Chadwick
Fixed a problem with exercise editor, which wasn't editing or adding
623
                raise IntegrityError()
1099.1.233 by Nick Chadwick
Exercise objects in the database module, along with their test cases,
624
        
625
        self.remove_all_exercises()
1099.1.242 by Nick Chadwick
Fixed a problem with exercise editor, which wasn't editing or adding
626
        Store.of(self).remove(self)
1099.1.233 by Nick Chadwick
Exercise objects in the database module, along with their test cases,
627
        
1080.1.50 by Matt Giuca
ivle.database: Added WorksheetExercise (relates worksheets to exercises), and
628
class WorksheetExercise(Storm):
1099.1.195 by William Grant
Rename problem to exercise in the DB.
629
    __storm_table__ = "worksheet_exercise"
1099.4.4 by Nick Chadwick
Made what should (hopefully) be the last changes to the database schema.
630
    
1099.1.195 by William Grant
Rename problem to exercise in the DB.
631
    id = Int(primary=True, name="ws_ex_id")
1080.1.50 by Matt Giuca
ivle.database: Added WorksheetExercise (relates worksheets to exercises), and
632
633
    worksheet_id = Int(name="worksheetid")
634
    worksheet = Reference(worksheet_id, Worksheet.id)
1099.1.195 by William Grant
Rename problem to exercise in the DB.
635
    exercise_id = Unicode(name="exerciseid")
1080.1.50 by Matt Giuca
ivle.database: Added WorksheetExercise (relates worksheets to exercises), and
636
    exercise = Reference(exercise_id, Exercise.id)
637
    optional = Bool()
1099.4.3 by Nick Chadwick
Updated the tutorial service, to now allow users to edit worksheets
638
    active = Bool()
639
    seq_no = Int()
1099.1.180 by Nick Chadwick
This commit changes the tutorial service, which now almost exclusively
640
    
641
    saves = ReferenceSet(id, "ExerciseSave.ws_ex_id")
1099.1.183 by William Grant
Fix a reference typo in ivle.database.
642
    attempts = ReferenceSet(id, "ExerciseAttempt.ws_ex_id")
1080.1.50 by Matt Giuca
ivle.database: Added WorksheetExercise (relates worksheets to exercises), and
643
644
    __init__ = _kwarg_init
645
646
    def __repr__(self):
647
        return "<%s %s in %s>" % (type(self).__name__, self.exercise.name,
1099.4.1 by Nick Chadwick
Working on putting worksheets into the database.
648
                                  self.worksheet.identifier)
1080.1.55 by Matt Giuca
ivle.database: Added ExerciseAttempt and ExerciseSave classes.
649
1131 by William Grant
Offerings now give 'view' only to user enrolled in them. 'edit' is granted
650
    def get_permissions(self, user):
651
        return self.worksheet.get_permissions(user)
1099.1.233 by Nick Chadwick
Exercise objects in the database module, along with their test cases,
652
    
1131 by William Grant
Offerings now give 'view' only to user enrolled in them. 'edit' is granted
653
1080.1.55 by Matt Giuca
ivle.database: Added ExerciseAttempt and ExerciseSave classes.
654
class ExerciseSave(Storm):
655
    """
656
    Represents a potential solution to an exercise that a user has submitted
657
    to the server for storage.
658
    A basic ExerciseSave is just the current saved text for this exercise for
659
    this user (doesn't count towards their attempts).
660
    ExerciseSave may be extended with additional semantics (such as
661
    ExerciseAttempt).
662
    """
1099.1.195 by William Grant
Rename problem to exercise in the DB.
663
    __storm_table__ = "exercise_save"
1099.1.180 by Nick Chadwick
This commit changes the tutorial service, which now almost exclusively
664
    __storm_primary__ = "ws_ex_id", "user_id"
665
1099.1.195 by William Grant
Rename problem to exercise in the DB.
666
    ws_ex_id = Int(name="ws_ex_id")
1099.1.180 by Nick Chadwick
This commit changes the tutorial service, which now almost exclusively
667
    worksheet_exercise = Reference(ws_ex_id, "WorksheetExercise.id")
668
1080.1.55 by Matt Giuca
ivle.database: Added ExerciseAttempt and ExerciseSave classes.
669
    user_id = Int(name="loginid")
670
    user = Reference(user_id, User.id)
671
    date = DateTime()
672
    text = Unicode()
673
674
    __init__ = _kwarg_init
675
676
    def __repr__(self):
677
        return "<%s %s by %s at %s>" % (type(self).__name__,
678
            self.exercise.name, self.user.login, self.date.strftime("%c"))
679
680
class ExerciseAttempt(ExerciseSave):
681
    """
682
    An ExerciseAttempt is a special case of an ExerciseSave. Like an
683
    ExerciseSave, it constitutes exercise solution data that the user has
684
    submitted to the server for storage.
685
    In addition, it contains additional information about the submission.
686
    complete - True if this submission was successful, rendering this exercise
687
        complete for this user.
688
    active - True if this submission is "active" (usually true). Submissions
689
        may be de-activated by privileged users for special reasons, and then
690
        they won't count (either as a penalty or success), but will still be
691
        stored.
692
    """
1099.1.195 by William Grant
Rename problem to exercise in the DB.
693
    __storm_table__ = "exercise_attempt"
1099.1.180 by Nick Chadwick
This commit changes the tutorial service, which now almost exclusively
694
    __storm_primary__ = "ws_ex_id", "user_id", "date"
1080.1.55 by Matt Giuca
ivle.database: Added ExerciseAttempt and ExerciseSave classes.
695
696
    # The "text" field is the same but has a different name in the DB table
697
    # for some reason.
698
    text = Unicode(name="attempt")
699
    complete = Bool()
700
    active = Bool()
1099.1.114 by Nick Chadwick
Modified the database so that exercises are now stored in the database, rather
701
    
1099.1.113 by William Grant
Give console and tutorial services security declarations.
702
    def get_permissions(self, user):
703
        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
704
  
705
class TestSuite(Storm):
706
    """A Testsuite acts as a container for the test cases of an exercise."""
707
    __storm_table__ = "test_suite"
708
    __storm_primary__ = "exercise_id", "suiteid"
709
    
710
    suiteid = Int()
1099.1.195 by William Grant
Rename problem to exercise in the DB.
711
    exercise_id = Unicode(name="exerciseid")
1099.1.141 by Nick Chadwick
Updated the exercises to be loaded from the database, not a local file.
712
    description = Unicode()
713
    seq_no = Int()
714
    function = Unicode()
715
    stdin = Unicode()
1099.1.114 by Nick Chadwick
Modified the database so that exercises are now stored in the database, rather
716
    exercise = Reference(exercise_id, Exercise.id)
1099.1.212 by Nick Chadwick
Added a new page to display exercises. This will then be modified to
717
    test_cases = ReferenceSet(suiteid, 'TestCase.suiteid', order_by="seq_no")
718
    variables = ReferenceSet(suiteid, 'TestSuiteVar.suiteid', order_by='arg_no')
1099.1.233 by Nick Chadwick
Exercise objects in the database module, along with their test cases,
719
    
1099.1.242 by Nick Chadwick
Fixed a problem with exercise editor, which wasn't editing or adding
720
    def delete(self):
1099.1.233 by Nick Chadwick
Exercise objects in the database module, along with their test cases,
721
        """Delete this suite, without asking questions."""
722
        for vaariable in self.variables:
723
            variable.delete()
724
        for test_case in self.test_cases:
725
            test_case.delete()
1099.1.242 by Nick Chadwick
Fixed a problem with exercise editor, which wasn't editing or adding
726
        Store.of(self).remove(self)
1099.1.114 by Nick Chadwick
Modified the database so that exercises are now stored in the database, rather
727
728
class TestCase(Storm):
729
    """A TestCase is a member of a TestSuite.
730
    
731
    It contains the data necessary to check if an exercise is correct"""
732
    __storm_table__ = "test_case"
733
    __storm_primary__ = "testid", "suiteid"
734
    
735
    testid = Int()
736
    suiteid = Int()
1099.1.141 by Nick Chadwick
Updated the exercises to be loaded from the database, not a local file.
737
    suite = Reference(suiteid, "TestSuite.suiteid")
1099.1.114 by Nick Chadwick
Modified the database so that exercises are now stored in the database, rather
738
    passmsg = Unicode()
739
    failmsg = Unicode()
1099.1.141 by Nick Chadwick
Updated the exercises to be loaded from the database, not a local file.
740
    test_default = Unicode()
1099.1.114 by Nick Chadwick
Modified the database so that exercises are now stored in the database, rather
741
    seq_no = Int()
742
    
1099.1.141 by Nick Chadwick
Updated the exercises to be loaded from the database, not a local file.
743
    parts = ReferenceSet(testid, "TestCasePart.testid")
744
    
745
    __init__ = _kwarg_init
1099.1.233 by Nick Chadwick
Exercise objects in the database module, along with their test cases,
746
    
1099.1.242 by Nick Chadwick
Fixed a problem with exercise editor, which wasn't editing or adding
747
    def delete(self):
1099.1.233 by Nick Chadwick
Exercise objects in the database module, along with their test cases,
748
        for part in self.parts:
749
            part.delete()
1099.1.242 by Nick Chadwick
Fixed a problem with exercise editor, which wasn't editing or adding
750
        Store.of(self).remove(self)
1099.1.141 by Nick Chadwick
Updated the exercises to be loaded from the database, not a local file.
751
752
class TestSuiteVar(Storm):
753
    """A container for the arguments of a Test Suite"""
1099.1.195 by William Grant
Rename problem to exercise in the DB.
754
    __storm_table__ = "suite_variable"
1099.1.141 by Nick Chadwick
Updated the exercises to be loaded from the database, not a local file.
755
    __storm_primary__ = "varid"
756
    
757
    varid = Int()
758
    suiteid = Int()
759
    var_name = Unicode()
760
    var_value = Unicode()
761
    var_type = Unicode()
762
    arg_no = Int()
763
    
764
    suite = Reference(suiteid, "TestSuite.suiteid")
765
    
766
    __init__ = _kwarg_init
767
    
1099.1.242 by Nick Chadwick
Fixed a problem with exercise editor, which wasn't editing or adding
768
    def delete(self):
769
        Store.of(self).remove(self)
1099.1.233 by Nick Chadwick
Exercise objects in the database module, along with their test cases,
770
    
1099.1.141 by Nick Chadwick
Updated the exercises to be loaded from the database, not a local file.
771
class TestCasePart(Storm):
772
    """A container for the test elements of a Test Case"""
1099.1.195 by William Grant
Rename problem to exercise in the DB.
773
    __storm_table__ = "test_case_part"
1099.1.141 by Nick Chadwick
Updated the exercises to be loaded from the database, not a local file.
774
    __storm_primary__ = "partid"
775
    
776
    partid = Int()
777
    testid = Int()
778
    
779
    part_type = Unicode()
780
    test_type = Unicode()
781
    data = Unicode()
782
    filename = Unicode()
783
    
784
    test = Reference(testid, "TestCase.testid")
785
    
1099.1.114 by Nick Chadwick
Modified the database so that exercises are now stored in the database, rather
786
    __init__ = _kwarg_init
1099.1.233 by Nick Chadwick
Exercise objects in the database module, along with their test cases,
787
    
1099.1.242 by Nick Chadwick
Fixed a problem with exercise editor, which wasn't editing or adding
788
    def delete(self):
789
        Store.of(self).remove(self)