Merge lp:~zedtux/bzr-gtk/gravatar-urllib2 into lp:bzr-gtk/gtk2

Proposed by Jelmer Vernooij
Status: Merged
Merged at revision: 723
Proposed branch: lp:~zedtux/bzr-gtk/gravatar-urllib2
Merge into: lp:bzr-gtk/gtk2
Diff against target: 428 lines (+375/-2)
3 files modified
avatarproviders.py (+110/-0)
avatarsbox.py (+249/-0)
revisionview.py (+16/-2)
To merge this branch: bzr merge lp:~zedtux/bzr-gtk/gravatar-urllib2
Reviewer Review Type Date Requested Status
Jelmer Vernooij Pending
Review via email: mp+53098@code.launchpad.net

Description of the change

Proposing zedtux's gravatar branch.

To post a comment you must log in.
Revision history for this message
Jelmer Vernooij (jelmer) wrote :
Download full text (7.7 KiB)

Hi Guillaume,

I think this is ready to land, though I have a few more minor remarks.
Please let me know if you're happy for me to just fix them before
landing or if you'd like fix them yourself.

> differences between files attachment (review-diff.txt)
> === added file 'avatar.py'
> --- avatar.py 1970-01-01 00:00:00 +0000
> +++ avatar.py 2011-03-11 21:59:14 +0000
> + # ~~~~~ Properties ~~~~~
> + # username
> + def get_username(self):
> + return self.__username
> + def set_username(self, username):
> + self.__username = username
> + username = property(get_username, set_username)
> +
> + # Name
> + def get_name(self):
> + return self.__name
> + def set_name(self, name):
> + self.__name = name
> + name = property(get_name, set_name)
> +
> + # Email
> + def get_email(self):
> + return self.__email
> + def set_email(self, email):
> + self.__email = email
> + email = property(get_email, set_email)
> +
> + # Image
> + def get_image(self):
> + return self.__image
> + def set_image(self, image):
> + self.__image = image
> + image = property(get_image, set_image)
Please avoid boilerplate code like this. Just let external users
set .image, .email, etc directory. They can always be changed to be
properties later, if necessary.

> + def is_identical(self, avatar):
> + """
> + Return True if attributes of the given avatar
> + match to current object attributes otherwise return False
> + """
> + return self.username == avatar.username and \
> + self.name == avatar.name and \
> + self.email == avatar.email
It might make sense to rename this to __eq__, so that you can just use
"==" to compare avatars.

> === added file 'avatarbox.py'
> --- avatarbox.py 1970-01-01 00:00:00 +0000
> +++ avatarbox.py 2011-03-11 21:59:14 +0000

> + # ~~~~~ Properties ~~~~~
> + # Avatar
> + def get_avatar(self):
> + return self.__current_avatar
> + def set_avatar(self, avatar):
> + self.__current_avatar = avatar
> + avatar = property(get_avatar, set_avatar)
^ See above.

> + # ~~~~~ Public methods ~~~~~

> + def and_avatar_email(self, email):
> + """
> + Select the avatar from avatars collection
> + in order to apply future actions
> + """
> + self.avatar = None
> + if not email is "":
> + self.avatar = self.__avatars[email] if email in self.__avatars else None
> + return self
Please avoid "if ... else" in one line as it doesn't work in Python 2.4,
which we bzr-gtk still supports.

> === added file 'avatardownloaderworker.py'
> --- avatardownloaderworker.py 1970-01-01 00:00:00 +0000
> +++ avatardownloaderworker.py 2011-03-11 21:59:14 +0000
> @@ -0,0 +1,72 @@
> +# Copyright (C) 2011 by Guillaume Hain (zedtux) <email address hidden>
> +#
> +# This program is free software; you can redistribute it and/or modify
> +# it under the terms of the GNU General Public License as published by
> +# the Free Software Foundation; either version 2 of the License, or
> +# (at your option) any later version.
> +#
> +# This...

Read more...

Revision history for this message
Guillaume Hain (zedtux) wrote :

Hello Jelmer,

Thank you for your merge proposal.

> Please avoid boilerplate code like this. Just let external users
> set .image, .email, etc directory. They can always be changed to be
> properties later, if necessary.

Okay, agree. I'm not a python expert, and you are learning me things about it. I did it in that way, because I was thinking that was the right way. Now you've told me that, it make sense to me to write it without property.

Could you explain me when it make sense to use property please?

> It might make sense to rename this to __eq__, so that you can just use
> "==" to compare avatars.

Same as above. I didn't learned it before. Thanks.

> Please avoid "if ... else" in one line as it doesn't work in Python 2.4,
> which we bzr-gtk still supports.

Again, thank you for your knowledge sharing.

> The urllib3 bit probably is no longer valid :)

Completely true.

> You'd probably want "super(AvatarProviderGravatar, self).__init__()" but
> that does the same thing as "AvatarProvider.__init__(self)".

Here is another point you are learning me.
Could you give me more details about this please?

Finally, about files: I will do it as you want. For me, a class per file is OK, but if you prefer the other way, I don't care.
How do you do to organize your files?
When do you decide when create a new file or not?

As I'm not at home for this week-end, I will patch my code during next week. Sorry about that.
I'm really happy to contribute to your project. And I hope we will have more collaboration together in the future! :)

Cheers,

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

On Sat, 2011-03-12 at 11:26 +0000, zedtux wrote:
> > Please avoid boilerplate code like this. Just let external users >
> set .image, .email, etc directory. They can always be changed to be >
> properties later, if necessary. Okay, agree. I'm not a python expert,
> and you are learning me things about it. I did it in that way, because
> I was thinking that was the right way. Now you've told me that, it make
> sense to me to write it without property.
No problem.

> Could you explain me when it make sense to use property please?
If there are side-effects of setting this member variable or if you're
not storing the property in this class but elsewhere you generally want
a property So you could have a "committer_email" property that was
implemented as:

def get_committer_email(self):
   return self.committer.email

committer_email = property(get_committer_email)

Though I'm not sure how useful having a property would be in that
example, as opposed to just having the user look at .committer.email.

The nice thing is that if something is a member variable and you'd like
to change it to a property later, you can do that without having to
change the code that uses it.

> > You'd probably want "super(AvatarProviderGravatar, self).__init__()" but
> > that does the same thing as "AvatarProvider.__init__(self)".
> Here is another point you are learning me.
> Could you give me more details about this please?
super(AvatarProviderGravatar, self) returns the base instance - so for
AvatarProverGravatar that would be a AvatarProvider object. When calling
__init__() on it there's no need to provide self as it's an instance
object.

In the second case you're taking the __init__ method from AvatarProvider
and calling that on the current object.

Both things have the same result, except when you have multiple base
classes in which case you're better off using the second mechanism for
clarity. I'm not exactly sure which base class super() will return in
case of multiple inheritance, I suspect it'll return the first base
class.

> Finally, about files: I will do it as you want. For me, a class per file is OK, but if you prefer the other way, I don't care.
> How do you do to organize your files?
> When do you decide when create a new file or not?
Generally we stick related things in the same file, or in the same
package and try to keep them readable but not too long. Having too many
files usually makes it hard to find what you're looking for, for example
if you're trying to figure out what file to import something from.
Python also has a bit of overhead per file that's loaded, which we're
already suffering from in Bazaar.

I realize that's a bit handwavy, I guess roughly anywhere under 2000
lines would be ok.

> As I'm not at home for this week-end, I will patch my code during next week. Sorry about that.
> I'm really happy to contribute to your project. And I hope we will have more collaboration together in the future! :)
No worries, looking forward to the followup patch :)

Cheers,

Jelmer

lp:~zedtux/bzr-gtk/gravatar-urllib2 updated
721. By Guillaume Hain

Patched code following Jelmer's recommandations

Revision history for this message
Guillaume Hain (zedtux) wrote :

Alright, I've patched my code.
The only thing I didn't is the AvatarProvider class move into the other file. When I've start to copy my code to move it, I was looking at the same time to your comments, and I saw that you're requesting me to move this class that should be inherited by all other future providers (If one day someone want to implement more providers) in the file of one provider, but it make no sense to me.

This master class actually only add the common attribute size, but it also could implement more attributes/method in the future if needed.

So, we have two possibles:
 - If you don't think this class is necessary, I can delete it.
 - Otherwise, it should stay in its file.

Tell me what is for you the best way.
After that point, you should merge my code ;)

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

