~launchpad-pqm/launchpad/devel

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
# Copyright 2009 Canonical Ltd.  This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).

# pylint: disable-msg=E0611,W0212

"""Database classes related to and including CodeImportEvent."""

__metaclass__ = type
__all__ = [
    'CodeImportEvent',
    'CodeImportEventSet',
    'CodeImportEventToken',
    ]


from lazr.enum import DBItem
from sqlobject import (
    ForeignKey,
    StringCol,
    )
from zope.interface import implements

from lp.code.enums import (
    CodeImportEventDataType,
    CodeImportEventType,
    CodeImportMachineOfflineReason,
    RevisionControlSystems,
    )
from lp.code.interfaces.codeimportevent import (
    ICodeImportEvent,
    ICodeImportEventSet,
    ICodeImportEventToken,
    )
from lp.registry.interfaces.person import validate_public_person
from lp.services.database.constants import (
    DEFAULT,
    UTC_NOW,
    )
from lp.services.database.datetimecol import UtcDateTimeCol
from lp.services.database.enumcol import EnumCol
from lp.services.database.sqlbase import SQLBase


class CodeImportEvent(SQLBase):
    """See `ICodeImportEvent`."""

    implements(ICodeImportEvent)
    _table = 'CodeImportEvent'

    date_created = UtcDateTimeCol(notNull=True, default=DEFAULT)

    event_type = EnumCol(
        dbName='entry_type', enum=CodeImportEventType, notNull=True)
    code_import = ForeignKey(
        dbName='code_import', foreignKey='CodeImport', default=None)
    person = ForeignKey(
        dbName='person', foreignKey='Person',
        storm_validator=validate_public_person, default=None)
    machine = ForeignKey(
        dbName='machine', foreignKey='CodeImportMachine', default=None)

    def items(self):
        """See `ICodeImportEvent`."""
        return [(data.data_type, data.data_value)
                for data in _CodeImportEventData.selectBy(event=self)]


class _CodeImportEventData(SQLBase):
    """Additional data associated to a CodeImportEvent.

    This class is for internal use only. This data should be created by
    CodeImportEventSet event creation methods, and should be accessed by
    CodeImport methods.
    """

    _table = 'CodeImportEventData'

    event = ForeignKey(dbName='event', foreignKey='CodeImportEvent')
    data_type = EnumCol(enum=CodeImportEventDataType, notNull=True)
    data_value = StringCol()


