Merge lp:~huwshimi/launchpad/avatars-everywhere-712894 into lp:launchpad

Proposed by Huw Wilkins
Status: Work in progress
Proposed branch: lp:~huwshimi/launchpad/avatars-everywhere-712894
Merge into: lp:launchpad
Diff against target: 202 lines (+73/-12)
6 files modified
lib/canonical/launchpad/icing/style.css (+9/-0)
lib/lp/app/browser/tales.py (+34/-4)
lib/lp/code/browser/branchlisting.py (+8/-3)
lib/lp/registry/model/person.py (+11/-3)
lib/lp/registry/model/product.py (+7/-2)
lib/lp/services/features/flags.py (+4/-0)
To merge this branch: bzr merge lp:~huwshimi/launchpad/avatars-everywhere-712894
Reviewer Review Type Date Requested Status
Robert Collins (community) Needs Fixing
Review via email: mp+81430@code.launchpad.net

Commit message

Usernames now have the user's avatar next to them instead of the generic user icon.

Description of the change

This branch swaps out the icon checking code for avatar checking code. The tail then returns the username with the avatar instead of the generic user icon.

Screenshot of the change: https://launchpadlibrarian.net/84606031/avatars_screenshot.png

To test the following feature rule needs to be set:

tales.avatars.enabled default 1 on

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

I haven't triple-checked this, but there is a decent chance this will cause widespread timeouts due to lazy evaluation of the logo (vs the icon).

getPrecachedPersonsFromIDs is the primary eager loading function to update, changing from need_icon to need_logo, and so on down the call chain from there.

(Reason being, that loading even the icons for 50 people can take a significant chunk of time, and some pages like blueprint reports load the icons for hundreds of people).

I'm going to mark this needs fixing, because I fully expect this problem to crop up. However, if you want to land it under a feature flag, so that widespread testing can be done, that would be fine with me.

I haven't done a detailed review, sorry.

review: Needs Fixing
Revision history for this message
Huw Wilkins (huwshimi) wrote :
Revision history for this message
Huw Wilkins (huwshimi) wrote :

Thanks Rob, I'll add the feature flag. I was kind of hoping I would get away with the change as custom_logo_url and custom_icon_url are very similar, but I don't understand the underlying code. I'll push some changes.

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

On Mon, Nov 7, 2011 at 6:42 PM, Huw Wilkins <email address hidden> wrote:
> Thanks Rob, I'll add the feature flag. I was kind of hoping I would get away with the change as custom_logo_url and custom_icon_url are very similar, but I don't understand the underlying code. I'll push some changes.

So here is what happens:
Our Person object has a link to a LibraryFileAlias for each of the icon sizes.
The link has to be traversed to render a URL to the librarian for the
icon/logo/branding.
So currently, where we have had performance issues and setup eager
loading, we traverse the *icon* link for all the Person objects at
once, which means one pretty efficient DB query. The results for this
then sit in the storm cache until the template renders them.

Your change makes the template rendering look at 'logo' not 'icon',
and that won't be in the storm cache, so a page with (say) 100
subscribers, will suddenly do 100 extra DB queries.

-Rob

Revision history for this message
Huw Wilkins (huwshimi) wrote :

Thanks a lot for the explanation Rob. I've added the feature flag.

To test you would need to set the feature rule:

tales.avatars.enabled default 1 on

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

This is pretty close to what I meant, yes.

119 columns.extend(valid_stuff["tables"])
120 decorators.extend(valid_stuff["decorators"])
121 - if need_icon:
122 - IconAlias = ClassAlias(LibraryFileAlias, "LibraryFileAlias")
123 - origin.append(LeftJoin(IconAlias, Person.icon == IconAlias.id))
124 - columns.append(IconAlias)
125 + if need_logo:
126 + LogoAlias = ClassAlias(LibraryFileAlias, "LibraryFileAlias")
127 + origin.append(LeftJoin(LogoAlias, Person.logo == LogoAlias.id))
128 + columns.append(LogoAlias)
129 if len(columns) == 1:
130 column = columns[0]
131 # Return a simple ResultSet
132

This unconditionally switches everything over to using logo rather than icon - it needs to be feature guarded as well.

I suggest the following approach:
 - add need_logo as a separate parameter
 - leave need_icon in place
 - grep for places passing need_icon in and feature flag them to pass need_icon or need_logo appropriately (or to not change if they are not affected by your patch).

This gives us feature flag safety in this code layer.

Or, you can change need_icon to need_person_graphic and use feature flags to decide which thing to add to the query - but I think this would be a little uglier and is less flexible, if we want different things in different places.

Revision history for this message
Huw Wilkins (huwshimi) wrote :

Thanks for your patience Robert. I've restored the need_icon parameter and changed calls with need_icon to need_logo under the feature flag.

Revision history for this message
Huw Wilkins (huwshimi) wrote :

I also fixed the XSS hole wgrant mentioned.

Revision history for this message
Curtis Hovey (sinzui) wrote :

Hi huw.