> Alright, I've patched my code.
> The only thing I didn't is the AvatarProvider class move into the other file.
> When I've start to copy my code to move it, I was looking at the same time to
> your comments, and I saw that you're requesting me to move this class that
> should be inherited by all other future providers (If one day someone want to
> implement more providers) in the file of one provider, but it make no sense to
> me.
>
> This master class actually only add the common attribute size, but it also
> could implement more attributes/method in the future if needed.
>
> So, we have two possibles:
> - If you don't think this class is necessary, I can delete it.
> - Otherwise, it should stay in its file.
I don't see why it would no longer be necessary if it's in the same file - it defines the interface that providers have to implement, that seems like a perfectly reasonable thing to have, even if it's in the same file.

As part of this, I think it would be useful to define a dummy get_base_url() in AvatarProvider that just raises NotImplementedError.

If you look at other parts of the bzr code you'll see this is a common pattern - a base class that provides a bunch of methods that all raise NotImplementedError and are then overridden by the subclasses. This makes it easy to see what to implement when you create a subclass and it gives a sane error message if a subclass forgot to provide a particular method.

Cheers,

Jelmer

lp:~zedtux/bzr-gtk/gravatar-urllib2 updated
722. By Guillaume Hain

Renamed avatarprovider.py to avatarproviders.py
Moved class AvatarProviderGravatar into the avatarproviders.py file
Added get_base_url method into the AvatarProvider class raising NotImplementedError exception

Revision history for this message
Guillaume Hain (zedtux) wrote :

> As part of this, I think it would be useful to define a dummy get_base_url() in AvatarProvider that just raises NotImplementedError.

Done.

Finally, I've renamed file avatarprovider.py to avatarproviders.py, and then moved the AvatarProviderGravatar class into it.
It clear like this for me.

If it's the same for you, you can merge it, it's fine for me.
Otherwise, I'm open to your suggestions! :-)

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

I think it's fine to merge now, but it still adds 5 files, all very small. Would you mind if I brought it down to 1 or 2 ?

lp:~zedtux/bzr-gtk/gravatar-urllib2 updated
723. By Guillaume Hain

Moved Avatar and AvatarBox classes into avatarsbox file

724. By Guillaume Hain

Moved AvatarDownloaderWorker class into avatarsbox.py file

Revision history for this message
Guillaume Hain (zedtux) wrote :

Alright, it's done. :)

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

On Tue, 2011-03-15 at 16:52 +0000, zedtux wrote:
> Alright, it's done. :)
Merged, thanks very much!

Cheers,

