Merge lp:~canonical-hwe-team/lp2kanban/hwe into lp:~launchpad/lp2kanban/trunk

Proposed by Chris Van Hoof
Status: Merged
Merged at revision: 51
Proposed branch: lp:~canonical-hwe-team/lp2kanban/hwe
Merge into: lp:~launchpad/lp2kanban/trunk
Diff against target: 273 lines (+157/-9)
3 files modified
example_addcard_method.py (+29/-0)
example_config.ini (+1/-1)
src/lp2kanban/kanban.py (+127/-8)
To merge this branch: bzr merge lp:~canonical-hwe-team/lp2kanban/hwe
Reviewer Review Type Date Requested Status
Brad Crittenden (community) Approve
Review via email: mp+103398@code.launchpad.net

Commit message

[ thanks to ~jk-ozlabs, ~anthonywong, and ~vanhoof ]

Description of the change

This merge request adds a new AddCard method (thanks jk!), which allows you to automagically create cards based in input. I have also included a simple example of how to make this work for reference, although I have not had a chance to put together testcase for this yet.

We've been able to add 100+ cards on the fly while we're working out the details of how we feel about the board layout once populated.

To post a comment you must log in.
Revision history for this message
Brad Crittenden (bac) wrote :

Hi Chris,

Thanks for picking up lp2kanban and adapting it for your needs. We've gotten a lot of use out of the tool and are glad to see others using it too.

Here are some comments on the code:

* The copy method added to LeankitCard is not used anywhere. If it isn't needed we should remove it. Let me know if you have a reason for keeping it.

* Yes, tests would be nice!

Overall it looks very good. I'll be glad to land the branch for you when I hear back.

review: Approve
Revision history for this message
Anthony Wong (anthonywong) wrote :

Hi Brad,

The copy method was added for card duplication. We use it in our team's script to duplicate cards in some scenarios. What we are doing is to create new cards that are based on an existing one, then modify some fields and save them. It's useful to us due to the lack of such a feature in LKK UI.

Thanks,
Anthony

Revision history for this message
Chris Van Hoof (vanhoof) wrote :

Thanks Anthony!

Brad if you could like an example of the card copy method, we could put one together similar to the add card example as well.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added file 'example_addcard_method.py'
2--- example_addcard_method.py 1970-01-01 00:00:00 +0000
3+++ example_addcard_method.py 2012-04-25 00:13:29 +0000
4@@ -0,0 +1,29 @@
5+# the directory containing the kanban.py file needs to be in your
6+# PYTHONPATH
7+import kanban
8+import sys
9+import os
10+import json
11+
12+# these are the IDs of the lane and board that you want to
13+# automagically add cards to
14+BOARD_ID = 12345678
15+LANE_ID = 23456789
16+
17+def main(args):
18+ kb = kanban.LeankitKanban('canonical',
19+ 'user@canonical.com',
20+ 'password')
21+
22+ board = kb.getBoard(board_id = BOARD_ID)
23+ lane = board.getLane(LANE_ID)
24+ #print lane.cards
25+ card = lane.addCard()
26+ card.title = 'automagic new card!'
27+ card.size = '99'
28+ card.external_card_id = '123456'
29+ card.is_blocked = 'false'
30+ card.save()
31+
32+if __name__ == '__main__':
33+ sys.exit(main(sys.argv))
34
35=== modified file 'example_config.ini'
36--- example_config.ini 2011-08-08 10:08:02 +0000
37+++ example_config.ini 2012-04-25 00:13:29 +0000
38@@ -1,6 +1,6 @@
39 [GLOBAL]
40 # This is the account part of https://<account>.leankitkanban.com/ URL.
41-account = launchpad
42+account = canonical
43 # LeankitKanban user name and password.
44 user = user@canonical.com
45 password = password
46
47=== modified file 'src/lp2kanban/kanban.py'
48--- src/lp2kanban/kanban.py 2012-01-25 15:29:10 +0000
49+++ src/lp2kanban/kanban.py 2012-04-25 00:13:29 +0000
50@@ -163,12 +163,16 @@
51 class LeankitUser(Converter):
52 attributes = ['UserName', 'FullName', 'EmailAddress', 'Id']
53
54+class LeankitCardType(Converter):
55+ attributes = ['Name', 'IsDefault', 'ColorHex', 'IconPath', 'Id']
56
57 class LeankitCard(Converter):
58 attributes = ['Id', 'Title', 'Priority', 'Description', 'Tags',
59- 'TypeId', 'AssignedUserId']
60+ 'TypeId']
61
62- optional_attributes = ['ExternalCardID']
63+ optional_attributes = ['ExternalCardID', 'AssignedUserId', 'Size', 'IsBlocked',
64+ 'BlockReason', 'ExternalSystemName', 'ExternalSystemUrl',
65+ 'ClassOfServiceId', 'DueDate']
66
67 def __init__(self, card_dict, lane):
68 super(LeankitCard, self).__init__(card_dict)
69@@ -177,6 +181,11 @@
70 self.tags_list = set([tag.strip() for tag in self.tags.split(',')])
71 if '' in self.tags_list:
72 self.tags_list.remove('')
73+ self.type = lane.board.cardtypes[self.type_id]
74+
75+ @property
76+ def is_new(self):
77+ return self.id is None
78
79 def addTag(self, tag):
80 tag = tag.strip()
81@@ -186,7 +195,7 @@
82 self.tags = ', '.join(self.tags_list)
83
84 def save(self):
85- if not self.is_dirty:
86+ if not (self.is_dirty or self.is_new):
87 # no-op.
88 return
89 data = self._raw_data
90@@ -199,15 +208,34 @@
91 for attr in self.dirty_attrs:
92 #print "Storing %s in %s..." % (attr, self._toCamelCase(attr))
93 data[self._toCamelCase(attr)] = getattr(self, attr)
94- result = self.lane.board.connector.post(
95- '/Board/' + str(self.lane.board.id) + '/UpdateCard', data=data)
96+
97+ if self.is_new:
98+ del data['Id']
99+ del data['LaneId']
100+ position = len(self.lane.cards)
101+ url_parts = ['/Board', str(self.lane.board.id), 'AddCard',
102+ 'Lane', str(self.lane.id), 'Position', str(position)]
103+ else:
104+ url_parts = ['/Board', str(self.lane.board.id), 'UpdateCard']
105+
106+ url = '/'.join(url_parts)
107+
108+ result = self.lane.board.connector.post(url, data=data)
109+
110+ if (self.is_new and
111+ result.ReplyCode in LeankitResponseCodes.SUCCESS_CODES):
112+ self.id = result.ReplyData[0]['CardId']
113+
114 return result.ReplyData[0]
115
116 def move(self, target_lane):
117 if target_lane is not None and target_lane != self.lane:
118 self.lane = target_lane
119 self._raw_data['LaneId'] = self.lane.id
120- return self._moveCard()
121+ if not self.is_new:
122+ return self._moveCard()
123+ else:
124+ return self.save()
125 else:
126 return None
127
128@@ -225,6 +253,39 @@
129 self.title, self.id, target_lane.path) +
130 "Error %s: %s" % (result.ReplyCode, result.ReplyText))
131
132+ @classmethod
133+ def create(cls, lane):
134+ default_card_data = {
135+ 'Id': None,
136+ 'Title': '',
137+ 'Priority': 1,
138+ 'Description': '',
139+ 'Tags': '',
140+ 'TypeId': lane.board.default_cardtype.id,
141+ 'LaneId': lane.id,
142+ 'IsBlocked': "false",
143+ 'BlockReason': None,
144+ 'ExternalCardID': None,
145+ 'ExternalSystemName': None,
146+ 'ExternalSystemUrl': None,
147+ 'ClassOfServiceId': None,
148+ }
149+ card = cls(default_card_data, lane)
150+ return card
151+
152+ def copy(self, src):
153+ self.title = src.title
154+ self.priority = src.priority
155+ self.description = src.description
156+ self.tags = src.tags
157+ self.type_id = src.type_id
158+ self.lane = src.lane
159+ self.is_blocked = src.is_blocked
160+ self.size = src.size
161+ self.block_reason = src.block_reason
162+ self.due_date = src.due_date
163+ self.external_card_id = src.external_card_id
164+ self.assigned_user_id = src.assigned_user_id
165
166 class LeankitLane(Converter):
167 attributes = ['Id', 'Title', 'Index', 'Orientation', 'ParentLaneId']
168@@ -233,10 +294,10 @@
169 def __init__(self, lane_dict, board):
170 super(LeankitLane, self).__init__(lane_dict)
171 self.parent_lane = None
172+ self.board = board
173+ self.child_lanes = []
174 self.cards = [LeankitCard(card_data, self)
175 for card_data in lane_dict['Cards']]
176- self.board = board
177- self.child_lanes = []
178
179 @property
180 def path(self):
181@@ -297,6 +358,10 @@
182 return self._getNextLanes(self.parent_lane, self.index,
183 self.orientation)
184
185+ def addCard(self):
186+ card = LeankitCard.create(self)
187+ self.cards.append(card)
188+ return card
189
190 class LeankitBoard(Converter):
191
192@@ -318,6 +383,7 @@
193 }, self)
194 self.lanes = { 0: self.root_lane }
195 self.cards = []
196+ self.default_cardtype = None
197
198 def getCardsWithExternalIds(self):
199 for card in self.cards:
200@@ -329,6 +395,7 @@
201 self.base_uri + str(self.id)).ReplyData[0]
202
203 self._populateUsers(self.details['BoardUsers'])
204+ self._populateCardTypes(self.details['CardTypes'])
205 self._populateLanes(self.details['Lanes'])
206
207 def _populateUsers(self, user_data):
208@@ -354,6 +421,16 @@
209 self.cards.extend(lane.cards)
210 self._sortLanes()
211
212+ def _populateCardTypes(self, cardtypes_data):
213+ self.cardtypes = {}
214+ for cardtype_dict in cardtypes_data:
215+ cardtype = LeankitCardType(cardtype_dict)
216+ self.cardtypes[cardtype.id] = cardtype
217+ if cardtype.is_default:
218+ self.default_cardtype = cardtype
219+
220+ assert self.default_cardtype is not None
221+
222 def _sortLanes(self, lane=None):
223 """Sorts the root lanes and lists of child lanes by their index."""
224 if lane is None:
225@@ -363,6 +440,48 @@
226 for lane in lanes:
227 self._sortLanes(lane)
228
229+ def getLane(self, lane_id):
230+ flat_lanes = {}
231+ def flatten_lane(lane):
232+ flat_lanes[lane.id] = lane
233+ for child in lane.child_lanes:
234+ flatten_lane(child)
235+ map(flatten_lane, self.root_lane.child_lanes)
236+ return flat_lanes[lane_id];
237+
238+ def getLaneByTitle(self, title):
239+ if len(self.root_lane.child_lanes) > 0:
240+ return self._getLaneByTitle(self.root_lane, title)
241+
242+ def _getLaneByTitle(self, lane, title):
243+ if (lane.title == title):
244+ return lane
245+ else:
246+ for child in lane.child_lanes:
247+ result = self._getLaneByTitle(child, title)
248+ if result != None:
249+ return result
250+ return None
251+
252+ def getLaneByPath(self, path, ignorecase=False):
253+ if len(self.root_lane.child_lanes) > 0:
254+ return self._getLaneByPath(self.root_lane, path, ignorecase)
255+
256+ def _getLaneByPath(self, lane, path, ignorecase):
257+ if ignorecase == True:
258+ if lane.path.lower() == path.lower():
259+ return lane
260+ else:
261+ if lane.path == path:
262+ return lane
263+
264+ for child in lane.child_lanes:
265+ result = self._getLaneByPath(child, path, ignorecase)
266+ if result != None:
267+ return result
268+ return None
269+
270+
271 def _printLanes(self, lane, indent, include_cards=False):
272 next_lane = lane.getNextLanes()
273 if next_lane is None:

Subscribers

People subscribed via source and target branches