Merge lp:~dholbach/harvest/504299 into lp:harvest

Proposed by Daniel Holbach
Status: Merged
Merged at revision: 234
Proposed branch: lp:~dholbach/harvest/504299
Merge into: lp:harvest
Diff against target: 343 lines (+97/-67)
6 files modified
harvest/common/opportunity_lists.py (+13/-1)
harvest/locale/harvest.pot (+54/-62)
harvest/media/css/style.css (+6/-0)
harvest/opportunities/filters.py (+14/-0)
harvest/opportunities/management/commands/updatelists.py (+5/-1)
harvest/opportunities/models.py (+5/-3)
To merge this branch: bzr merge lp:~dholbach/harvest/504299
Reviewer Review Type Date Requested Status
James Westby Approve
Dylan McCall Pending
Review via email: mp+31864@code.launchpad.net

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

To post a comment you must log in.
Revision history for this message
Daniel Holbach (dholbach) wrote : Posted in a previous version of this proposal
Revision history for this message
James Westby (james-w) wrote : Posted in a previous version of this proposal

Hi Daniel, this looks great.

326 + explanation = models.TextField(_("Explanatory instructions"), max_length=500, null=True,

is null=True or blank=True the right thing here, I'm never sure?

Thanks,

James

review: Approve
Revision history for this message
Daniel Holbach (dholbach) wrote : Posted in a previous version of this proposal

Thanks for the review and thanks for your sharp eye. The blank/null mis-match was in there for a long time.

"""
Field.null
If True, Django will store empty values as NULL in the database. Default is False.

Note that empty string values will always get stored as empty strings, not as NULL. Only use null=True for non-string fields such as integers, booleans and dates. For both types of fields, you will also need to set blank=True if you wish to permit empty values in forms, as the null parameter only affects database storage (see blank).

Avoid using null on string-based fields such as CharField and TextField unless you have an excellent reason. If a string-based field has null=True, that means it has two possible values for “no data”: NULL, and the empty string. In most cases, it’s redundant to have two possible values for “no data;” Django convention is to use the empty string, not NULL.

Field.blank
If True, the field is allowed to be blank. Default is False.

Note that this is different than null. null is purely database-related, whereas blank is validation-related. If a field has blank=True, validation on Django’s admin site will allow entry of an empty value. If a field has blank=False, the field will be required.
"""

In this branch I basically just changed the description and help_text of Opportunity.explanation. I'll file a separate bug about the null/blank mistake

Revision history for this message
Daniel Holbach (dholbach) wrote : Posted in a previous version of this proposal
Revision history for this message
Daniel Holbach (dholbach) wrote : Posted in a previous version of this proposal

Dylan: can you please comment too? I'm sure there's a much neater way to do it UI-wise. :)

Revision history for this message
Dylan McCall (dylanmccall) wrote : Posted in a previous version of this proposal

Yay! It looks good, but there are a few things.

For code readability, it would be good to move your changes in filters/filters.py over to opportunities/filters.py instead.
filters/filters.py is generic so far, and it would be nice to keep it that way :)
So, we'd be overriding render_html_value_choice in OppListFilter.

I think I would prefer a really simple symbolic style for that help icon. One that matches the text colour, so it doesn't shout for attention. The rest of the site's design gives any bright colour a very powerful presence. That can wait a little while, though (unless you have something handy!).
The help icon is 14px, but the text size is 12px. I don't think it's causing any trouble for spacing (because it's a floated element and there's 5px extra padding on the bottom of the list item), but if someone is already thinking of a nice monochrome icon, one that looks good at 12px would be cool.
Being a fan of plain text, I tried scratching the image and using just a bold question mark character. I thought it looked rather nice — especially with the very pretty UbuntuBeta font, which I'm sure it can use happily some day. It also gives us a lot of room to play with colours. I tried giving it the colour we use for other links — rgb(110,64,84) — and it seemed okay :)
http://people.ubuntu.com/~dylanmccall/harvest/media/gsoc-week11/opportunitylist-questionmark.png

Speaking of floated elements, we need this…
Below line 204:
#filters > .filtergroup > .filter-value > ul > li
Add:
clear:both;
That will force the list item to always add a line between itself and a floated element. (Very important if the help icon wraps to a new line).

