--- lptools-0.0.1~bzr9.orig/lp-review-list.desktop +++ lptools-0.0.1~bzr9/lp-review-list.desktop @@ -0,0 +1,10 @@ +[Desktop Entry] +Encoding=UTF-8 +Name=Launchpad Reviews +GenericName=Code Review Listing Dialog +Comment=Show when new code reviews appear +Exec=review-list +StartupNotify=true +Terminal=false +Type=Application +Categories=Network; --- lptools-0.0.1~bzr9.orig/review-list +++ lptools-0.0.1~bzr9/review-list @@ -166,8 +166,8 @@ gtk.Window.__init__(self) self.set_title("Pending Reviews") self.set_default_size(320, 400) - self.connect("destroy", lambda w: gtk.main_quit()) - self.connect("delete_event", lambda w: gtk.main_quit()) + self.connect("destroy", lambda w : gtk.main_quit()) + self.connect("delete_event", lambda w, x: gtk.main_quit()) vbox = gtk.VBox() self.add(vbox) --- lptools-0.0.1~bzr9.orig/setup.py +++ lptools-0.0.1~bzr9/setup.py @@ -0,0 +1,9 @@ +#!/usr/bin/python + +from distutils.core import setup + +setup(name='lptools', + version='0.0', + data_files=[('/etc/xdg/autostart', ['lp-review-notifier.desktop']), + ('share/applications', ['lp-review-list.desktop'])], + scripts=['review-notifier', 'review-list', 'milestone2ical']) --- lptools-0.0.1~bzr9.orig/review-notifier +++ lptools-0.0.1~bzr9/review-notifier @@ -25,22 +25,161 @@ import pynotify +from ConfigParser import ConfigParser import os import sys +import subprocess from xdg.BaseDirectory import xdg_cache_home +from xdg.BaseDirectory import xdg_config_home from launchpadlib.launchpad import Launchpad, EDGE_SERVICE_ROOT from launchpadlib.credentials import Credentials +import httplib2 + +import indicate +import time ICON_NAME = "bzr-icon-64" +class Preferences(object): + + def __init__(self): + self.filename = os.path.join(xdg_config_home, "lptools", + "lptools.conf") + self.config = ConfigParser() + self.config.read(self.filename) + if not os.path.isdir(os.path.dirname(self.filename)): + os.makedirs(os.path.dirname(self.filename)) + + if not self.config.has_section("lptools"): + self.config.add_section("lptools") + + if self.config.has_option("lptools", "projects"): + self.projects = self.config.get("lptools", + "projects").split(",") + else: + self.projects = [] + + if self.config.has_option("lptools", "server"): + self.api_server = self.config.get("lptools", "server") + else: + self.api_server = EDGE_SERVICE_ROOT + + # gtk.ListStore for the dialog + self.store = None + self.dialog = self.__build_dialog() + + def __build_dialog(self): + dialog = gtk.Dialog() + dialog.set_title("Pending Reviews Preferences") + dialog.set_destroy_with_parent(True) + dialog.set_has_separator(False) + dialog.set_default_size(240, 320) + + area = dialog.get_content_area() + + vbox = gtk.VBox(spacing=6) + vbox.set_border_width(12) + area.add(vbox) + vbox.show() + + label = gtk.Label("%s" % "_Projects") + label.set_use_underline(True) + label.set_use_markup(True) + label.set_alignment(0.0, 0.5) + vbox.pack_start(label, expand=False, fill=False) + label.show() + + hbox = gtk.HBox(spacing=12) + vbox.pack_start(hbox, expand=True, fill=True) + hbox.show() + + misc = gtk.Label() + hbox.pack_start(misc, expand=False, fill=False) + misc.show() + + scrollwin = gtk.ScrolledWindow() + scrollwin.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC) + hbox.pack_start(scrollwin, expand=True, fill=True) + scrollwin.show() + + self.store = gtk.ListStore(str) + + view = gtk.TreeView(self.store) + label.set_mnemonic_widget(view) + view.set_headers_visible(False) + scrollwin.add(view) + view.show() + + cell = gtk.CellRendererText() + cell.set_property("editable", True) + cell.connect("editing_started", self.__edit_started) + cell.connect("edited", self.__edit_finished) + col = gtk.TreeViewColumn("Project", cell, markup=0) + view.append_column(col) + + dialog.connect("close", self.__dialog_closed, 0) + dialog.connect("response", self.__dialog_closed) + + return dialog + + def __edit_started(self, cell, editable, path): + return + + def __edit_finished(self, sell, path, text): + if text == "Click here to add a project...": + return + treeiter = self.store.get_iter_from_string(path) + label = "%s" % "Click here to add a project..." + self.store.set(treeiter, 0, label) + self.projects.append(text) + self.store.append([text,]) + + def __dialog_closed(self, dialog, response): + dialog.hide() + if len(self.projects) > 0: + self.config.set("lptools", "projects", + ",".join(self.projects)) + with open(self.filename, "w+b") as f: + self.config.write(f) + + def show_dialog(self, parent): + if not self.dialog.get_transient_for(): + self.dialog.set_transient_for(parent) + self.store.clear() + text = "%s" % "Click here to add a project..." + self.store.append([text,]) + if len(self.projects) != 0: + for project in self.projects: + self.store.append([project,]) + self.dialog.run() + +def server_display (server): + ret = subprocess.call(["review-list"]) + if ret != 0: + sys.stderr.write("Failed to run 'review-list'\n") + +def indicator_display (indicator): + name = indicator.get_property("name") + url = "http://code.launchpad.net/" + name + "/+activereviews" + ret = subprocess.call(["xdg-open", url]) + if ret != 0: + sys.stderr.write("Failed to run 'xdg-open %s'\n" % url) + class Main(object): def __init__(self): self.id = 0 self.cached_candidates = {} + self.indicators = { } + server = indicate.indicate_server_ref_default() + server.set_type("message.instant") + server.set_desktop_file(os.path.join('etc', 'xdg', 'autostart', 'lp-review-notifier.desktop')) + server.connect("server-display", server_display) + server.show() + self.cachedir = os.path.join(xdg_cache_home, "review-notifier") credsfile = os.path.join(self.cachedir, "credentials") @@ -51,7 +190,15 @@ creds = Credentials() with file(credsfile) as f: creds.load(f) - self.launchpad = Launchpad(creds, EDGE_SERVICE_ROOT) + sleeptime = 5 + self.launchpad = None + while not self.launchpad: + try: + self.launchpad = Launchpad(creds, EDGE_SERVICE_ROOT) + except httplib2.ServerNotFoundError: + print "Launchpad server not found. Waiting to try again." + time.sleep(sleeptime) + sleeptime *= 2 else: self.launchpad = Launchpad.get_token_and_login('review-notifier', EDGE_SERVICE_ROOT, @@ -61,103 +208,131 @@ self.me = self.launchpad.me - print "Allo, %s" % self.me.name + self.project_idle_ids = {} - self.projects = [] + self.config = Preferences() - for arg in sys.argv: - if not arg.endswith("review-notifier"): - self.projects.append(arg) + if len(self.config.projects) == 0: + print "No Projects specified" + sys.exit(1) + + for project in self.config.projects: + ind = indicate.Indicator() + ind.set_property("name", project) + ind.set_property("count", "%d" % 0) + ind.connect("user-display", indicator_display) + ind.hide() + self.indicators[project] = ind pynotify.init("Review Notifier") self.timeout() def timeout(self): - for project in self.projects: - lp_project = None - lp_focus = None - try: - lp_project = self.launchpad.projects[project] - focus = lp_project.development_focus.branch - except AttributeError: - print "Project %s has no development focus." % project - continue - except KeyError: - print "Project %s not found." % project - continue - - if not focus: - print "Project %s has no development focus." % project - continue - - trunk = focus - - if trunk.landing_candidates: - for c in trunk.landing_candidates: - c_name = c.source_branch.unique_name - status = None - try: - status = self.cached_candidates[c_name] - except KeyError: - status = None - - # If the proposal hasn't changed, get on with it - if status and status == c.queue_status: - continue - - self.cached_candidates[c_name] = c.queue_status - - n = pynotify.Notification("Review Notification") - updated = False - - # Source and target branch URIs - source = c.source_branch.display_name - target = c.target_branch.display_name - - if c.queue_status == "Needs review": - # First time we see the branch - n.update("Branch Proposal", - "%s has proposed merging %s into %s." % ( - c.registrant.display_name, source, target), - ICON_NAME) - updated = True - elif c.queue_status == "Approved": - # Branch was approved - n.update("Branch Approval", - "%s was approved for merging into %s." % ( - source, target), - ICON_NAME) - udpated = True - elif c.queue_status == "Rejected": - # Branch was rejected - n.update("Branch Rejected", - """The proposal to merge %s into %s has been rejected.""" % ( - source, target), - ICON_NAME) - updated = True - elif c.queue_status == "Merged": - # Code has landed in the target branch - n.update("Branch Merged", - "%s has been merged into %s." % (source, - target), - ICON_NAME) - updated = True - else: - print "%s status is %s." % (source, c.queue_status) - - if updated: - n.set_urgency(pynotify.URGENCY_LOW) - n.show() - else: - n.close() + for project in self.config.projects: + self.project_idle_ids[project] = gobject.idle_add(self.project_idle, project) return True + def project_idle (self, project): + lp_project = None + lp_focus = None + try: + lp_project = self.launchpad.projects[project] + focus = lp_project.development_focus.branch + except AttributeError: + print "Project %s has no development focus." % project + return False + except KeyError: + print "Project %s not found." % project + return False + + if not focus: + print "Project %s has no development focus." % project + return False + + trunk = focus + + if trunk.landing_candidates: + self.indicators[project].show() + for c in trunk.landing_candidates: + gobject.idle_add(self.landing_idle, project, c) + else: + self.indicators[project].hide() + + return False + + def landing_idle (self, project, c): + c_name = c.source_branch.unique_name + status = None + try: + status = self.cached_candidates[c_name] + except KeyError: + status = None + + # If the proposal hasn't changed, get on with it + if status and status == c.queue_status: + return False + + self.cached_candidates[c_name] = c.queue_status + + n = pynotify.Notification("Review Notification") + updated = False + + # Source and target branch URIs + source = c.source_branch.display_name + target = c.target_branch.display_name + + if c.queue_status == "Needs review": + # First time we see the branch + n.update("Branch Proposal", + "%s has proposed merging %s into %s." % ( + c.registrant.display_name, source, target), + ICON_NAME) + updated = True + elif c.queue_status == "Approved": + # Branch was approved + n.update("Branch Approval", + "%s was approved for merging into %s." % ( + source, target), + ICON_NAME) + udpated = True + elif c.queue_status == "Rejected": + # Branch was rejected + n.update("Branch Rejected", + """The proposal to merge %s into %s has been rejected.""" % ( + source, target), + ICON_NAME) + updated = True + elif c.queue_status == "Merged": + # Code has landed in the target branch + n.update("Branch Merged", + "%s has been merged into %s." % (source, + target), + ICON_NAME) + updated = True + else: + print "%s status is %s." % (source, c.queue_status) + + if updated: + n.set_urgency(pynotify.URGENCY_LOW) + n.set_hint("x-canonical-append", "allow") + n.show() + self.indicators[project].set_property_time("time", time.time()) + else: + n.close() + def run(self): self.id = gobject.timeout_add_seconds(5 * 60, self.timeout) gtk.main() if __name__ == "__main__": - foo = Main() - foo.run() + gobject.threads_init() + gtk.gdk.threads_init() + try: + foo = Main() + gtk.gdk.threads_enter() + foo.run() + gtk.gdk.threads_leave() + except KeyboardInterrupt: + gtk.main_quit() --- lptools-0.0.1~bzr9.orig/lp-review-notifier.desktop +++ lptools-0.0.1~bzr9/lp-review-notifier.desktop @@ -0,0 +1,10 @@ +[Desktop Entry] +Encoding=UTF-8 +Name=Launchpad Reviews Notifier +GenericName=Code Review Notifier +Comment=Notify when new code reviews appear +Exec=review-notifier +StartupNotify=true +Terminal=false +Type=Application +Categories=Network; --- lptools-0.0.1~bzr9.orig/debian/rules +++ lptools-0.0.1~bzr9/debian/rules @@ -0,0 +1,6 @@ +#!/usr/bin/make -f + +DEB_PYTHON_SYSTEM := pycentral + +include /usr/share/cdbs/1/rules/debhelper.mk +include /usr/share/cdbs/1/class/python-distutils.mk --- lptools-0.0.1~bzr9.orig/debian/changelog +++ lptools-0.0.1~bzr9/debian/changelog @@ -0,0 +1,22 @@ +lptools (0.0.1~bzr9-1) unstable; urgency=low + + * New upstream snapshot. + * Fix dependencies. + * Inclusion in Debian (Closes: #557731: ITP: lptools -- desktop tools for + Launchpad) + + -- Robert Collins Tue, 24 Nov 2009 11:37:05 +1100 + +lptools (0.0.0-0ubuntu1~ppa2) karmic; urgency=low + + * Add required packaging file copyright. + * Bump compat to 7. + * Upgrade standards version. + + -- Robert Collins Wed, 04 Nov 2009 16:55:02 +1100 + +lptools (0.0.0-0ubuntu1~ppa1) karmic; urgency=low + + * Initial release. + + -- Ted Gould Fri, 11 Sep 2009 22:55:05 -0500 --- lptools-0.0.1~bzr9.orig/debian/pycompat +++ lptools-0.0.1~bzr9/debian/pycompat @@ -0,0 +1 @@ +2 --- lptools-0.0.1~bzr9.orig/debian/compat +++ lptools-0.0.1~bzr9/debian/compat @@ -0,0 +1 @@ +7 --- lptools-0.0.1~bzr9.orig/debian/copyright +++ lptools-0.0.1~bzr9/debian/copyright @@ -0,0 +1,33 @@ +This package was debianized by Ted Gould Fri, 11 sep 2009 + +The source code is from http://launchpad.net/lptools. + +Upstream Author: + + Rodney Dawes + Ted Gould + +URL: + http://launchpad.net/lptools + + +Copyright: + + Copyright (C) 2009 Canonical Limited + +License: + + This program is free software: you can redistribute it and/or modify it + under the terms of the GNU General Public License version 3, as published + by the Free Software Foundation. + + This program is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranties of + MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR + PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program. If not, see . + +On Debian/Ubuntu systems, the complete text of this license can be found in +/usr/share/common-licenses/GPL-3. --- lptools-0.0.1~bzr9.orig/debian/control +++ lptools-0.0.1~bzr9/debian/control @@ -0,0 +1,26 @@ +Source: lptools +Section: python +Priority: extra +Build-Depends: cdbs, + debhelper (>= 7), + python, + python-central +Maintainer: Robert Collins +Standards-Version: 3.8.3 +XS-Python-Version: current + +Package: lptools +Architecture: all +XB-Python-Version: ${python:Versions} +Depends: ${misc:Depends}, + ${misc:Depends}, + ${python:Depends}, + python-gtk2, + python-launchpadlib +Description: Tools for working with Launchpad + LP Tools allow you to work with Launchpad without ever having to deal + with the web interface. The review-list tool can list reviews, and + review-notifier provides a desktop notifier about reviews that can be done. + . + milestone2ical converts milestones on a project or project group into the + iCal format.