Merge lp://staging/~ted/lptools/review-changes into lp://staging/~dobey/lptools/trunk

Proposed by Ted Gould
Status: Merged
Merge reported by: dobey
Merged at revision: not available
Proposed branch: lp://staging/~ted/lptools/review-changes
Merge into: lp://staging/~dobey/lptools/trunk
Diff against target: None lines
To merge this branch: bzr merge lp://staging/~ted/lptools/review-changes
Reviewer Review Type Date Requested Status
Jelmer Vernooij (community) Approve
dobey Pending
Review via email: mp+11642@code.staging.launchpad.net

This proposal supersedes a proposal from 2009-08-05.

To post a comment you must log in.
Revision history for this message
Robert Collins (lifeless) wrote :

30 + self.config.read(self.filename)
31 + if not os.path.isdir(os.path.dirname(self.filename)):
32 + os.makedirs(os.path.dirname(self.filename))

perhaps the read should come after the mkdir?

The call to ./review-list looks buggy: the cwd could be anything; surely you want to run 'review-list', or alternatively $libdir/review-list, if review-list isn't meant to be user executable.

Also, tests woud be good ;)

Revision history for this message
Robert Collins (lifeless) wrote :

Rodney, ping? This would be really good to land.

I can imagine splitting out the Debian directory (in fact I think it would be appropriate to split it out), but the setup.py and so on are definitely appropriate to land.

Revision history for this message
Robert Collins (lifeless) wrote :

Bah, ignore the mention of Debian in that comment, branch confusion.

Revision history for this message
Jelmer Vernooij (jelmer) :
review: Approve
Revision history for this message
Jelmer Vernooij (jelmer) wrote :

Ted, can you perhaps resubmit this, but targetted for lp:lptools?

Revision history for this message
Jelmer Vernooij (jelmer) wrote :