Jelmer

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added file 'avatarproviders.py'
2--- avatarproviders.py 1970-01-01 00:00:00 +0000
3+++ avatarproviders.py 2011-03-15 16:52:28 +0000
4@@ -0,0 +1,110 @@
5+# Copyright (C) 2011 by Guillaume Hain (zedtux) <zedtux@zedroot.org>
6+#
7+# This program is free software; you can redistribute it and/or modify
8+# it under the terms of the GNU General Public License as published by
9+# the Free Software Foundation; either version 2 of the License, or
10+# (at your option) any later version.
11+#
12+# This program is distributed in the hope that it will be useful,
13+# but WITHOUT ANY WARRANTY; without even the implied warranty of
14+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15+# GNU General Public License for more details.
16+#
17+# You should have received a copy of the GNU General Public License
18+# along with this program; if not, write to the Free Software
19+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
20+
21+import Queue
22+import urllib
23+import urllib2
24+import hashlib
25+import threading
26+
27+class AvatarProvider(object):
28+ """
29+ Master class for Avatar providers.
30+
31+ All AvatarProviderXxxx classes should inherite from this one
32+ and override at least get_base_url.
33+ """
34+ def __init__(self, size=80):
35+ """ Constructor """
36+ self.size = size
37+
38+ def get_base_url(self):
39+ """
40+ Override this methode in your provider class in order to return
41+ base url of your provider.
42+ """
43+ raise NotImplementedError("You must implement the get_base_url method.")
44+
45+
46+class AvatarDownloaderWorker(threading.Thread):
47+ """
48+ Threaded worker to retrieve avatar from a provider.
49+
50+ It create a persitante connection to the provider in order
51+ to get avatars quickly through the same socket (urllib3).
52+ """
53+
54+ def __init__(self, provider_method):
55+ """
56+ Constructor
57+
58+ provider_method: Provider method that return fields
59+ to send with the request.
60+ """
61+ threading.Thread.__init__(self)
62+ self.__stop = threading.Event()
63+ self.__queue = Queue.Queue()
64+
65+ self.__provider_method = provider_method
66+ self.__end_thread = False
67+
68+ def stop(self):
69+ """ Stop this worker """
70+ self.__end_thread = True
71+ self.__stop.set()
72+
73+ def set_callback_method(self, method):
74+ """ Fire the given callback method when treatment is finished """
75+ self.__callback_method = method
76+
77+ def queue(self, id_field):
78+ """
79+ Put in Queue the id_field to treat in the thread.
80+ This id_field is for example with Gravatar the email address.
81+ """
82+ self.__queue.put(id_field)
83+
84+ def run(self):
85+ """ Worker core code. """
86+ while not self.__end_thread:
87+ id_field = self.__queue.get()
88+ # Call provider method to get fields to pass in the request
89+ url = self.__provider_method(id_field)
90+ # Execute the request
91+ response = urllib2.urlopen(url)
92+ # Fire the callback method
93+ if not self.__callback_method is None:
94+ self.__callback_method(response, id_field)
95+ self.__queue.task_done()
96+
97+
98+class AvatarProviderGravatar(AvatarProvider):
99+ """ Gravatar provider """
100+
101+ def __init__(self):
102+ """ Constructor """
103+ super(AvatarProviderGravatar, self).__init__()
104+
105+ def get_base_url(self):
106+ return "http://www.gravatar.com/avatar.php?"
107+
108+ def gravatar_id_for_email(self, email):
109+ """ Return a converted email address to a gravatar_id """
110+ return self.get_base_url() + \
111+ urllib.urlencode({
112+ 'gravatar_id':hashlib.md5(email.lower()).hexdigest(),
113+ 'size':str(self.size)
114+ })
115
116=== added file 'avatarsbox.py'
117--- avatarsbox.py 1970-01-01 00:00:00 +0000
118+++ avatarsbox.py 2011-03-15 16:52:28 +0000
119@@ -0,0 +1,249 @@
120+# Copyright (C) 2011 by Guillaume Hain (zedtux) <zedtux@zedroot.org>
121+#
122+# This program is free software; you can redistribute it and/or modify
123+# it under the terms of the GNU General Public License as published by
124+# the Free Software Foundation; either version 2 of the License, or
125+# (at your option) any later version.
126+#
127+# This program is distributed in the hope that it will be useful,
128+# but WITHOUT ANY WARRANTY; without even the implied warranty of
129+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
130+# GNU General Public License for more details.
131+#
132+# You should have received a copy of the GNU General Public License
133+# along with this program; if not, write to the Free Software
134+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
135+
136+import gtk
137+
138+from bzrlib.plugins.gtk import _i18n
139+from bzrlib.config import parse_username
140+from bzrlib.plugins.gtk.avatarproviders import AvatarProviderGravatar, AvatarDownloaderWorker
141+
142+
143+class Avatar(gtk.Box):
144+ """ Author or committer avatar """
145+
146+ def __init__(self, apparent_username):
147+ """ Constructor """
148+ gtk.Box.__init__(self)
149+
150+ self.apparent_username = apparent_username
151+ self.username, self.email = parse_username(apparent_username)
152+ self.image = None
153+
154+ def __eq__(self, other):
155+ """
156+ Return True if attributes of the given avatar
157+ match to current object attributes otherwise return False
158+ """
159+ return self.apparent_username == other.apparent_username and \
160+ self.name == other.name and \
161+ self.email == other.email
162+
163+ # ~~~~~ Public methods ~~~~~
164+ def show_spinner(self):
165+ """
166+ Replace the current content of the Avatar with a gtk.Spinner
167+ if an email address has been parsed. If not, show an gtk.Label with
168+ the translatable 'No email' text.
169+ """
170+ if not self.email is "":
171+ spinner = gtk.Spinner()
172+ spinner.start()
173+ self.pack_start(spinner, False)
174+ spinner.set_tooltip_text(_i18n("Retrieving avatar for %s...") % self.email)
175+ spinner.set_size_request(20, 20)
176+ spinner.show()
177+ else:
178+ no_email = gtk.Label(_i18n("No email"))
179+ self.pack_start(no_email)
180+ self.set_tooltip_text(self.apparent_username)
181+ no_email.show()
182+
183+ def show_image(self):
184+ """ Replace the current content of the Avatar with the gtk.Image """
185+ if not self.email is "" and self.image:
186+ self.remove(self.get_children()[0])
187+ self.pack_start(self.image)
188+ self.image.set_tooltip_text(self.apparent_username)
189+ self.image.show()
190+
191+
192+class AvatarBox(gtk.HBox):
193+ """ Improved gtk.HBox """
194+
195+ def __init__(self, homogeneous=False, spacing=0):
196+ """ Constructor """
197+ gtk.HBox.__init__(self, homogeneous, spacing)
198+ self.__avatars = {}
199+ self.avatar = None
200+ self.__displaying = None
201+
202+
203+ # ~~~~~ Public methods ~~~~~
204+ def reset_view(self):
205+ """ Remove current avatars from the gtk box """
206+ for child in self.get_children():
207+ self.remove(child)
208+ self.__displaying = None
209+
210+ def have_avatar(self, avatar):
211+ """
212+ Return True if this box has registered given avatar,
213+ otherwise return False
214+ """
215+ return avatar.email in self.__avatars
216+
217+ def showing(self, avatar):
218+ """
219+ Return True if the displaying avatar is the same
220+ as the given one otherwise return False
221+ """
222+ return self.__displaying and self.__displaying == avatar
223+
224+ def append_avatars_with(self, avatar):
225+ """
226+ Append avatars collection with the given one if not already registed
227+ otherwise render it back.
228+ When an avatar is added this method True, otherwise, if the avatar
229+ was in the collection, return False.
230+ """
231+ if not avatar.email is "" and not avatar.email in self.__avatars:
232+ self.__avatars[avatar.email] = avatar
233+ self._new_cell_for_avatar(avatar)
234+ return True
235+ else:
236+ self.and_avatar_email(avatar.email).come_back_to_gui()
237+ return False
238+
239+ def and_avatar_email(self, email):
240+ """
241+ Select the avatar from avatars collection
242+ in order to apply future actions
243+ """
244+ self.avatar = None
245+ if not email is "" and email in self.__avatars:
246+ self.avatar = self.__avatars[email]
247+ else:
248+ self.avatar = None
249+ return self
250+
251+ def update_avatar_image_from_pixbuf_loader(self, loader):
252+ """ Replace the gtk.Spinner with the given loader """
253+ if self.avatar:
254+ self.avatar.image = gtk.Image()
255+ self.avatar.image.set_from_pixbuf(loader.get_pixbuf())
256+ self.avatar.show_image()
257+ self.__displaying = self.avatar
258+
259+ def come_back_to_gui(self):
260+ """ Render back avatar in the GUI """
261+ if self.avatar:
262+ self.pack_start(self.avatar)
263+ self.__displaying = self.avatar
264+ else:
265+ self._show_no_email()
266+
267+
268+ # ~~~~~ Private methods ~~~~~~
269+ def _new_cell_for_avatar(self, avatar):
270+ """ Create a new cell in this box with a gtk.Spinner """
271+ avatar.show_spinner()
272+ self.pack_start(avatar)
273+ avatar.show()
274+ self.__displaying = avatar
275+
276+ def _show_no_email(self):
277+ """ Show a gtk.Label with test 'No email' """
278+ no_email = gtk.Label(_i18n("No email"))
279+ self.pack_start(no_email)
280+ no_email.show()
281+
282+
283+class AvatarsBox(gtk.HBox):
284+ """ GTK container for authors and committers avatars """
285+
286+ def __init__(self):
287+ """ Constructor """
288+ gtk.HBox.__init__(self, False, 10)
289+
290+ self.__committer_box = None
291+ self.__authors_box = None
292+ self._init_gui()
293+
294+ # If more later you want to implement more avatar providers, to it like this:
295+ # Create a new class named AvatarProvider + provider_name that inherit from
296+ # the AvatarProvider class.
297+ # Implement a method that return url to use in the request.
298+ # For example, with Gravatar, the method return the complete url
299+ # with MD5 hash of the email address and put the value in a gravatar_id field.
300+ # Then create a new worker (manage them in a python dictionnary).
301+ provider = AvatarProviderGravatar()
302+ self.__worker = AvatarDownloaderWorker(
303+ provider.gravatar_id_for_email
304+ )
305+ # This callback method should be fired bt all workers when a request is done.
306+ self.__worker.set_callback_method(self._update_avatar_from_response)
307+ self.__worker.start()
308+
309+
310+ # ~~~~~ Public methods ~~~~~
311+ def add(self, username, role):
312+ """
313+ Add the given username in the right role box and add in the worker queue.
314+ Here again: If you want to implement more providers, you should add the
315+ avatar request in all workers queue.
316+ """
317+ avatar = Avatar(username)
318+ if role is "author" and not self._role_box_for("committer").showing(avatar) or role is "committer":
319+ if self._role_box_for(role).append_avatars_with(avatar):
320+ self.__worker.queue(avatar.email)
321+
322+ def merge(self, usernames, role):
323+ """ Add avatars from a list """
324+ for username in usernames:
325+ self.add(username, role)
326+
327+ def reset(self):
328+ """
329+ Request a reset view for all boxes in order to show only avatars
330+ of the selected line in the revision view screen.
331+ """
332+ [self._role_box_for(role).reset_view() for role in ["committer", "author"]]
333+
334+
335+ # ~~~~~ Private methods ~~~~~
336+ def _init_gui(self):
337+ """ Create boxes where avatars will be displayed """
338+ # 2 gtk.HBox: One for the committer and one for authors
339+ # Committer
340+ self.__committer_box = AvatarBox()
341+ self.__committer_box.set_size_request(80, 80)
342+ self.pack_end(self.__committer_box, False)
343+ self.__committer_box.show()
344+ # Authors
345+ self.__authors_box = AvatarBox()
346+ self.pack_end(self.__authors_box, False)
347+ self.__authors_box.set_spacing(10)
348+ self.__authors_box.show()
349+
350+ def _update_avatar_from_response(self, response, email):
351+ """
352+ Callback method fired by avatar worker when finished.
353+
354+ response is a urllib2.urlopen() return value.
355+ email is used to identify item from self.__avatars.
356+ """
357+ if not email is "":
358+ # Convert downloaded image from provider to gtk.Image
359+ loader = gtk.gdk.PixbufLoader()
360+ loader.write(response.read())
361+ loader.close()
362+
363+ for role in ["committer", "author"]:
364+ self._role_box_for(role).and_avatar_email(email).update_avatar_image_from_pixbuf_loader(loader)
365+
366+ def _role_box_for(self, role):
367+ """ Return the gtk.HBox for the given role """
368+ return self.__committer_box if role is "committer" else self.__authors_box
369
370=== modified file 'revisionview.py'
371--- revisionview.py 2009-12-09 13:27:53 +0000
372+++ revisionview.py 2011-03-15 16:52:28 +0000
373@@ -32,6 +32,9 @@
374
375 from bzrlib.plugins.gtk import icon_path
376
377+from bzrlib.plugins.gtk.avatarsbox import AvatarsBox
378+from bzrlib.plugins.gtk.avatar import Avatar
379+
380 try:
381 from bzrlib.plugins.gtk import seahorse
382 except ImportError:
383@@ -402,13 +405,18 @@
384
385 def _set_revision(self, revision):
386 if revision is None: return
387-
388+
389+ self.avatarsbox.reset()
390+
391 self._revision = revision
392 if revision.committer is not None:
393 self.committer.set_text(revision.committer)
394+ self.avatarsbox.add(revision.committer, "committer")
395 else:
396 self.committer.set_text("")
397+ self.avatarsbox.hide()
398 author = revision.properties.get('author', '')
399+ self.avatarsbox.merge(revision.get_apparent_authors(), "author")
400 if author != '':
401 self.author.set_text(author)
402 self.author.show()
403@@ -579,10 +587,15 @@
404 self.connect_after('notify::revision', self._update_signature)
405
406 def _create_headers(self):
407+ self.avatarsbox = AvatarsBox()
408+
409 self.table = gtk.Table(rows=5, columns=2)
410 self.table.set_row_spacings(6)
411 self.table.set_col_spacings(6)
412 self.table.show()
413+
414+ self.avatarsbox.pack_start(self.table)
415+ self.avatarsbox.show()
416
417 row = 0
418
419@@ -673,7 +686,8 @@
420
421 self.connect('notify::revision', self._add_tags)
422
423- return self.table
424+ self.avatarsbox.show()
425+ return self.avatarsbox
426
427 def _create_parents(self):
428 hbox = gtk.HBox(True, 3)

Subscribers

People subscribed via source and target branches

to all changes:
to status/vote changes: