~bjornt/lp2kanban/no-sync-lanes

« back to all changes in this revision

Viewing changes to src/lp2kanban/kanban.py

[r=benji] It's now possible to store JSON-encoded annotations in a card's description.

Show diffs side-by-side

added added

removed removed

Lines of Context:
5
5
import json
6
6
import operator
7
7
import re
 
8
from textwrap import dedent
8
9
import time
9
10
 
10
11
 
 
12
ANNOTATION_REGEX = re.compile('^\s*{.*}\s*$', re.MULTILINE|re.DOTALL)
 
13
 
 
14
 
11
15
class Record(dict):
12
16
    """A little dict subclass that adds attribute access to values."""
13
17
 
23
27
    def __setattr__(self, name, value):
24
28
        self[name] = value
25
29
 
 
30
 
26
31
class LeankitResponseCodes:
27
32
    """Enum listing all possible response codes from LeankitKanban API."""
28
33
    NoData = 100
187
192
        if '' in self.tags_list:
188
193
            self.tags_list.remove('')
189
194
        self.type = lane.board.cardtypes[self.type_id]
 
195
        self._description_annotations = None
190
196
 
191
197
    @property
192
198
    def is_new(self):
200
206
            self.tags = ', '.join(self.tags_list)
201
207
 
202
208
    def save(self):
 
209
        self._setDescriptionAnnotations(self.description_annotations)
203
210
        if not (self.is_dirty or self.is_new):
204
211
            # no-op.
205
212
            return
207
214
        data["UserWipOverrideComment"] =  None;
208
215
        if ("AssignedUsers" in data and
209
216
            "assigned_user_id" not in self.dirty_attrs):
210
 
            del data['AssignedUserId']
211
 
            del data['AssignedUserName']
 
217
            if 'AssignedUserId' in data.keys():
 
218
                del data['AssignedUserId']
 
219
            if 'AssignedUserName' in data.keys():
 
220
                del data['AssignedUserName']
212
221
            data['AssignedUserIds'] = map(
213
222
                lambda X: X['AssignedUserId'], data['AssignedUsers'])
214
223
 
293
302
        self.external_card_id = src.external_card_id
294
303
        self.assigned_user_id = src.assigned_user_id
295
304
 
 
305
 
 
306
    @property
 
307
    def parsed_description(self):
 
308
        """Parse the card description to find key=value pairs.
 
309
 
 
310
        :return: A tuple of (json_annotations, text_before_json,
 
311
                 text_after_json), where json_annotations contains the
 
312
                 JSON loaded with json.loads().
 
313
        """
 
314
        match = ANNOTATION_REGEX.search(self.description)
 
315
        if match:
 
316
            start = match.start()
 
317
            end = match.end()
 
318
            return (
 
319
                Record(json.loads(self.description[start:end])),
 
320
                self.description[:start].strip(),
 
321
                self.description[end:].strip())
 
322
        else:
 
323
            return Record(), self.description, ''
 
324
 
 
325
    def _setDescriptionAnnotations(self, new_annotations):
 
326
        """Update the card's description annotations.
 
327
 
 
328
        Note that this will overwrite all the annotations in the
 
329
        description.
 
330
 
 
331
        :param new_annotations: A dict of new annotations to store.
 
332
        """
 
333
        old_annotations, text_before_json, text_after_json = (
 
334
            self.parsed_description)
 
335
        annotation_text = json.dumps(new_annotations)
 
336
        new_description = dedent("""
 
337
            {text_before_json}
 
338
            {json_data}
 
339
            {text_after_json}
 
340
        """).format(
 
341
            text_before_json=text_before_json, json_data=annotation_text,
 
342
            text_after_json=text_after_json)
 
343
        self.description = new_description.strip()
 
344
 
 
345
    @property
 
346
    def description_annotations(self):
 
347
        if self._description_annotations is None:
 
348
            annotations, ignored_1, ignored_2 = self.parsed_description
 
349
            self._description_annotations = annotations
 
350
        return self._description_annotations
 
351
 
 
352
 
296
353
class LeankitLane(Converter):
297
354
    attributes = ['Id', 'Title', 'Index', 'Orientation', 'ParentLaneId']
298
355
    optional_attributes = ['Type']
400
457
 
401
458
    def getCardsWithExternalLinks(self):
402
459
        for card in self.cards:
403
 
            if card.external_system_url is not None:
 
460
            if (card.external_system_url is not None and
 
461
                card.external_system_url != ''):
404
462
                yield card
405
463
 
406
464
    def fetchDetails(self):
409
467
 
410
468
        self._populateUsers(self.details['BoardUsers'])
411
469
        self._populateCardTypes(self.details['CardTypes'])
 
470
        self._archive = self.connector.get(
 
471
            "/Board/" + str(self.id) + "/Archive").ReplyData[0]
 
472
        archive_lanes = [lane_dict['Lane'] for lane_dict in self._archive]
 
473
        archive_lanes.extend([lane_dict['Lane'] for lane_dict in self._archive[0]['ChildLanes']])
 
474
        self._backlog = self.connector.get(
 
475
            "/Board/" + str(self.id) + "/Backlog").ReplyData[0]
412
476
        self._populateLanes(
413
 
            self.details['Lanes'] + self.details['Archive'] +
414
 
            self.details['Backlog'])
 
477
            self.details['Lanes'] + archive_lanes + self._backlog)
415
478
 
416
479
    def _populateUsers(self, user_data):
417
480
        self.users = {}
577
640
            board.fetchDetails()
578
641
        return board
579
642
 
 
643
 
580
644
if __name__ == '__main__':
581
645
    kanban = LeankitKanban('launchpad.leankitkanban.com',
582
646
                           'user@email', 'password')