The link for the explanation has the explanation itself as the url right now. Probably the best thing to do is a new view function to get an opportunity set's explanation. (Hm… and other details? Could be fun!). It could just be ridiculously simple for now and we'll make it all fancy later on.

I haven't done it yet, but I think you'll want to use:
from django.core.urlresolvers import reverse
(The reverse function works just like the {% url url_name %} template tag).

Thanks!

review: Needs Fixing
Revision history for this message
Daniel Holbach (dholbach) wrote : Posted in a previous version of this proposal

Thanks a lot for your comments Dylan. I tried to implement them.

Can you elaborate on the last two abstracts you wrote? I'm not quite sure I understand.

Revision history for this message
James Westby (james-w) wrote : Posted in a previous version of this proposal

Still looks ok to me.

Thanks,

James

review: Approve
Revision history for this message
Dylan McCall (dylanmccall) wrote : Posted in a previous version of this proposal

Oh, looking at the admin interface I see that the explanation field has help text saying it's a link. So, I was just using it wrong :)
I had written an explanation itself in that field earlier, so the link didn't work for me.
In that case, Django does have a URLField available we can use:
http://docs.djangoproject.com/en/dev/ref/models/fields/#urlfield

Another option is to store the explanation itself on the object, just like we do with the description. That link on the help icon would lead to something like "/opportunities/help/opplist/bitesize", and that page (doesn't need to be anything fancy — yet) would show the opportunity list's explanation. An advantage there is, at some point, it's trivial to have the explanation as a little popup instead of a whole new page.

Revision history for this message
Daniel Holbach (dholbach) wrote : Posted in a previous version of this proposal

> Oh, looking at the admin interface I see that the explanation field has help
> text saying it's a link. So, I was just using it wrong :)
> I had written an explanation itself in that field earlier, so the link didn't
> work for me.
> In that case, Django does have a URLField available we can use:
> http://docs.djangoproject.com/en/dev/ref/models/fields/#urlfield

Will do.

> Another option is to store the explanation itself on the object, just like we
> do with the description. That link on the help icon would lead to something
> like "/opportunities/help/opplist/bitesize", and that page (doesn't need to be
> anything fancy — yet) would show the opportunity list's explanation. An
> advantage there is, at some point, it's trivial to have the explanation as a
> little popup instead of a whole new page.

The problem with that is that we have problems getting that data in. Shall Harvest Admins put it into the db through the admin interface? Should it be part of the harvest-data .csv files? We have some docs already for things like merges (https://wiki.ubuntu.com/UbuntuDevelopment/Merging) - it'd be sweet to just link to that info. :-)

Revision history for this message
Dylan McCall (dylanmccall) wrote : Posted in a previous version of this proposal

Ah, I get it now! In that case I'm quite content with this :)
Thank you!

review: Approve
Revision history for this message
Daniel Holbach (dholbach) wrote :

Thanks for the review!

Revision history for this message
James Westby (james-w) :
review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'harvest/common/opportunity_lists.py'
2--- harvest/common/opportunity_lists.py 2009-07-09 09:40:47 +0000
3+++ harvest/common/opportunity_lists.py 2010-08-05 17:04:49 +0000
4@@ -33,14 +33,26 @@
5 sock.close()
6 return ([map(unicode, [a for a in l]) for l in csv.reader(lines)], datetime_lm)
7
8+def unify_list_entry(entry):
9+ for e in entry:
10+ e = unicode(e.strip())
11+ # only contains url and short description
12+ if len(entry) == 2:
13+ entry.extend([None])
14+ return entry
15
16 def read_lists(list_dir):
17 list_file = os.path.join(list_dir, "opportunities")
18 if not os.path.exists(list_file):
19 return []
20 lines = open(list_file).readlines()
21+
22+ # comment out empty entries, or entries that were commented out
23 lines = filter(lambda b: b.strip() != "" and not b.startswith("#"), lines)
24- return [map(unicode, [a.strip() for a in l]) for l in csv.reader(lines)]
25+
26+ csv_entries = [l for l in csv.reader(lines)]
27+ entries = map(unify_list_entry, csv_entries)
28+ return entries
29
30
31 def pull_lists(data_dir):
32
33=== modified file 'harvest/locale/harvest.pot'
34--- harvest/locale/harvest.pot 2010-07-12 09:33:48 +0000
35+++ harvest/locale/harvest.pot 2010-08-05 17:04:49 +0000
36@@ -8,10 +8,11 @@
37 msgstr ""
38 "Project-Id-Version: PACKAGE VERSION\n"
39 "Report-Msgid-Bugs-To: \n"
40-"POT-Creation-Date: 2010-07-12 04:32-0500\n"
41+"POT-Creation-Date: 2010-08-04 06:14-0500\n"
42 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
43 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
44 "Language-Team: LANGUAGE <LL@li.org>\n"
45+"Language: \n"
46 "MIME-Version: 1.0\n"
47 "Content-Type: text/plain; charset=UTF-8\n"
48 "Content-Transfer-Encoding: 8bit\n"
49@@ -21,15 +22,15 @@
50 msgid "Name"
51 msgstr ""
52
53-#: opportunities/models.py:50 opportunities/models.py:77
54+#: opportunities/models.py:50 opportunities/models.py:78
55 msgid "URL"
56 msgstr ""
57
58-#: opportunities/models.py:51 opportunities/models.py:76
59+#: opportunities/models.py:51 opportunities/models.py:77
60 msgid "Description"
61 msgstr ""
62
63-#: opportunities/models.py:52 opportunities/models.py:78
64+#: opportunities/models.py:52 opportunities/models.py:79
65 msgid "Last Updated"
66 msgstr ""
67
68@@ -46,90 +47,69 @@
69 msgstr ""
70
71 #: opportunities/models.py:56
72-msgid "Explanatory help text"
73+msgid "Explanatory instructions"
74 msgstr ""
75
76 #: opportunities/models.py:57
77+msgid "Link to more instructions about this type of opportunities."
78+msgstr ""
79+
80+#: opportunities/models.py:58
81 msgid "Commentable"
82 msgstr ""
83
84-#: opportunities/models.py:58
85+#: opportunities/models.py:59
86 msgid "Can opportunities on this list have comments?"
87 msgstr ""
88
89-#: opportunities/models.py:59 opportunities/models.py:86
90+#: opportunities/models.py:60 opportunities/models.py:87
91 msgid "Required Experience"
92 msgstr ""
93
94-#: opportunities/models.py:60
95+#: opportunities/models.py:61
96 msgid "Level of experience required for this type of opportunities."
97 msgstr ""
98
99-#: opportunities/models.py:79
100+#: opportunities/models.py:80
101 msgid "Since"
102 msgstr ""
103
104-#: opportunities/models.py:79
105+#: opportunities/models.py:80
106 msgid "On the list since"
107 msgstr ""
108
109-#: opportunities/models.py:80
110+#: opportunities/models.py:81
111 msgid "Irrelevant"
112 msgstr ""
113
114-#: opportunities/models.py:81
115+#: opportunities/models.py:82
116 msgid "Applied"
117 msgstr ""
118
119-#: opportunities/models.py:84
120+#: opportunities/models.py:85
121 msgid "Comment"
122 msgstr ""
123
124-#: opportunities/models.py:85
125+#: opportunities/models.py:86
126 msgid "Valid"
127 msgstr ""
128
129-#: opportunities/models.py:87
130+#: opportunities/models.py:88
131 msgid "Level of experience required for this specific opportunity."
132 msgstr ""
133
134-#: opportunities/models.py:101
135+#: opportunities/models.py:102
136 msgid "Timestamp"
137 msgstr ""
138
139-#: opportunities/models.py:103
140+#: opportunities/models.py:104
141 msgid "Action"
142 msgstr ""
143
144-#: opportunities/views.py:81
145+#: opportunities/views.py:53
146 msgid "Opportunity details could not be saved."
147 msgstr ""
148
149-#: opportunities/templates/opportunities/opportunities_by_package.html:9
150-msgid "Opportunities By Package"
151-msgstr ""
152-
153-#: opportunities/templates/opportunities/opportunities_by_package.html:18
154-#: opportunities/templates/opportunities/opportunities_by_type.html:18
155-#: templates/opportunities/opportunitylist_list.html:11
156-#: templates/opportunities/sourcepackage_list.html:11
157-#, python-format
158-msgid "(1 opportunity)"
159-msgid_plural "(%(counter)s opportunities)"
160-msgstr[0] ""
161-msgstr[1] ""
162-
163-#: opportunities/templates/opportunities/opportunities_by_package.html:49
164-#: opportunities/templates/opportunities/opportunities_by_type.html:24
165-#: templates/opportunities/opportunities_filter.html:45
166-#: templates/opportunities/opportunity_index.html:41
167-msgid "There are currently no opportunities in Harvest. :("
168-msgstr ""
169-
170-#: opportunities/templates/opportunities/opportunities_by_type.html:9
171-msgid "Opportunities By Type"
172-msgstr ""
173-
174 #: templates/404.html:5 templates/404.html.py:8
175 msgid "Page Not Found"
176 msgstr ""
177@@ -146,12 +126,25 @@
178 msgid "A server error has occurred."
179 msgstr ""
180
181-#: templates/base.html:7 templates/base.html.py:82 templates/index.html:9
182+#: templates/base.html:7 templates/base.html.py:31 templates/base.html:62
183+#: templates/base.html.py:73 templates/index.html:9
184 #: templates/admin/base_site.html:7
185 msgid "Harvest"
186 msgstr ""
187
188-#: templates/base.html:73 templates/index.html:22
189+#: templates/base.html:62
190+msgid "Help"
191+msgstr ""
192+
193+#: templates/base.html:62
194+msgid "Bugs"
195+msgstr ""
196+
197+#: templates/base.html:62
198+msgid "Code"
199+msgstr ""
200+
201+#: templates/base.html:75 templates/index.html:16
202 msgid "Translated by:"
203 msgstr ""
204
205@@ -159,15 +152,19 @@
206 msgid "Harvest Admin"
207 msgstr ""
208
209-#: templates/opportunities/opportunities_filter.html:4
210-#: templates/opportunities/opportunity_index.html:4
211+#: templates/opportunities/filter.html:4
212 msgid "Opportunity Index"
213 msgstr ""
214
215-#: templates/opportunities/opportunities_filter.html:9
216-#: templates/opportunities/opportunity_index.html:9
217-#: templates/opportunities/opportunity_list.html:5
218-msgid "Opportunities"
219+#: templates/opportunities/filter_results.html:25
220+#, python-format
221+msgid "%(counter)s package has no matching opportunities"
222+msgid_plural "%(counter)s packages have no matching opportunities"
223+msgstr[0] ""
224+msgstr[1] ""
225+
226+#: templates/opportunities/filter_results.html:31
227+msgid "There are currently no opportunities in Harvest. :("
228 msgstr ""
229
230 #: templates/opportunities/opportunity_edit.html:10
231@@ -178,14 +175,9 @@
232 msgid "Update Information Now!"
233 msgstr ""
234
235-#: templates/opportunities/opportunitylist_list.html:5
236-msgid "Opportunity Lists"
237-msgstr ""
238-
239-#: templates/opportunities/packageset_list.html:5
240-msgid "Packagesets"
241-msgstr ""
242-
243-#: templates/opportunities/sourcepackage_list.html:5
244-msgid "Source Packages"
245-msgstr ""
246+#: templates/opportunities/package_details.html:10
247+#, python-format
248+msgid "%(counter)s opportunity hidden"
249+msgid_plural "%(counter)s opportunities hidden"
250+msgstr[0] ""
251+msgstr[1] ""
252
253=== modified file 'harvest/media/css/style.css'
254--- harvest/media/css/style.css 2010-07-31 20:22:58 +0000
255+++ harvest/media/css/style.css 2010-08-05 17:04:49 +0000
256@@ -196,6 +196,12 @@
257 }
258 #filters > .filtergroup > .filter-value > ul > li {
259 padding-bottom:5px;
260+ clear:both;
261+}
262+
263+#filters > .filtergroup > .filter-value > ul > li a.help {
264+ font-weight: bold;
265+ text-color: rgb(110,64,84);
266 }
267
268 #filters .editfilter input {
269
270=== modified file 'harvest/opportunities/filters.py'
271--- harvest/opportunities/filters.py 2010-07-16 05:21:21 +0000
272+++ harvest/opportunities/filters.py 2010-08-05 17:04:49 +0000
273@@ -32,6 +32,20 @@
274 def process_queryset(self, queryset):
275 return queryset.filter(opportunitylist__in = self.get_selected_choices())
276
277+ def render_html_value_choice(self, item_id):
278+ toggle_params = self.serialize(self.get_value_with_selection(item_id))
279+ item_href = self.get_system().get_url_with_parameters(toggle_params)
280+ title_attribute = ''
281+ help_html = ''
282+ if hasattr(self.choices_dict[item_id], 'description') and \
283+ self.choices_dict[item_id].description:
284+ title_attribute = 'title="%s"' % self.choices_dict[item_id].description
285+ if hasattr(self.choices_dict[item_id], 'explanation') and \
286+ self.choices_dict[item_id].explanation:
287+ help_html = '<a class="help" href="%s">?</a>' % \
288+ self.choices_dict[item_id].explanation
289+ return '<a class="item-toggle" href="%s" %s>%s</a> %s' % (item_href, title_attribute, item_id, help_html)
290+
291
292 #we don't really need to create a special type here, but it may be handy
293 class HarvestFilters(containers.FilterSystem):
294
295=== modified file 'harvest/opportunities/management/commands/updatelists.py'
296--- harvest/opportunities/management/commands/updatelists.py 2010-03-21 15:32:06 +0000
297+++ harvest/opportunities/management/commands/updatelists.py 2010-08-05 17:04:49 +0000
298@@ -19,9 +19,13 @@
299 os.makedirs(data_dir)
300 list_dir = opportunity_lists.pull_lists(data_dir)
301
302- for (list_url, list_description) in opportunity_lists.read_lists(list_dir):
303+ for (list_url,
304+ list_description,
305+ list_explanation) in opportunity_lists.read_lists(list_dir):
306 op_list, created = OpportunityList.objects.get_or_create(url=list_url)
307 op_list.description = list_description
308+ if list_explanation:
309+ op_list.explanation = list_explanation
310 op_list.active = True
311 op_list.name = self.chop_name(list_url)
312 op_list.save()
313
314=== modified file 'harvest/opportunities/models.py'
315--- harvest/opportunities/models.py 2010-07-29 02:57:09 +0000
316+++ harvest/opportunities/models.py 2010-08-05 17:04:49 +0000
317@@ -47,13 +47,15 @@
318
319 class OpportunityList(models.Model):
320 name = models.SlugField(_("Name"), max_length=70)
321- url = models.URLField(_("URL"))
322+ url = models.URLField(_("URL"), verify_exists=False)
323 description = models.TextField(_("Description"), max_length=350)
324 last_updated = models.DateTimeField(_("Last Updated"), null=True)
325 active = models.BooleanField(_("Active"), default=True)
326 featured = models.BooleanField(_("Featured"), default=False,
327 help_text=_("Specially feature this list of opportunities?"))
328- explanation = models.TextField(_("Explanatory help text"), max_length=500, null=True)
329+ explanation = models.URLField(_("Explanatory instructions"), max_length=500, null=True,
330+ help_text=_("Link to more instructions about this type of opportunities."),
331+ verify_exists=False)
332 commentable = models.BooleanField(_("Commentable"), default=True,
333 help_text=_("Can opportunities on this list have comments?"))
334 experience = models.IntegerField(_("Required Experience"), default=0, choices=EXPERIENCE_CHOICES,
335@@ -74,7 +76,7 @@
336
337 class Opportunity(models.Model):
338 description = models.TextField(_("Description"), max_length=350)
339- url = models.URLField(_("URL"), max_length=350)
340+ url = models.URLField(_("URL"), max_length=350, verify_exists=False)
341 last_updated = models.DateTimeField(_("Last Updated"), null=True)
342 since = models.DateTimeField(_("Since"), help_text=_("On the list since"), null=True)
343 reviewed = models.BooleanField(_("Irrelevant"), default=False, blank=True)

Subscribers

People subscribed via source and target branches

to all changes: