Merge lp:~aaronp/software-center/modify-reviews into lp:software-center
- modify-reviews
- Merge into trunk
Proposed by
Aaron Peachey
Status: | Merged | ||||
---|---|---|---|---|---|
Merged at revision: | 1887 | ||||
Proposed branch: | lp:~aaronp/software-center/modify-reviews | ||||
Merge into: | lp:software-center | ||||
Diff against target: |
947 lines (+542/-65) 8 files modified
setup.py (+2/-0) softwarecenter/backend/reviews.py (+87/-0) softwarecenter/backend/rnrclient_pristine.py (+18/-0) softwarecenter/paths.py (+2/-0) softwarecenter/ui/gtk/appdetailsview.py (+11/-0) softwarecenter/ui/gtk/appdetailsview_gtk.py (+10/-1) softwarecenter/ui/gtk/widgets/reviews.py (+109/-13) utils/submit_review.py (+303/-51) |
||||
To merge this branch: | bzr merge lp:~aaronp/software-center/modify-reviews | ||||
Related bugs: |
|
||||
Related blueprints: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
software-store-developers | Pending | ||
Review via email: mp+63693@code.launchpad.net |
Commit message
Description of the change
implements edit and delete review functionality
production rnr server currently does not support the edit functionality (achuni advised ~3 weeks before next release) but the changes to USC are designed to fail gracefully on errors from the server
To post a comment you must log in.
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === added symlink 'data/delete_review.py' |
2 | === target is u'../utils/delete_review.py' |
3 | === added symlink 'data/modify_review.py' |
4 | === target is u'../utils/modify_review.py' |
5 | === modified file 'setup.py' |
6 | --- setup.py 2011-05-31 21:12:53 +0000 |
7 | +++ setup.py 2011-06-07 12:26:16 +0000 |
8 | @@ -79,6 +79,8 @@ |
9 | "utils/submit_review.py", |
10 | "utils/report_review.py", |
11 | "utils/submit_usefulness.py", |
12 | + "utils/delete_review.py", |
13 | + "utils/modify_review.py", |
14 | "utils/get_reviews_helper.py", |
15 | "utils/get_review_stats_helper.py", |
16 | "utils/get_useful_votes_helper.py", |
17 | |
18 | === modified file 'softwarecenter/backend/reviews.py' |
19 | --- softwarecenter/backend/reviews.py 2011-06-01 08:04:32 +0000 |
20 | +++ softwarecenter/backend/reviews.py 2011-06-07 12:26:16 +0000 |
21 | @@ -47,6 +47,8 @@ |
22 | SUBMIT_REVIEW_APP, |
23 | REPORT_REVIEW_APP, |
24 | SUBMIT_USEFULNESS_APP, |
25 | + DELETE_REVIEW_APP, |
26 | + MODIFY_REVIEW_APP, |
27 | GET_REVIEWS_HELPER, |
28 | GET_REVIEW_STATS_HELPER, |
29 | GET_USEFUL_VOTES_HELPER, |
30 | @@ -184,6 +186,8 @@ |
31 | self.usefulness_favorable = 0 |
32 | # this will be set if tryint to submit usefulness for this review failed |
33 | self.usefulness_submit_error = False |
34 | + self.delete_error = False |
35 | + self.modify_error = False |
36 | def __repr__(self): |
37 | return "[Review id=%s review_text='%s' reviewer_username='%s']" % ( |
38 | self.id, self.review_text, self.reviewer_username) |
39 | @@ -328,6 +332,29 @@ |
40 | (pid, stdin, stdout, stderr) = glib.spawn_async( |
41 | cmd, flags=glib.SPAWN_DO_NOT_REAP_CHILD, standard_output=True) |
42 | glib.child_watch_add(pid, self._on_submit_usefulness_finished, (review_id, is_useful, stdout, callback)) |
43 | + |
44 | + def spawn_delete_review_ui(self, review_id, parent_xid, datadir, callback): |
45 | + cmd = [os.path.join(datadir, DELETE_REVIEW_APP), |
46 | + "--review-id", "%s" % review_id, |
47 | + "--parent-xid", "%s" % parent_xid, |
48 | + "--datadir", datadir, |
49 | + ] |
50 | + (pid, stdin, stdout, stderr) = glib.spawn_async( |
51 | + cmd, flags=glib.SPAWN_DO_NOT_REAP_CHILD, standard_output=True) |
52 | + glib.child_watch_add(pid, self._on_delete_review_finished, (review_id, callback)) |
53 | + |
54 | + def spawn_modify_review_ui(self, parent_xid, iconname, datadir, review_id, callback): |
55 | + """ this spawns the UI for writing a new review and |
56 | + adds it automatically to the reviews DB """ |
57 | + cmd = [os.path.join(datadir, MODIFY_REVIEW_APP), |
58 | + "--parent-xid", "%s" % parent_xid, |
59 | + "--iconname", iconname, |
60 | + "--datadir", "%s" % datadir, |
61 | + "--review-id", "%s" % review_id, |
62 | + ] |
63 | + (pid, stdin, stdout, stderr) = glib.spawn_async( |
64 | + cmd, flags=glib.SPAWN_DO_NOT_REAP_CHILD, standard_output=True) |
65 | + glib.child_watch_add(pid, self._on_modify_review_finished, (review_id, stdout, callback)) |
66 | |
67 | # internal callbacks/helpers |
68 | def _on_submit_review_finished(self, pid, status, (app, stdout_fd, callback)): |
69 | @@ -369,6 +396,64 @@ |
70 | callback(app, self._reviews[app]) |
71 | break |
72 | |
73 | + def _on_delete_review_finished(self, pid, status, (review_id, callback)): |
74 | + """ called when delete_review finished""" |
75 | + exitcode = os.WEXITSTATUS(status) |
76 | + if exitcode == 0: |
77 | + LOG.debug("delete id %s " % review_id) |
78 | + for (app, reviews) in self._reviews.iteritems(): |
79 | + for review in reviews: |
80 | + if str(review.id) == str(review_id): |
81 | + # remove the one we don't want to see anymore |
82 | + self._reviews[app].remove(review) |
83 | + callback(app, self._reviews[app]) |
84 | + break |
85 | + else: |
86 | + LOG.debug("delete review id=%s failed with exitcode %s" % ( |
87 | + review_id, exitcode)) |
88 | + for (app, reviews) in self._reviews.iteritems(): |
89 | + for review in reviews: |
90 | + if str(review.id) == str(review_id): |
91 | + review.delete_error = exitcode |
92 | + callback(app, self._reviews[app]) |
93 | + break |
94 | + |
95 | + def _on_modify_review_finished(self, pid, status, (review_id, stdout_fd, callback)): |
96 | + """called when modify_review finished""" |
97 | + exitcode = os.WEXITSTATUS(status) |
98 | + LOG.debug("_on_modify_review_finished") |
99 | + stdout = "" |
100 | + while True: |
101 | + s = os.read(stdout_fd, 1024) |
102 | + if not s: break |
103 | + stdout += s |
104 | + LOG.debug("stdout from modify_review: '%s'" % stdout) |
105 | + if exitcode == 0: |
106 | + try: |
107 | + review_json = simplejson.loads(stdout) |
108 | + except simplejson.decoder.JSONDecodeError: |
109 | + LOG.error("failed to parse '%s'" % stdout) |
110 | + return |
111 | + mod_review = ReviewDetails.from_dict(review_json) |
112 | + |
113 | + for (app, reviews) in self._reviews.iteritems(): |
114 | + for review in reviews: |
115 | + if str(review.id) == str(review_id): |
116 | + # remove the one we don't want to see anymore |
117 | + self._reviews[app].remove(review) |
118 | + self._reviews[app].insert(0, Review.from_piston_mini_client(mod_review)) |
119 | + callback(app, self._reviews[app]) |
120 | + break |
121 | + else: |
122 | + LOG.debug("modify review id=%s failed with exitcode %s" % ( |
123 | + review_id, exitcode)) |
124 | + for (app, reviews) in self._reviews.iteritems(): |
125 | + for review in reviews: |
126 | + if str(review.id) == str(review_id): |
127 | + review.modify_error = exitcode |
128 | + callback(app, self._reviews[app]) |
129 | + break |
130 | + |
131 | def _on_submit_usefulness_finished(self, pid, status, (review_id, is_useful, stdout_fd, callback)): |
132 | """ called when report_usefulness finished """ |
133 | exitcode = os.WEXITSTATUS(status) |
134 | @@ -421,6 +506,7 @@ |
135 | self.rnrclient = RatingsAndReviewsAPI(cachedir=cachedir) |
136 | self._reviews = {} |
137 | |
138 | + |
139 | def _update_rnrclient_offline_state(self): |
140 | # this needs the lp:~mvo/piston-mini-client/offline-mode branch |
141 | self.rnrclient._offline_mode = not network_state_is_connected() |
142 | @@ -506,6 +592,7 @@ |
143 | # stats |
144 | def refresh_review_stats(self, callback): |
145 | """ public api, refresh the available statistics """ |
146 | + |
147 | try: |
148 | mtime = os.path.getmtime(self.REVIEW_STATS_CACHE_FILE) |
149 | days_delta = int((time.time() - mtime) // (24*60*60)) |
150 | |
151 | === modified file 'softwarecenter/backend/rnrclient_pristine.py' |
152 | --- softwarecenter/backend/rnrclient_pristine.py 2011-04-11 14:09:54 +0000 |
153 | +++ softwarecenter/backend/rnrclient_pristine.py 2011-06-07 12:26:16 +0000 |
154 | @@ -167,3 +167,21 @@ |
155 | |
156 | return self._get('usefulness/', args=data, |
157 | scheme=PUBLIC_API_SCHEME) |
158 | + |
159 | + @validate('review_id', int) |
160 | + @returns_json |
161 | + def delete_review(self, review_id): |
162 | + """Delete a review""" |
163 | + return self._post('/reviews/delete/%s/' % review_id, data={}, |
164 | + scheme=AUTHENTICATED_API_SCHEME) |
165 | + |
166 | + @validate('review_id', int) |
167 | + @validate('rating', int) |
168 | + @validate_pattern('summary', r'[^\n]+') |
169 | + @validate_pattern('review_text', r'[^\n]+') |
170 | + @returns(ReviewDetails) |
171 | + def modify_review(self, review_id, rating, summary, review_text): |
172 | + """Modify an existing review""" |
173 | + data = {'rating':rating, 'summary':summary, 'review_text':review_text} |
174 | + return self._put('/reviews/modify/%s/' % review_id, data=data, |
175 | + scheme=AUTHENTICATED_API_SCHEME) |
176 | |
177 | === modified file 'softwarecenter/paths.py' |
178 | --- softwarecenter/paths.py 2011-05-31 07:26:24 +0000 |
179 | +++ softwarecenter/paths.py 2011-06-07 12:26:16 +0000 |
180 | @@ -58,6 +58,8 @@ |
181 | SUBMIT_REVIEW_APP = "submit_review.py" |
182 | REPORT_REVIEW_APP = "report_review.py" |
183 | SUBMIT_USEFULNESS_APP = "submit_usefulness.py" |
184 | +MODIFY_REVIEW_APP = "modify_review.py" |
185 | +DELETE_REVIEW_APP = "delete_review.py" |
186 | GET_REVIEWS_HELPER = "get_reviews_helper.py" |
187 | GET_REVIEW_STATS_HELPER = "get_review_stats_helper.py" |
188 | GET_USEFUL_VOTES_HELPER = "get_useful_votes_helper.py" |
189 | |
190 | === modified file 'softwarecenter/ui/gtk/appdetailsview.py' |
191 | --- softwarecenter/ui/gtk/appdetailsview.py 2011-06-05 22:50:04 +0000 |
192 | +++ softwarecenter/ui/gtk/appdetailsview.py 2011-06-07 12:26:16 +0000 |
193 | @@ -131,6 +131,17 @@ |
194 | review_id, is_useful, parent_xid, self.datadir, |
195 | self._reviews_ready_callback) |
196 | |
197 | + def _review_modify(self, review_id): |
198 | + parent_xid = get_parent_xid(self) |
199 | + self.review_loader.spawn_modify_review_ui( |
200 | + parent_xid, self.appdetails.icon, self.datadir, review_id, |
201 | + self._reviews_ready_callback) |
202 | + |
203 | + def _review_delete(self, review_id): |
204 | + parent_xid = get_parent_xid(self) |
205 | + self.review_loader.spawn_delete_review_ui( |
206 | + review_id, parent_xid, self.datadir, self._reviews_ready_callback) |
207 | + |
208 | # public interface |
209 | def reload(self): |
210 | """ reload the package cache, this goes straight to the backend """ |
211 | |
212 | === modified file 'softwarecenter/ui/gtk/appdetailsview_gtk.py' |
213 | --- softwarecenter/ui/gtk/appdetailsview_gtk.py 2011-06-05 22:50:04 +0000 |
214 | +++ softwarecenter/ui/gtk/appdetailsview_gtk.py 2011-06-07 12:26:16 +0000 |
215 | @@ -850,6 +850,7 @@ |
216 | if my_votes: |
217 | self.reviews.update_useful_votes(my_votes) |
218 | |
219 | + self.reviews.clear() |
220 | for review in reviews_data: |
221 | self.reviews.add_review(review) |
222 | self.reviews.configure_reviews_ui() |
223 | @@ -1150,6 +1151,8 @@ |
224 | self.reviews.connect("new-review", self._on_review_new) |
225 | self.reviews.connect("report-abuse", self._on_review_report_abuse) |
226 | self.reviews.connect("submit-usefulness", self._on_review_submit_usefulness) |
227 | + self.reviews.connect("modify-review", self._on_review_modify) |
228 | + self.reviews.connect("delete-review", self._on_review_delete) |
229 | self.reviews.connect("more-reviews-clicked", self._on_more_reviews_clicked) |
230 | self.reviews.connect("different-review-language-clicked", self._on_reviews_in_different_language_clicked) |
231 | vb.pack_start(self.reviews, False) |
232 | @@ -1197,7 +1200,13 @@ |
233 | return False |
234 | |
235 | def _on_review_new(self, button): |
236 | - self._review_write_new() |
237 | + self._review_write_new() |
238 | + |
239 | + def _on_review_modify(self, button, review_id): |
240 | + self._review_modify(review_id) |
241 | + |
242 | + def _on_review_delete(self, button, review_id): |
243 | + self._review_delete(review_id) |
244 | |
245 | def _on_review_report_abuse(self, button, review_id): |
246 | self._review_report_abuse(str(review_id)) |
247 | |
248 | === modified file 'softwarecenter/ui/gtk/widgets/reviews.py' |
249 | --- softwarecenter/ui/gtk/widgets/reviews.py 2011-06-05 22:50:04 +0000 |
250 | +++ softwarecenter/ui/gtk/widgets/reviews.py 2011-06-07 12:26:16 +0000 |
251 | @@ -714,6 +714,12 @@ |
252 | 'submit-usefulness':(gobject.SIGNAL_RUN_FIRST, |
253 | gobject.TYPE_NONE, |
254 | (gobject.TYPE_PYOBJECT, bool)), |
255 | + 'modify-review':(gobject.SIGNAL_RUN_FIRST, |
256 | + gobject.TYPE_NONE, |
257 | + (gobject.TYPE_PYOBJECT,)), |
258 | + 'delete-review':(gobject.SIGNAL_RUN_FIRST, |
259 | + gobject.TYPE_NONE, |
260 | + (gobject.TYPE_PYOBJECT,)), |
261 | 'more-reviews-clicked':(gobject.SIGNAL_RUN_FIRST, |
262 | gobject.TYPE_NONE, |
263 | () ), |
264 | @@ -957,14 +963,24 @@ |
265 | self.yes_like = None |
266 | self.no_like = None |
267 | self.status_box = gtk.HBox() |
268 | + self.delete_status_box = gtk.HBox() |
269 | + self.delete_error_img = gtk.Image() |
270 | + self.delete_error_img.set_from_stock(gtk.STOCK_DIALOG_ERROR, gtk.ICON_SIZE_SMALL_TOOLBAR) |
271 | self.submit_error_img = gtk.Image() |
272 | self.submit_error_img.set_from_stock(gtk.STOCK_DIALOG_ERROR, gtk.ICON_SIZE_SMALL_TOOLBAR) |
273 | self.submit_status_spinner = gtk.Spinner() |
274 | self.submit_status_spinner.set_size_request(12,12) |
275 | + self.delete_status_spinner = gtk.Spinner() |
276 | + self.delete_status_spinner.set_size_request(12,12) |
277 | self.acknowledge_error = mkit.VLinkButton(_("<small>OK</small>")) |
278 | self.acknowledge_error.set_underline(True) |
279 | self.acknowledge_error.set_subdued(True) |
280 | + self.delete_acknowledge_error = mkit.VLinkButton(_("<small>OK</small>")) |
281 | + self.delete_acknowledge_error.set_underline(True) |
282 | + self.delete_acknowledge_error.set_subdued(True) |
283 | self.usefulness_error = False |
284 | + self.delete_error = False |
285 | + self.modify_error = False |
286 | |
287 | self.pack_start(self.header, False) |
288 | self.pack_start(self.body, False) |
289 | @@ -1011,6 +1027,11 @@ |
290 | if reviews: |
291 | reviews.emit("report-abuse", self.id) |
292 | |
293 | + def _on_modify_clicked(self, button): |
294 | + reviews = self.get_ancestor(UIReviewsList) |
295 | + if reviews: |
296 | + reviews.emit("modify-review", self.id) |
297 | + |
298 | def _on_useful_clicked(self, btn, is_useful): |
299 | reviews = self.get_ancestor(UIReviewsList) |
300 | if reviews: |
301 | @@ -1066,6 +1087,56 @@ |
302 | # example raw_date str format: 2011-01-28 19:15:21 |
303 | return datetime.datetime.strptime(raw_date_str, '%Y-%m-%d %H:%M:%S') |
304 | |
305 | + def _delete_ui_update(self, type, current_user_reviewer=False, action=None): |
306 | + self._hide_delete_elements() |
307 | + if type == 'renew': |
308 | + self._build_delete_flag_ui(current_user_reviewer) |
309 | + return |
310 | + if type == 'progress': |
311 | + self.delete_status_spinner.start() |
312 | + self.delete_status_spinner.show() |
313 | + self.delete_status_label = gtk.Label("<small><b>%s</b></small>" % _(u"Deleting now\u2026")) |
314 | + self.delete_status_box.pack_start(self.delete_status_spinner, False) |
315 | + self.delete_status_label.set_use_markup(True) |
316 | + self.delete_status_label.set_padding(2,0) |
317 | + self.delete_status_box.pack_start(self.delete_status_label,False) |
318 | + self.delete_status_label.show() |
319 | + if type == 'error': |
320 | + self.delete_error_img.show() |
321 | + self.delete_status_label = gtk.Label("<small><b>%s</b></small>" % _("Error %s review" % action)) |
322 | + self.delete_status_box.pack_start(self.delete_error_img, False) |
323 | + self.delete_status_label.set_use_markup(True) |
324 | + self.delete_status_label.set_padding(2,0) |
325 | + self.delete_status_box.pack_start(self.delete_status_label,False) |
326 | + self.delete_status_label.show() |
327 | + self.delete_acknowledge_error.show() |
328 | + self.delete_status_box.pack_start(self.delete_acknowledge_error,False) |
329 | + self.delete_acknowledge_error.connect('clicked', self._on_delete_error_acknowledged, current_user_reviewer) |
330 | + self.delete_status_box.show() |
331 | + self.footer.pack_end(self.delete_status_box, False) |
332 | + return |
333 | + |
334 | + def _on_delete_clicked(self, btn): |
335 | + reviews = self.get_ancestor(UIReviewsList) |
336 | + if reviews: |
337 | + self._delete_ui_update('progress') |
338 | + reviews.emit("delete-review", self.id) |
339 | + |
340 | + def _on_delete_error_acknowledged(self, button, current_user_reviewer): |
341 | + self.delete_error = False |
342 | + self._delete_ui_update('renew', current_user_reviewer) |
343 | + |
344 | + def _hide_delete_elements(self): |
345 | + """ hide all delete elements """ |
346 | + for attr in ["complain", "edit", "delete", "delete_status_spinner", |
347 | + "delete_error_img", "delete_status_box", "delete_status_label", |
348 | + "delete_acknowledge_error", "flagbox" |
349 | + ]: |
350 | + o = getattr(self, attr, None) |
351 | + if o: |
352 | + o.hide() |
353 | + return |
354 | + |
355 | def _build(self, review_data, app_version, logged_in_person, useful_votes): |
356 | |
357 | # all the attributes of review_data may need markup escape, |
358 | @@ -1081,6 +1152,8 @@ |
359 | self.useful_total = useful_total = review_data.usefulness_total |
360 | useful_favorable = review_data.usefulness_favorable |
361 | useful_submit_error = review_data.usefulness_submit_error |
362 | + delete_error = review_data.delete_error |
363 | + modify_error = review_data.modify_error |
364 | |
365 | dark_color = self.style.dark[gtk.STATE_NORMAL] |
366 | m = self._whom_when_markup(self.person, displayname, cur_t, dark_color) |
367 | @@ -1133,19 +1206,12 @@ |
368 | |
369 | self._build_usefulness_ui(current_user_reviewer, useful_total, |
370 | useful_favorable, useful_votes, useful_submit_error) |
371 | - |
372 | - # Translators: This link is for flagging a review as inappropriate. |
373 | - # To minimize repetition, if at all possible, keep it to a single word. |
374 | - # If your language has an obvious verb, it won't need a question mark. |
375 | - self.complain = mkit.VLinkButton('<small>%s</small>' % _('Inappropriate?')) |
376 | - self.complain.set_subdued(True) |
377 | - self.complain.set_underline(True) |
378 | - self.footer.pack_end(self.complain, False) |
379 | - self.complain.connect('clicked', self._on_report_abuse_clicked) |
380 | - # FIXME: dynamically update this on network changes |
381 | - self.complain.set_sensitive(network_state_is_connected()) |
382 | - self.body.connect('size-allocate', self._on_allocate, stars, |
383 | - summary, text, who_when, version_lbl, self.complain) |
384 | + |
385 | + self.flagbox = gtk.HBox() |
386 | + self._build_delete_flag_ui(current_user_reviewer, delete_error, modify_error) |
387 | + self.footer.pack_end(self.flagbox,False) |
388 | + self.body.connect('size-allocate', self._on_allocate, stars, summary, text, who_when, version_lbl, self.flagbox) |
389 | + |
390 | return |
391 | |
392 | def _build_usefulness_ui(self, current_user_reviewer, useful_total, |
393 | @@ -1264,6 +1330,36 @@ |
394 | |
395 | return gtk.Label('<small>%s</small>' % s) |
396 | |
397 | + def _build_delete_flag_ui(self, current_user_reviewer, delete_error=False, modify_error=False): |
398 | + if delete_error: |
399 | + self._delete_ui_update('error', current_user_reviewer, 'deleting') |
400 | + elif modify_error: |
401 | + self._delete_ui_update('error', current_user_reviewer, 'modifying') |
402 | + else: |
403 | + if current_user_reviewer: |
404 | + self.edit = mkit.VLinkButton('<small>%s</small>' %_('Edit')) |
405 | + self.delete = mkit.VLinkButton('<small>%s</small>' %_('Delete')) |
406 | + self.edit.set_underline(True) |
407 | + self.delete.set_underline(True) |
408 | + self.edit.set_subdued(True) |
409 | + self.delete.set_subdued(True) |
410 | + self.flagbox.pack_start(self.edit, False) |
411 | + self.flagbox.pack_start(self.delete, False) |
412 | + self.edit.connect('clicked', self._on_modify_clicked) |
413 | + self.delete.connect('clicked', self._on_delete_clicked) |
414 | + else: |
415 | + # Translators: This link is for flagging a review as inappropriate. |
416 | + # To minimize repetition, if at all possible, keep it to a single word. |
417 | + # If your language has an obvious verb, it won't need a question mark. |
418 | + self.complain = mkit.VLinkButton('<small>%s</small>' % _('Inappropriate?')) |
419 | + self.complain.set_subdued(True) |
420 | + self.complain.set_underline(True) |
421 | + self.complain.set_sensitive(network_state_is_connected()) |
422 | + self.flagbox.pack_start(self.complain, False) |
423 | + self.complain.connect('clicked', self._on_report_abuse_clicked) |
424 | + self.flagbox.show_all() |
425 | + return |
426 | + |
427 | def _whom_when_markup(self, person, displayname, cur_t, dark_color): |
428 | nice_date = get_nice_date_string(cur_t) |
429 | #dt = datetime.datetime.utcnow() - cur_t |
430 | |
431 | === added symlink 'utils/delete_review.py' |
432 | === target is u'submit_review.py' |
433 | === added symlink 'utils/modify_review.py' |
434 | === target is u'submit_review.py' |
435 | === modified file 'utils/submit_review.py' |
436 | --- utils/submit_review.py 2011-06-06 08:49:12 +0000 |
437 | +++ utils/submit_review.py 2011-06-07 12:26:16 +0000 |
438 | @@ -69,6 +69,7 @@ |
439 | distro = get_distro() |
440 | SERVER_ROOT=distro.REVIEWS_SERVER |
441 | |
442 | + |
443 | # server status URL |
444 | SERVER_STATUS_URL = SERVER_ROOT+"/server-status/" |
445 | |
446 | @@ -117,6 +118,12 @@ |
447 | def submit_usefulness(self, review_id, is_useful): |
448 | self.emit("transmit-start", review_id) |
449 | self.worker_thread.pending_usefulness.put((int(review_id), is_useful)) |
450 | + def modify_review(self, review_id, review): |
451 | + self.emit("transmit-start", review_id) |
452 | + self.worker_thread.pending_modify.put((int(review_id), review)) |
453 | + def delete_review(self, review_id): |
454 | + self.emit("transmit-start", review_id) |
455 | + self.worker_thread.pending_delete.put(int(review_id)) |
456 | def server_status(self): |
457 | self.worker_thread.pending_server_status() |
458 | def shutdown(self): |
459 | @@ -140,6 +147,8 @@ |
460 | self.pending_reviews = Queue() |
461 | self.pending_reports = Queue() |
462 | self.pending_usefulness = Queue() |
463 | + self.pending_modify = Queue() |
464 | + self.pending_delete = Queue() |
465 | self.pending_server_status = Queue() |
466 | self._shutdown = False |
467 | # FIXME: instead of a binary value we need the state associated |
468 | @@ -176,11 +185,15 @@ |
469 | self._submit_reviews_if_pending() |
470 | self._submit_reports_if_pending() |
471 | self._submit_usefulness_if_pending() |
472 | + self._submit_modify_if_pending() |
473 | + self._submit_delete_if_pending() |
474 | time.sleep(0.2) |
475 | if (self._shutdown and |
476 | self.pending_reviews.empty() and |
477 | self.pending_usefulness.empty() and |
478 | - self.pending_reports.empty()): |
479 | + self.pending_reports.empty() and |
480 | + self.pending_modify.empty() and |
481 | + self.pending_delete.empty()): |
482 | return |
483 | |
484 | # usefulness |
485 | @@ -207,6 +220,59 @@ |
486 | self._write_exception_html_log_if_needed(e) |
487 | self._transmit_state = TRANSMIT_STATE_ERROR |
488 | self.pending_usefulness.task_done() |
489 | + |
490 | + #modify |
491 | + def queue_modify(self, modification): |
492 | + """ queue a new review modification request for sending to LP """ |
493 | + logging.debug("queue_modify %s %s" % modification) |
494 | + self.pending_modify.put(modification) |
495 | + |
496 | + def _submit_modify_if_pending(self): |
497 | + """ the actual modify function """ |
498 | + while not self.pending_modify.empty(): |
499 | + logging.debug("_modify_review") |
500 | + self._transmit_state = TRANSMIT_STATE_INPROGRESS |
501 | + (review_id, review) = self.pending_modify.get() |
502 | + summary = review['summary'] |
503 | + review_text = review['review_text'] |
504 | + rating = review['rating'] |
505 | + try: |
506 | + res = self.rnrclient.modify_review(review_id=review_id, |
507 | + summary=summary, |
508 | + review_text=review_text, |
509 | + rating=rating) |
510 | + self._transmit_state = TRANSMIT_STATE_DONE |
511 | + sys.stdout.write(simplejson.dumps(vars(res))) |
512 | + except Exception as e: |
513 | + logging.exception("modify_review") |
514 | + err_str = self._get_error_messages(e) |
515 | + self._write_exception_html_log_if_needed(e) |
516 | + self._transmit_state = TRANSMIT_STATE_ERROR |
517 | + self._transmit_error_str = err_str |
518 | + self.pending_modify.task_done() |
519 | + |
520 | + #delete |
521 | + def queue_delete(self, deletion): |
522 | + """ queue a new deletion request for sending to LP """ |
523 | + logging.debug("queue_delete review id: %s" % deletion) |
524 | + self.pending_delete.put(deletion) |
525 | + |
526 | + def _submit_delete_if_pending(self): |
527 | + """ the actual deletion """ |
528 | + while not self.pending_delete.empty(): |
529 | + logging.debug("POST delete") |
530 | + self._transmit_state = TRANSMIT_STATE_INPROGRESS |
531 | + review_id = self.pending_delete.get() |
532 | + try: |
533 | + res = self.rnrclient.delete_review(review_id=review_id) |
534 | + self._transmit_state = TRANSMIT_STATE_DONE |
535 | + sys.stdout.write(simplejson.dumps(res)) |
536 | + except Exception as e: |
537 | + logging.exception("delete_review failed") |
538 | + self._write_exception_html_log_if_needed(e) |
539 | + self._transmit_error_str = _("Failed to delete review") |
540 | + self._transmit_state = TRANSMIT_STATE_ERROR |
541 | + self.pending_delete.task_done() |
542 | |
543 | # reports |
544 | def queue_report(self, report): |
545 | @@ -350,6 +416,9 @@ |
546 | #submit success image |
547 | self.submit_success_img = gtk.Image() |
548 | self.submit_success_img.set_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_SMALL_TOOLBAR) |
549 | + #submit warn image |
550 | + self.submit_warn_img = gtk.Image() |
551 | + self.submit_warn_img.set_from_stock(gtk.STOCK_DIALOG_INFO, gtk.ICON_SIZE_SMALL_TOOLBAR) |
552 | #label size to prevent image or spinner from resizing |
553 | self.label_transmit_status.set_size_request(-1, gtk.icon_size_lookup(gtk.ICON_SIZE_SMALL_TOOLBAR)[1]) |
554 | |
555 | @@ -472,6 +541,8 @@ |
556 | """method to separate the updating of status icon/spinner and message in the submit review window, |
557 | takes a type (progress, fail, success) as a string and a message string then updates status area accordingly""" |
558 | self._clear_status_imagery() |
559 | + self.label_transmit_status.set_text("") |
560 | + |
561 | if type == "progress": |
562 | self.status_hbox.pack_start(self.submit_spinner, False) |
563 | self.status_hbox.reorder_child(self.submit_spinner, 0) |
564 | @@ -490,6 +561,11 @@ |
565 | self.status_hbox.reorder_child(self.submit_success_img, 0) |
566 | self.submit_success_img.show() |
567 | self.label_transmit_status.set_text(message) |
568 | + elif type == "warning": |
569 | + self.status_hbox.pack_start(self.submit_warn_img, False) |
570 | + self.status_hbox.reorder_child(self.submit_warn_img, 0) |
571 | + self.submit_warn_img.show() |
572 | + self.label_transmit_status.set_text(message) |
573 | |
574 | def _clear_status_imagery(self): |
575 | self.detail_expander.hide() |
576 | @@ -514,13 +590,18 @@ |
577 | except TypeError: |
578 | pass |
579 | |
580 | + try: |
581 | + self.status_hbox.query_child_packing(self.submit_warn_img) |
582 | + self.status_hbox.remove(self.submit_warn_img) |
583 | + except TypeError: |
584 | + pass |
585 | + |
586 | return |
587 | - |
588 | + |
589 | |
590 | class SubmitReviewsApp(BaseApp): |
591 | """ review a given application or package """ |
592 | |
593 | - |
594 | STAR_SIZE = (32, 32) |
595 | APP_ICON_SIZE = 48 |
596 | #character limits for text boxes and hurdles for indicator changes |
597 | @@ -532,8 +613,9 @@ |
598 | ERROR_COLOUR = "FF0000" |
599 | SUBMIT_MESSAGE = _("Submitting Review") |
600 | FAILURE_MESSAGE = _("Failed to submit review") |
601 | + SUCCESS_MESSAGE = _("Review submitted") |
602 | |
603 | - def __init__(self, app, version, iconname, origin, parent_xid, datadir): |
604 | + def __init__(self, app, version, iconname, origin, parent_xid, datadir, action="submit", review_id=0): |
605 | BaseApp.__init__(self, datadir, "submit_review.ui") |
606 | self.datadir = datadir |
607 | # legal fineprint, do not change without consulting a lawyer |
608 | @@ -546,20 +628,6 @@ |
609 | self.submit_window.connect("destroy", self.on_button_cancel_clicked) |
610 | self._add_spellcheck_to_textview(self.textview_review) |
611 | |
612 | - # gwibber stuff |
613 | - self.gwibber_combo = gtk.combo_box_new_text() |
614 | - #cells = self.gwibber_combo.get_cells() |
615 | - #cells[0].set_property("ellipsize", pango.ELLIPSIZE_END) |
616 | - self.gwibber_hbox.pack_start(self.gwibber_combo, True) |
617 | - if "SOFTWARE_CENTER_GWIBBER_MOCK_USERS" in os.environ: |
618 | - self.gwibber_helper = GwibberHelperMock() |
619 | - else: |
620 | - self.gwibber_helper = GwibberHelper() |
621 | - |
622 | - #get a dict with a saved gwibber_send (boolean) and gwibber account_id for persistent state |
623 | - self.gwibber_prefs = self._get_gwibber_prefs() |
624 | - |
625 | - # interactive star rating |
626 | self.star_rating = StarRatingSelector(0, star_size=self.STAR_SIZE) |
627 | self.star_caption = StarCaption() |
628 | |
629 | @@ -573,33 +641,85 @@ |
630 | self.rating_hbox.reorder_child(self.star_caption, 1) |
631 | |
632 | self.review_buffer = self.textview_review.get_buffer() |
633 | - |
634 | + |
635 | self.detail_expander.hide() |
636 | + |
637 | + self.retrieve_api = RatingsAndReviewsAPI() |
638 | |
639 | + |
640 | # data |
641 | self.app = app |
642 | self.version = version |
643 | self.origin = origin |
644 | self.iconname = iconname |
645 | + self.action = action |
646 | + self.review_id = int(review_id) |
647 | |
648 | - # title |
649 | - self.submit_window.set_title(_("Review %s") % self.app.name) |
650 | + # parent xid |
651 | + if parent_xid: |
652 | + win = gtk.gdk.window_foreign_new(int(parent_xid)) |
653 | + if win: |
654 | + self.submit_window.realize() |
655 | + self.submit_window.window.set_transient_for(win) |
656 | |
657 | + self.submit_window.set_position(gtk.WIN_POS_MOUSE) |
658 | + |
659 | self.review_summary_entry.connect('changed', self._on_mandatory_text_entry_changed) |
660 | self.star_rating.connect('changed', self._on_mandatory_fields_changed) |
661 | self.review_buffer.connect('changed', self._on_text_entry_changed) |
662 | |
663 | # gwibber stuff |
664 | + self.gwibber_combo = gtk.combo_box_new_text() |
665 | + #cells = self.gwibber_combo.get_cells() |
666 | + #cells[0].set_property("ellipsize", pango.ELLIPSIZE_END) |
667 | + self.gwibber_hbox.pack_start(self.gwibber_combo, True) |
668 | + if "SOFTWARE_CENTER_GWIBBER_MOCK_USERS" in os.environ: |
669 | + self.gwibber_helper = GwibberHelperMock() |
670 | + else: |
671 | + self.gwibber_helper = GwibberHelper() |
672 | + |
673 | + #get a dict with a saved gwibber_send (boolean) and gwibber account_id for persistent state |
674 | + self.gwibber_prefs = self._get_gwibber_prefs() |
675 | + |
676 | + # gwibber stuff |
677 | self._setup_gwibber_gui() |
678 | |
679 | - # parent xid |
680 | - if parent_xid: |
681 | - win = gtk.gdk.window_foreign_new(int(parent_xid)) |
682 | - if win: |
683 | - self.submit_window.realize() |
684 | - self.submit_window.window.set_transient_for(win) |
685 | + #now setup rest of app based on whether submit or modify |
686 | + if self.action == "submit": |
687 | + self._init_submit() |
688 | + elif self.action == "modify": |
689 | + self._init_modify() |
690 | |
691 | - self.submit_window.set_position(gtk.WIN_POS_MOUSE) |
692 | + def _init_submit(self): |
693 | + self.submit_window.set_title(_("Review %s" % self.app.name)) |
694 | + |
695 | + def _init_modify(self): |
696 | + self._populate_review() |
697 | + self.submit_window.set_title(_("Modify Your %s Review" % self.app.name)) |
698 | + self.button_post.set_label(_("Modify")) |
699 | + self.SUBMIT_MESSAGE = _("Updating your review") |
700 | + self.FAILURE_MESSAGE = _("Failed to edit review") |
701 | + self.SUCCESS_MESSAGE = _("Review updated") |
702 | + self._enable_or_disable_post_button() |
703 | + |
704 | + def _populate_review(self): |
705 | + try: |
706 | + review_data = self.retrieve_api.get_review(review_id=self.review_id) |
707 | + app = Application(pkgname=review_data.package_name) |
708 | + self.app = app |
709 | + self.review_summary_entry.set_text(review_data.summary) |
710 | + self.star_rating.set_rating(review_data.rating) |
711 | + self.review_buffer.set_text(review_data.review_text) |
712 | + #save original review field data, for comparison purposes when user makes changes to fields |
713 | + self.orig_summary_text = review_data.summary |
714 | + self.orig_star_rating = review_data.rating |
715 | + self.orig_review_text = review_data.review_text |
716 | + self.version = review_data.version |
717 | + self.origin = review_data.origin |
718 | + return |
719 | + except piston_mini_client.APIError: |
720 | + logging.warn('Unable to retrieve review id %s for editing. Exiting' % self.review_id) |
721 | + self.quit(2) |
722 | |
723 | def _setup_details(self, widget, app, iconname, version, display_name): |
724 | # icon shazam |
725 | @@ -654,8 +774,41 @@ |
726 | review_chars and review_chars <= self.REVIEW_CHAR_LIMITS[0] and |
727 | self.star_rating.get_rating()): |
728 | self.button_post.set_sensitive(True) |
729 | + self._change_status("clear", "") |
730 | else: |
731 | self.button_post.set_sensitive(False) |
732 | + self._change_status("clear", "") |
733 | + |
734 | + #set post button insensitive, if review being modified is the same as what is currently in the UI fields |
735 | + #checks if 'original' review attributes exist to avoid exceptions when this method has been called prior to review being retrieved |
736 | + if self.action == 'modify' and hasattr(self, "orig_star_rating"): |
737 | + if self._modify_review_is_the_same(): |
738 | + self.button_post.set_sensitive(False) |
739 | + self._change_status("warning", _("Can't submit unmodified")) |
740 | + else: |
741 | + self._change_status("clear", "") |
742 | + |
743 | + def _modify_review_is_the_same(self): |
744 | + '''checks if review fields are the same as the review being modified and returns true if so''' |
745 | + |
746 | + #perform an initial check on character counts to return False if any don't match, avoids doing unnecessary string comparisons |
747 | + if (self.review_summary_entry.get_text_length() != len(self.orig_summary_text) or |
748 | + self.review_buffer.get_char_count() != len(self.orig_review_text)): |
749 | + return False |
750 | + #compare rating |
751 | + if self.star_rating.get_rating() != self.orig_star_rating: |
752 | + return False |
753 | + #compare summary text |
754 | + if self.review_summary_entry.get_text() != self.orig_summary_text: |
755 | + return False |
756 | + #compare review text |
757 | + if self.review_buffer.get_text(self.review_buffer.get_start_iter(), |
758 | + self.review_buffer.get_end_iter()) != self.orig_review_text: |
759 | + return False |
760 | + return True |
761 | + |
762 | + |
763 | + |
764 | |
765 | def _check_summary_character_count(self): |
766 | summary_chars = self.review_summary_entry.get_text_length() |
767 | @@ -737,7 +890,14 @@ |
768 | review.rating = self.star_rating.get_rating() |
769 | review.package_version = self.version |
770 | review.origin = self.origin |
771 | - self.api.submit_review(review) |
772 | + |
773 | + if self.action == "submit": |
774 | + self.api.submit_review(review) |
775 | + elif self.action == "modify": |
776 | + changes = {'review_text':review.text, |
777 | + 'summary':review.summary, |
778 | + 'rating':review.rating} |
779 | + self.api.modify_review(self.review_id, changes) |
780 | |
781 | def login_successful(self, display_name): |
782 | self.main_notebook.set_current_page(1) |
783 | @@ -887,7 +1047,7 @@ |
784 | |
785 | def _success_status(self): |
786 | """Updates status area to show success for 2 seconds then allows window to proceed""" |
787 | - self._change_status("success", _("Review submitted.")) |
788 | + self._change_status("success", self.SUCCESS_MESSAGE) |
789 | while gtk.events_pending(): |
790 | gtk.main_iteration(False) |
791 | time.sleep(2) |
792 | @@ -995,7 +1155,6 @@ |
793 | self.textview_report.get_buffer().connect( |
794 | "changed", self._enable_or_disable_report_button) |
795 | |
796 | - |
797 | # data |
798 | self.review_id = review_id |
799 | |
800 | @@ -1097,27 +1256,67 @@ |
801 | self.api.submit_usefulness(self.review_id, self.is_useful) |
802 | |
803 | def on_transmit_failure(self, api, trans, error): |
804 | - print "exiting - error: %s" % error |
805 | - self.api.shutdown() |
806 | - self.quit(2) |
807 | - |
808 | - # override parents run to only trigger login (and subsequent |
809 | - # events) but no UI, if this is commented out, there is some |
810 | - # stub ui that can be useful for testing |
811 | - def run(self): |
812 | - self.login() |
813 | - |
814 | - # override UI update methods from BaseApp to prevent them |
815 | - # causing errors if called when UI is hidden |
816 | - def _clear_status_imagery(self): |
817 | - pass |
818 | - |
819 | - def _change_status(self, type, message): |
820 | - pass |
821 | - |
822 | + logging.warn("exiting - error: %s" % error) |
823 | + self.api.shutdown() |
824 | + self.quit(2) |
825 | + |
826 | + # override parents run to only trigger login (and subsequent |
827 | + # events) but no UI, if this is commented out, there is some |
828 | + # stub ui that can be useful for testing |
829 | + def run(self): |
830 | + self.login() |
831 | + |
832 | + # override UI update methods from BaseApp to prevent them |
833 | + # causing errors if called when UI is hidden |
834 | + def _clear_status_imagery(self): |
835 | + pass |
836 | + |
837 | + def _change_status(self, type, message): |
838 | + pass |
839 | + |
840 | +class DeleteReviewApp(BaseApp): |
841 | + SUBMIT_MESSAGE = _(u"Deleting review\u2026") |
842 | + FAILURE_MESSAGE = _("Failed to delete review") |
843 | + |
844 | + def __init__(self, review_id, parent_xid, datadir): |
845 | + # uses same UI as submit usefulness because |
846 | + # (a) it isn't shown and (b) it's similar in usage |
847 | + BaseApp.__init__(self, datadir, "submit_usefulness.ui") |
848 | + # data |
849 | + self.review_id = review_id |
850 | + # no UI except for error conditions |
851 | + self.parent_xid = parent_xid |
852 | + |
853 | + # override behavior of baseapp here as we don't actually |
854 | + # have a UI by default |
855 | + def _get_parent_xid_for_login_window(self): |
856 | + return self.parent_xid |
857 | + |
858 | + def login_successful(self, display_name): |
859 | + logging.debug("delete review") |
860 | + self.main_notebook.set_current_page(1) |
861 | + self.api.delete_review(self.review_id) |
862 | + |
863 | + def on_transmit_failure(self, api, trans, error): |
864 | + logging.warn("exiting - error: %s" % error) |
865 | + self.api.shutdown() |
866 | + self.quit(2) |
867 | + |
868 | + # override parents run to only trigger login (and subsequent |
869 | + # events) but no UI, if this is commented out, there is some |
870 | + # stub ui that can be useful for testing |
871 | + def run(self): |
872 | + self.login() |
873 | + |
874 | + # override UI update methods from BaseApp to prevent them |
875 | + # causing errors if called when UI is hidden |
876 | + def _clear_status_imagery(self): |
877 | + pass |
878 | + |
879 | + def _change_status(self, type, message): |
880 | + pass |
881 | |
882 | if __name__ == "__main__": |
883 | - |
884 | try: |
885 | locale.setlocale(locale.LC_ALL, "") |
886 | except: |
887 | @@ -1219,6 +1418,59 @@ |
888 | parent_xid=options.parent_xid, |
889 | is_useful=int(options.is_useful)) |
890 | usefulness_app.run() |
891 | - |
892 | + |
893 | + if "delete_review" in sys.argv[0]: |
894 | + #check options |
895 | + parser.add_option("", "--review-id") |
896 | + parser.add_option("", "--parent-xid") |
897 | + parser.add_option("", "--debug", |
898 | + action="store_true", default=False) |
899 | + (options, args) = parser.parse_args() |
900 | + |
901 | + if not (options.review_id): |
902 | + parser.error(_("Missing review-id argument")) |
903 | + |
904 | + if options.debug: |
905 | + logging.basicConfig(level=logging.DEBUG) |
906 | + |
907 | + logging.debug("delete review mode") |
908 | + |
909 | + delete_app = DeleteReviewApp(datadir=options.datadir, |
910 | + review_id=options.review_id, |
911 | + parent_xid=options.parent_xid) |
912 | + delete_app.run() |
913 | + |
914 | + if "modify_review" in sys.argv[0]: |
915 | + # check options |
916 | + parser.add_option("", "--review-id") |
917 | + parser.add_option("-i", "--iconname") |
918 | + parser.add_option("", "--parent-xid") |
919 | + parser.add_option("", "--debug", |
920 | + action="store_true", default=False) |
921 | + (options, args) = parser.parse_args() |
922 | + |
923 | + if not (options.review_id): |
924 | + parser.error(_("Missing review-id argument")) |
925 | + |
926 | + if options.debug: |
927 | + logging.basicConfig(level=logging.DEBUG) |
928 | + |
929 | + # personality |
930 | + logging.debug("modify_review mode") |
931 | + |
932 | + # initialize and run |
933 | + modify_app = SubmitReviewsApp(datadir=options.datadir, |
934 | + app=None, |
935 | + parent_xid=options.parent_xid, |
936 | + iconname=options.iconname, |
937 | + origin=None, |
938 | + version=None, |
939 | + action="modify", |
940 | + review_id=options.review_id |
941 | + ) |
942 | + |
943 | + modify_app.run() |
944 | + |
945 | + |
946 | # main |
947 | gtk.main() |