class CodeImportEventSet:
    """See `ICodeImportEventSet`."""

    implements(ICodeImportEventSet)

    def getAll(self):
        """See `ICodeImportEventSet`."""
        return CodeImportEvent.select(orderBy=['date_created', 'id'])

    def getEventsForCodeImport(self, code_import):
        """See `ICodeImportEventSet`."""
        return CodeImportEvent.selectBy(code_import=code_import).orderBy(
            ['date_created', 'id'])

    # All CodeImportEvent creation methods should assert arguments against
    # None. The database schema and the interface allow all foreign keys to be
    # NULL, but specific event types should be created with specific non-NULL
    # values. We want to fail when the client code is buggy and passes None
    # where a real object is expected.

    def newCreate(self, code_import, person):
        """See `ICodeImportEventSet`."""
        assert code_import is not None, "code_import must not be None"
        assert person is not None, "person must not be None"
        event = CodeImportEvent(
            event_type=CodeImportEventType.CREATE,
            code_import=code_import, person=person)
        self._recordSnapshot(event, code_import)
        return event

    def beginModify(self, code_import):
        """See `ICodeImportEventSet`."""
        assert code_import is not None, "code_import must not be None"
        items = list(self._iterItemsForSnapshot(code_import))
        return CodeImportEventToken(items)

    def newModify(self, code_import, person, token):
        """See `ICodeImportEventSet`."""
        assert code_import is not None, "code_import must not be None"
        assert token is not None, "token must not be None"
        items = self._findModifications(code_import, token)
        if items is None:
            return None
        event = CodeImportEvent(
            event_type=CodeImportEventType.MODIFY,
            code_import=code_import, person=person)
        self._recordItems(event, items)
        return event

    def newRequest(self, code_import, person):
        """See `ICodeImportEventSet`."""
        assert code_import is not None, "code_import must not be None"
        assert person is not None, "person must not be None"
        event = CodeImportEvent(
            event_type=CodeImportEventType.REQUEST,
            code_import=code_import, person=person)
        self._recordCodeImport(event, code_import)
        return event

    def _recordMessage(self, event, message):
        """Record a message if there is a message set."""
        if message:
            _CodeImportEventData(
                event=event,
                data_type=CodeImportEventDataType.MESSAGE,
                data_value=message)

    def newOnline(self, machine, user=None, message=None, _date_created=None):
        """See `ICodeImportEventSet`."""
        assert machine is not None, "machine must not be None"
        if _date_created is None:
            _date_created = UTC_NOW
        event = CodeImportEvent(
            event_type=CodeImportEventType.ONLINE,
            machine=machine, person=user, date_created=_date_created)
        self._recordMessage(event, message)
        return event

    def newOffline(self, machine, reason, user=None, message=None):
        """See `ICodeImportEventSet`."""
        assert machine is not None, "machine must not be None"
        assert (type(reason) == DBItem
                and reason.enum == CodeImportMachineOfflineReason), (
            "reason must be a CodeImportMachineOfflineReason value, "
            "but was: %r" % (reason,))
        event = CodeImportEvent(
            event_type=CodeImportEventType.OFFLINE,
            machine=machine, person=user)
        _CodeImportEventData(
            event=event, data_type=CodeImportEventDataType.OFFLINE_REASON,
            data_value=reason.name)
        self._recordMessage(event, message)
        return event

    def newQuiesce(self, machine, user, message=None):
        """See `ICodeImportEventSet`."""
        assert machine is not None, "machine must not be None"
        assert user is not None, "user must not be None"
        event = CodeImportEvent(
            event_type=CodeImportEventType.QUIESCE,
            machine=machine, person=user)
        self._recordMessage(event, message)
        return event

    def newStart(self, code_import, machine):
        """See `ICodeImportEventSet`."""
        assert code_import is not None, "code_import must not be None"
        assert machine is not None, "machine must not be None"
        return CodeImportEvent(
            event_type=CodeImportEventType.START,
            code_import=code_import, machine=machine)

    def newFinish(self, code_import, machine):
        """See `ICodeImportEventSet`."""
        assert code_import is not None, "code_import must not be None"
        assert machine is not None, "machine must not be None"
        return CodeImportEvent(
            event_type=CodeImportEventType.FINISH,
            code_import=code_import, machine=machine)

    def newKill(self, code_import, machine):
        """See `ICodeImportEventSet`."""
        assert code_import is not None, "code_import must not be None"
        assert machine is not None, "machine must not be None"
        return CodeImportEvent(
            event_type=CodeImportEventType.KILL,
            code_import=code_import, machine=machine)

    def newReclaim(self, code_import, machine, job_id):
        """See `ICodeImportEventSet`."""
        assert code_import is not None, "code_import must not be None"
        assert machine is not None, "machine must not be None"
        assert isinstance(job_id, int), (
            "job_id must be an int, was: %r" % job_id)
        event = CodeImportEvent(
            event_type=CodeImportEventType.RECLAIM,
            code_import=code_import, machine=machine)
        _CodeImportEventData(
            event=event, data_type=CodeImportEventDataType.RECLAIMED_JOB_ID,
            data_value=str(job_id))
        return event

    def _recordSnapshot(self, event, code_import):
        """Record a snapshot of the code import in the event data."""
        self._recordItems(event, self._iterItemsForSnapshot(code_import))

    def _recordCodeImport(self, event, code_import):
        """Record the code import id in the event data."""
        self._recordItems(event, [self._getCodeImportItem(code_import)])

    def _recordItems(self, event, items):
        """Record the specified event data into the database."""
        for key, value in items:
            data_type = getattr(CodeImportEventDataType, key)
            _CodeImportEventData(
                event=event, data_type=data_type, data_value=value)

    def _iterItemsForSnapshot(self, code_import):
        """Yield key-value tuples to save a snapshot of the code import."""
        yield self._getCodeImportItem(code_import)
        yield 'REVIEW_STATUS', code_import.review_status.name
        yield 'OWNER', str(code_import.owner.id)
        yield 'UPDATE_INTERVAL', self._getNullableValue(
            code_import.update_interval)
        yield 'ASSIGNEE', self._getNullableValue(
            code_import.assignee, use_id=True)
        for detail in self._iterSourceDetails(code_import):
            yield detail

    def _getCodeImportItem(self, code_import):
        """Return the key-value tuple for the code import id."""
        return 'CODE_IMPORT', str(code_import.id)

    def _getNullableValue(self, value, use_id=False):
        """Return the string value for a nullable value.

        :param value: The value to represent as a string.
        :param use_id: Return the id of the object instead of the object, such
            as for a foreign key.
        """
        if value is None:
            return None
        elif use_id:
            return str(value.id)
        else:
            return str(value)

    def _iterSourceDetails(self, code_import):
        """Yield key-value tuples describing the source of the import."""
        if code_import.rcs_type in (RevisionControlSystems.SVN,
                                    RevisionControlSystems.BZR_SVN,
                                    RevisionControlSystems.GIT,
                                    RevisionControlSystems.HG,
                                    RevisionControlSystems.BZR):
            yield 'URL', code_import.url
        elif code_import.rcs_type == RevisionControlSystems.CVS:
            yield 'CVS_ROOT', code_import.cvs_root
            yield 'CVS_MODULE', code_import.cvs_module
        else:
            raise AssertionError(
                "Unknown RCS type: %s" % (code_import.rcs_type,))

    def _findModifications(self, code_import, token):
        """Find modifications made to the code import.

        If no change was found, return None. Otherwise return a list of items
        that describe the old and new state of the modified code import.

        :param code_import: CodeImport object that was presumably modified.

        :param token: Token returned by a call to _makeModificationToken
            before the code import was modified.
        :return: Set of items that can be passed to _recordItems, or None.
        """
        old_dict = dict(token.items)
        new_dict = dict(self._iterItemsForSnapshot(code_import))

        assert old_dict['CODE_IMPORT'] == new_dict['CODE_IMPORT'], (
            "Token was produced from a different CodeImport object: "
            "id in token = %s, id of code_import = %s"
            % (old_dict['CODE_IMPORT'], new_dict['CODE_IMPORT']))

        # The set of keys are not identical if the rcstype changed.
        all_keys = set(old_dict.keys()).union(set(new_dict.keys()))

        items = set()
        has_changes = False
        for key in all_keys:
            old_value = old_dict.get(key)
            new_value = new_dict.get(key)

            # Record current value for this key.
            items.add((key, new_value))

            if old_value != new_value:
                # Value has changed. Record previous value as well as current.
                has_changes = True
                items.add(('OLD_' + key, old_value))

        if has_changes:
            return items
        else:
            return None


class CodeImportEventToken:
    """See `ICodeImportEventToken`."""

    implements(ICodeImportEventToken)

    def __init__(self, items):
        self.items = items