I think there is a disconnect between your change and what a user can do. We rejected scaling images because early tests showed that they scaled badly. So I expect users will lots of ugly logos every where. When the user visits <https://launchpad.net/~/+branding>, the page does not explain that the logo will be used as an icon to make links to the person. Actually, this is not entirely true. When Lp had maps, we had icons for users to show in the map. So many users will be confused if they see the size, and know they had set something different for that size.

Team's do have icons set, your branch ignores them, so the instructions on a team +branding page are not just wrong, they are contradictory.

See https://bugs.launchpad.net/launchpad/+bugs?field.tag=branding for a short list of bugs about what is wrong with branding, including the fact that I have have a link called "branding" on my profile page--I assume I have an agent if I have a brand.

Revision history for this message
Huw Wilkins (huwshimi) wrote :

Hi Curtis,

I realise the problem of the size issues. Really there is an abnormal distiction between sizes in Launchpad compared with how everyone else handles avatars. I guess this is because we don't have any image resizing infrastructure. Once we do have the infrastructure I hope to replace the different size uploads with one image. It'll also mean we can resize these images properly for the small avatars instead of doing browser resizing like this branch has to do.

For this branch I've ignored all of that because we have to start somewhere.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/canonical/launchpad/icing/style.css'
2--- lib/canonical/launchpad/icing/style.css 2011-11-02 21:19:13 +0000
3+++ lib/canonical/launchpad/icing/style.css 2011-11-08 02:25:27 +0000
4@@ -203,6 +203,15 @@
5 padding-bottom: 0.5em;
6 padding-left: 18px;
7 }
8+a.bg-image.has-avatar {
9+ padding-left: 0;
10+ }
11+.avatar {
12+ width: 18px;
13+ height: 18px;
14+ margin-right: 5px;
15+ vertical-align: top;
16+ }
17
18 /* == Application-specific styles == */
19
20
21=== modified file 'lib/lp/app/browser/tales.py'
22--- lib/lp/app/browser/tales.py 2011-10-28 15:23:20 +0000
23+++ lib/lp/app/browser/tales.py 2011-11-08 02:25:27 +0000
24@@ -70,6 +70,7 @@
25 from canonical.launchpad.webapp.menu import (
26 get_current_view,
27 get_facet,
28+ structured,
29 )
30 from canonical.launchpad.webapp.publisher import (
31 get_current_browser_request,
32@@ -831,6 +832,25 @@
33 #XXX: this should go away as soon as all image:icon where replaced
34 return None
35
36+ def custom_logo_url(self):
37+ """Return the URL for this object's logo."""
38+ context = self._context
39+ if not IHasLogo.providedBy(context):
40+ context = nearest(context, IHasLogo)
41+ if context is None:
42+ # we use the Launchpad logo for anything which is in no way
43+ # related to a Pillar (for example, a buildfarm)
44+ url = '/@@/launchpad-logo'
45+ elif context.logo is not None:
46+ url = context.logo.getURL()
47+ else:
48+ url = self.default_logo_resource(context)
49+ if url is None:
50+ # We want to indicate that there is no logo for this
51+ # object.
52+ return None
53+ return url
54+
55 def logo(self):
56 """Return the appropriate <img> tag for this object's logo.
57
58@@ -1201,15 +1221,25 @@
59 def _makeLink(self, view_name, rootsite, text):
60 person = self._context
61 url = self.url(view_name, rootsite)
62- custom_icon = ObjectImageDisplayAPI(person).custom_icon_url()
63+ if getFeatureFlag('tales.avatars.enabled'):
64+ custom_icon = ObjectImageDisplayAPI(person).custom_logo_url()
65+ else:
66+ custom_icon = ObjectImageDisplayAPI(person).custom_icon_url()
67 if custom_icon is None:
68 css_class = ObjectImageDisplayAPI(person).sprite_css()
69 return (u'<a href="%s" class="%s">%s</a>') % (
70 url, css_class, cgi.escape(text))
71 else:
72- return (u'<a href="%s" class="bg-image" '
73- 'style="background-image: url(%s)">%s</a>') % (
74- url, custom_icon, cgi.escape(text))
75+ if getFeatureFlag('tales.avatars.enabled'):
76+ return structured(u'<a href="%(url)s"'
77+ 'class="bg-image has-avatar">'
78+ '<img src="%(icon_url)s" alt="Avatar for %(link_text)s"'
79+ 'class="avatar" />%(link_text)s</a>',
80+ url=url, icon_url=custom_icon, link_text=text).escapedtext
81+ else:
82+ return (u'<a href="%s" class="bg-image" '
83+ 'style="background-image: url(%s)">%s</a>') % (
84+ url, custom_icon, cgi.escape(text))
85
86 def link(self, view_name, rootsite='mainsite'):
87 """See `ObjectFormatterAPI`.
88
89=== modified file 'lib/lp/code/browser/branchlisting.py'
90--- lib/lp/code/browser/branchlisting.py 2011-11-04 15:23:35 +0000
91+++ lib/lp/code/browser/branchlisting.py 2011-11-08 02:25:27 +0000
92@@ -454,9 +454,14 @@
93
94 # Cache display information for authors of branches' respective
95 # last revisions.
96- getUtility(IPersonSet).getPrecachedPersonsFromIDs(
97- [revision.revision_author.personID for revision in revisions],
98- need_icon=True)
99+ if getFeatureFlag('tales.avatars.enabled'):
100+ getUtility(IPersonSet).getPrecachedPersonsFromIDs(
101+ [revision.revision_author.personID for revision in revisions],
102+ need_logo=True)
103+ else:
104+ getUtility(IPersonSet).getPrecachedPersonsFromIDs(
105+ [revision.revision_author.personID for revision in revisions],
106+ need_icon=True)
107
108 # Return a dict keyed on branch id.
109 return dict(
110
111=== modified file 'lib/lp/registry/model/person.py'
112--- lib/lp/registry/model/person.py 2011-10-27 03:02:10 +0000
113+++ lib/lp/registry/model/person.py 2011-11-08 02:25:27 +0000
114@@ -4276,7 +4276,8 @@
115 def getPrecachedPersonsFromIDs(
116 self, person_ids, need_karma=False, need_ubuntu_coc=False,
117 need_location=False, need_archive=False,
118- need_preferred_email=False, need_validity=False, need_icon=False):
119+ need_preferred_email=False, need_validity=False,
120+ need_icon=False, need_logo=False):
121 """See `IPersonSet`."""
122 origin = [Person]
123 conditions = [
124@@ -4286,13 +4287,14 @@
125 need_karma=need_karma, need_ubuntu_coc=need_ubuntu_coc,
126 need_location=need_location, need_archive=need_archive,
127 need_preferred_email=need_preferred_email,
128- need_validity=need_validity, need_icon=need_icon)
129+ need_validity=need_validity,
130+ need_icon=need_icon, need_logo=need_logo)
131
132 def _getPrecachedPersons(
133 self, origin, conditions, store=None,
134 need_karma=False, need_ubuntu_coc=False,
135 need_location=False, need_archive=False, need_preferred_email=False,
136- need_validity=False, need_icon=False):
137+ need_validity=False, need_icon=False, need_logo=False):
138 """Lookup all members of the team with optional precaching.
139
140 :param store: Provide ability to specify the store.
141@@ -4309,6 +4311,8 @@
142 :param need_validity: The is_valid attribute will be cached.
143 :param need_icon: Cache the persons' icons so that their URLs can
144 be generated without further reference to the database.
145+ :param need_logo: Cache the persons' logos so that their URLs can
146+ be generated without further reference to the database.
147 """
148 if store is None:
149 store = IStore(Person)
150@@ -4367,6 +4371,10 @@
151 IconAlias = ClassAlias(LibraryFileAlias, "LibraryFileAlias")
152 origin.append(LeftJoin(IconAlias, Person.icon == IconAlias.id))
153 columns.append(IconAlias)
154+ if need_logo:
155+ LogoAlias = ClassAlias(LibraryFileAlias, "LibraryFileAlias")
156+ origin.append(LeftJoin(LogoAlias, Person.logo == LogoAlias.id))
157+ columns.append(LogoAlias)
158 if len(columns) == 1:
159 column = columns[0]
160 # Return a simple ResultSet
161
162=== modified file 'lib/lp/registry/model/product.py'
163--- lib/lp/registry/model/product.py 2011-10-29 18:23:06 +0000
164+++ lib/lp/registry/model/product.py 2011-11-08 02:25:27 +0000
165@@ -168,6 +168,7 @@
166 from lp.registry.model.productseries import ProductSeries
167 from lp.registry.model.series import ACTIVE_STATUSES
168 from lp.registry.model.sourcepackagename import SourcePackageName
169+from lp.services.features import getFeatureFlag
170 from lp.services.database import bulk
171 from lp.services.propertycache import (
172 cachedproperty,
173@@ -1378,8 +1379,12 @@
174 def do_eager_load(rows):
175 owner_ids = set(map(operator.attrgetter('_ownerID'), rows))
176 # +detailed-listing renders the person with team branding.
177- list(getUtility(IPersonSet).getPrecachedPersonsFromIDs(
178- owner_ids, need_validity=True, need_icon=True))
179+ if getFeatureFlag('tales.avatars.enabled'):
180+ list(getUtility(IPersonSet).getPrecachedPersonsFromIDs(
181+ owner_ids, need_validity=True, need_logo=True))
182+ else:
183+ list(getUtility(IPersonSet).getPrecachedPersonsFromIDs(
184+ owner_ids, need_validity=True, need_icon=True))
185
186 return DecoratedResultSet(result, pre_iter_hook=do_eager_load)
187
188
189=== modified file 'lib/lp/services/features/flags.py'
190--- lib/lp/services/features/flags.py 2011-11-03 13:58:07 +0000
191+++ lib/lp/services/features/flags.py 2011-11-08 02:25:27 +0000
192@@ -225,6 +225,10 @@
193 '',
194 '',
195 ''),
196+ ('tales.avatars.enabled',
197+ 'boolean',
198+ 'Replaces generic user icons with avatars next to usernames.',
199+ ''),
200 ])
201
202 # The set of all flag names that are documented.