Merge lp:~dholbach/harvest/504299 into lp:harvest
- 504299
- Merge into trunk
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 | ||||
Related bugs: |
|
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.
Commit message
Description of the change
Daniel Holbach (dholbach) wrote : Posted in a previous version of this proposal | # |
James Westby (james-w) wrote : Posted in a previous version of this proposal | # |
Hi Daniel, this looks great.
326 + explanation = models.
is null=True or blank=True the right thing here, I'm never sure?
Thanks,
James
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.
Daniel Holbach (dholbach) wrote : Posted in a previous version of this proposal | # |
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. :)
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/filters.py is generic so far, and it would be nice to keep it that way :)
So, we'd be overriding render_
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://
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.
(The reverse function works just like the {% url url_name %} template tag).
Thanks!
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.
James Westby (james-w) wrote : Posted in a previous version of this proposal | # |
Still looks ok to me.
Thanks,
James
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://
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
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://
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
> 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:/
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!
Daniel Holbach (dholbach) wrote : | # |
Thanks for the review!
James Westby (james-w) : | # |
Preview Diff
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) |
http:// people. canonical. com/~dholbach/ tmp/Bildschirmf oto.png is what it looks like.