Thanks, merged! Launchpad won't recognize this MP as being merged, as it was proposed against Dobey's branch rather than against lp:lptools.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'review-notifier'
2--- review-notifier 2009-08-05 18:18:15 +0000
3+++ review-notifier 2009-09-12 02:57:50 +0000
4@@ -25,22 +25,160 @@
5
6 import pynotify
7
8+from ConfigParser import ConfigParser
9 import os
10 import sys
11+import subprocess
12
13 from xdg.BaseDirectory import xdg_cache_home
14+from xdg.BaseDirectory import xdg_config_home
15
16 from launchpadlib.launchpad import Launchpad, EDGE_SERVICE_ROOT
17 from launchpadlib.credentials import Credentials
18
19+import indicate
20+from time import time
21+
22 ICON_NAME = "bzr-icon-64"
23
24+class Preferences(object):
25+
26+ def __init__(self):
27+ self.filename = os.path.join(xdg_config_home, "lptools",
28+ "lptools.conf")
29+ self.config = ConfigParser()
30+ self.config.read(self.filename)
31+ if not os.path.isdir(os.path.dirname(self.filename)):
32+ os.makedirs(os.path.dirname(self.filename))
33+
34+ if not self.config.has_section("lptools"):
35+ self.config.add_section("lptools")
36+
37+ if self.config.has_option("lptools", "projects"):
38+ self.projects = self.config.get("lptools",
39+ "projects").split(",")
40+ else:
41+ self.projects = []
42+
43+ if self.config.has_option("lptools", "server"):
44+ self.api_server = self.config.get("lptools", "server")
45+ else:
46+ self.api_server = EDGE_SERVICE_ROOT
47+
48+ # gtk.ListStore for the dialog
49+ self.store = None
50+ self.dialog = self.__build_dialog()
51+
52+ def __build_dialog(self):
53+ dialog = gtk.Dialog()
54+ dialog.set_title("Pending Reviews Preferences")
55+ dialog.set_destroy_with_parent(True)
56+ dialog.set_has_separator(False)
57+ dialog.set_default_size(240, 320)
58+
59+ area = dialog.get_content_area()
60+
61+ vbox = gtk.VBox(spacing=6)
62+ vbox.set_border_width(12)
63+ area.add(vbox)
64+ vbox.show()
65+
66+ label = gtk.Label("<b>%s</b>" % "_Projects")
67+ label.set_use_underline(True)
68+ label.set_use_markup(True)
69+ label.set_alignment(0.0, 0.5)
70+ vbox.pack_start(label, expand=False, fill=False)
71+ label.show()
72+
73+ hbox = gtk.HBox(spacing=12)
74+ vbox.pack_start(hbox, expand=True, fill=True)
75+ hbox.show()
76+
77+ misc = gtk.Label()
78+ hbox.pack_start(misc, expand=False, fill=False)
79+ misc.show()
80+
81+ scrollwin = gtk.ScrolledWindow()
82+ scrollwin.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
83+ hbox.pack_start(scrollwin, expand=True, fill=True)
84+ scrollwin.show()
85+
86+ self.store = gtk.ListStore(str)
87+
88+ view = gtk.TreeView(self.store)
89+ label.set_mnemonic_widget(view)
90+ view.set_headers_visible(False)
91+ scrollwin.add(view)
92+ view.show()
93+
94+ cell = gtk.CellRendererText()
95+ cell.set_property("editable", True)
96+ cell.connect("editing_started", self.__edit_started)
97+ cell.connect("edited", self.__edit_finished)
98+ col = gtk.TreeViewColumn("Project", cell, markup=0)
99+ view.append_column(col)
100+
101+ dialog.connect("close", self.__dialog_closed, 0)
102+ dialog.connect("response", self.__dialog_closed)
103+
104+ return dialog
105+
106+ def __edit_started(self, cell, editable, path):
107+ return
108+
109+ def __edit_finished(self, sell, path, text):
110+ if text == "Click here to add a project...":
111+ return
112+ treeiter = self.store.get_iter_from_string(path)
113+ label = "<i>%s</i>" % "Click here to add a project..."
114+ self.store.set(treeiter, 0, label)
115+ self.projects.append(text)
116+ self.store.append([text,])
117+
118+ def __dialog_closed(self, dialog, response):
119+ dialog.hide()
120+ if len(self.projects) > 0:
121+ self.config.set("lptools", "projects",
122+ ",".join(self.projects))
123+ with open(self.filename, "w+b") as f:
124+ self.config.write(f)
125+
126+ def show_dialog(self, parent):
127+ if not self.dialog.get_transient_for():
128+ self.dialog.set_transient_for(parent)
129+ self.store.clear()
130+ text = "<i>%s</i>" % "Click here to add a project..."
131+ self.store.append([text,])
132+ if len(self.projects) != 0:
133+ for project in self.projects:
134+ self.store.append([project,])
135+ self.dialog.run()
136+
137+def server_display (server):
138+ ret = subprocess.call(["./review-list"])
139+ if ret != 0:
140+ sys.stderr.write("Failed to run './review-list'\n")
141+
142+def indicator_display (indicator):
143+ name = indicator.get_property("name")
144+ url = "http://code.launchpad.net/" + name + "/+activereviews"
145+ ret = subprocess.call(["xdg-open", url])
146+ if ret != 0:
147+ sys.stderr.write("Failed to run 'xdg-open %s'\n" % url)
148+
149 class Main(object):
150
151 def __init__(self):
152 self.id = 0
153 self.cached_candidates = {}
154
155+ self.indicators = { }
156+ server = indicate.indicate_server_ref_default()
157+ server.set_type("message.instant")
158+ server.set_desktop_file(os.path.join(os.getcwd(), "review-tools.desktop"))
159+ server.connect("server-display", server_display)
160+ server.show()
161+
162 self.cachedir = os.path.join(xdg_cache_home, "review-notifier")
163 credsfile = os.path.join(self.cachedir, "credentials")
164
165@@ -61,103 +199,131 @@
166
167 self.me = self.launchpad.me
168
169- print "Allo, %s" % self.me.name
170-
171- self.projects = []
172-
173- for arg in sys.argv:
174- if not arg.endswith("review-notifier"):
175- self.projects.append(arg)
176+ self.project_idle_ids = {}
177+
178+ self.config = Preferences()
179+
180+ if len(self.config.projects) == 0:
181+ print "No Projects specified"
182+ sys.exit(1)
183+
184+ for project in self.config.projects:
185+ ind = indicate.Indicator()
186+ ind.set_property("name", project)
187+ ind.set_property("count", "%d" % 0)
188+ ind.connect("user-display", indicator_display)
189+ ind.hide()
190+ self.indicators[project] = ind
191
192 pynotify.init("Review Notifier")
193 self.timeout()
194
195 def timeout(self):
196- for project in self.projects:
197- lp_project = None
198- lp_focus = None
199- try:
200- lp_project = self.launchpad.projects[project]
201- focus = lp_project.development_focus.branch
202- except AttributeError:
203- print "Project %s has no development focus." % project
204- continue
205- except KeyError:
206- print "Project %s not found." % project
207- continue
208-
209- if not focus:
210- print "Project %s has no development focus." % project
211- continue
212-
213- trunk = focus
214-
215- if trunk.landing_candidates:
216- for c in trunk.landing_candidates:
217- c_name = c.source_branch.unique_name
218- status = None
219- try:
220- status = self.cached_candidates[c_name]
221- except KeyError:
222- status = None
223-
224- # If the proposal hasn't changed, get on with it
225- if status and status == c.queue_status:
226- continue
227-
228- self.cached_candidates[c_name] = c.queue_status
229-
230- n = pynotify.Notification("Review Notification")
231- updated = False
232-
233- # Source and target branch URIs
234- source = c.source_branch.display_name
235- target = c.target_branch.display_name
236-
237- if c.queue_status == "Needs review":
238- # First time we see the branch
239- n.update("Branch Proposal",
240- "%s has proposed merging %s into %s." % (
241- c.registrant.display_name, source, target),
242- ICON_NAME)
243- updated = True
244- elif c.queue_status == "Approved":
245- # Branch was approved
246- n.update("Branch Approval",
247- "%s was approved for merging into %s." % (
248- source, target),
249- ICON_NAME)
250- udpated = True
251- elif c.queue_status == "Rejected":
252- # Branch was rejected
253- n.update("Branch Rejected",
254- """The proposal to merge %s into %s has been rejected.""" % (
255- source, target),
256- ICON_NAME)
257- updated = True
258- elif c.queue_status == "Merged":
259- # Code has landed in the target branch
260- n.update("Branch Merged",
261- "%s has been merged into %s." % (source,
262- target),
263- ICON_NAME)
264- updated = True
265- else:
266- print "%s status is %s." % (source, c.queue_status)
267-
268- if updated:
269- n.set_urgency(pynotify.URGENCY_LOW)
270- n.show()
271- else:
272- n.close()
273+ for project in self.config.projects:
274+ self.project_idle_ids[project] = gobject.idle_add(self.project_idle, project)
275
276 return True
277
278+ def project_idle (self, project):
279+ lp_project = None
280+ lp_focus = None
281+ try:
282+ lp_project = self.launchpad.projects[project]
283+ focus = lp_project.development_focus.branch
284+ except AttributeError:
285+ print "Project %s has no development focus." % project
286+ return False
287+ except KeyError:
288+ print "Project %s not found." % project
289+ return False
290+
291+ if not focus:
292+ print "Project %s has no development focus." % project
293+ return False
294+
295+ trunk = focus
296+
297+ if trunk.landing_candidates:
298+ self.indicators[project].show()
299+ for c in trunk.landing_candidates:
300+ gobject.idle_add(self.landing_idle, project, c)
301+ else:
302+ self.indicators[project].hide()
303+
304+ return False
305+
306+ def landing_idle (self, project, c):
307+ c_name = c.source_branch.unique_name
308+ status = None
309+ try:
310+ status = self.cached_candidates[c_name]
311+ except KeyError:
312+ status = None
313+
314+ # If the proposal hasn't changed, get on with it
315+ if status and status == c.queue_status:
316+ return False
317+
318+ self.cached_candidates[c_name] = c.queue_status
319+
320+ n = pynotify.Notification("Review Notification")
321+ updated = False
322+
323+ # Source and target branch URIs
324+ source = c.source_branch.display_name
325+ target = c.target_branch.display_name
326+
327+ if c.queue_status == "Needs review":
328+ # First time we see the branch
329+ n.update("Branch Proposal",
330+ "%s has proposed merging %s into %s." % (
331+ c.registrant.display_name, source, target),
332+ ICON_NAME)
333+ updated = True
334+ elif c.queue_status == "Approved":
335+ # Branch was approved
336+ n.update("Branch Approval",
337+ "%s was approved for merging into %s." % (
338+ source, target),
339+ ICON_NAME)
340+ udpated = True
341+ elif c.queue_status == "Rejected":
342+ # Branch was rejected
343+ n.update("Branch Rejected",
344+ """The proposal to merge %s into %s has been rejected.""" % (
345+ source, target),
346+ ICON_NAME)
347+ updated = True
348+ elif c.queue_status == "Merged":
349+ # Code has landed in the target branch
350+ n.update("Branch Merged",
351+ "%s has been merged into %s." % (source,
352+ target),
353+ ICON_NAME)
354+ updated = True
355+ else:
356+ print "%s status is %s." % (source, c.queue_status)
357+
358+ if updated:
359+ n.set_urgency(pynotify.URGENCY_LOW)
360+ n.set_hint("x-canonical-append", "allow")
361+ n.show()
362+ self.indicators[project].set_property_time("time", time())
363+ else:
364+ n.close()
365+
366 def run(self):
367 self.id = gobject.timeout_add_seconds(5 * 60, self.timeout)
368 gtk.main()
369
370
371 if __name__ == "__main__":
372- foo = Main()
373- foo.run()
374+ gobject.threads_init()
375+ gtk.gdk.threads_init()
376+ try:
377+ foo = Main()
378+ gtk.gdk.threads_enter()
379+ foo.run()
380+ gtk.gdk.threads_leave()
381+ except KeyboardInterrupt:
382+ gtk.main_quit()
383
384=== added file 'review-tools.desktop'
385--- review-tools.desktop 1970-01-01 00:00:00 +0000
386+++ review-tools.desktop 2009-08-04 23:32:09 +0000
387@@ -0,0 +1,10 @@
388+[Desktop Entry]
389+Encoding=UTF-8
390+Name=Launchpad Reviews
391+GenericName=Code Review Notifier
392+Comment=Notify when new code reviews appear
393+Exec=review-list
394+StartupNotify=true
395+Terminal=false
396+Type=Application
397+Categories=Network;

Subscribers

People subscribed via source and target branches

to all changes: