14557.1.20
by Curtis Hovey
Updated copyright. |
1 |
# Copyright 2009-2011 Canonical Ltd. This software is licensed under the
|
8687.15.17
by Karl Fogel
Add the copyright header block to the rest of the files under lib/lp/. |
2 |
# GNU Affero General Public License version 3 (see the file LICENSE).
|
5151.1.49
by Mark Shuttleworth
Additional review feedback and test fixes |
3 |
"""Browser code for the Launchpad root page."""
|
4 |
||
5 |
__metaclass__ = type |
|
6 |
__all__ = [ |
|
7 |
'LaunchpadRootIndexView', |
|
6314.4.2
by Curtis Hovey
Added the launchpadSearchView, helper objects and doc test. |
8 |
'LaunchpadSearchView', |
5151.1.49
by Mark Shuttleworth
Additional review feedback and test fixes |
9 |
]
|
10 |
||
7322.2.3
by Curtis Hovey
Added error reporting and a explanation in the page when a GoogleResponseError |
11 |
|
6314.4.2
by Curtis Hovey
Added the launchpadSearchView, helper objects and doc test. |
12 |
import re |
9550.3.1
by Henning Eggers
Implemented 'project of the day' algorithm to select top featured project on home page. Bumped number of featured project rows back up to 10, like it was before. |
13 |
import time |
6314.4.2
by Curtis Hovey
Added the launchpadSearchView, helper objects and doc test. |
14 |
|
11403.1.4
by Henning Eggers
Reformatted imports using format-imports script r32. |
15 |
import feedparser |
16 |
from lazr.batchnavigator.z3batching import batch |
|
14056.1.1
by Steve Kowalik
Deal with unicode errors, and handle ConversionError in +search validation. |
17 |
from zope.app.form.interfaces import ConversionError |
5323.2.1
by Mark Shuttleworth
Manage featured projects in the db |
18 |
from zope.component import getUtility |
13130.1.6
by Curtis Hovey
Move ILaunchpadCelebrity to lp.app. |
19 |
from zope.interface import Interface |
13130.1.12
by Curtis Hovey
Sorted imports. |
20 |
from zope.schema import TextLine |
6409.1.2
by Curtis Hovey
Revisions per review. |
21 |
from zope.schema.interfaces import TooLong |
6314.4.2
by Curtis Hovey
Added the launchpadSearchView, helper objects and doc test. |
22 |
from zope.schema.vocabulary import getVocabularyRegistry |
5323.2.1
by Mark Shuttleworth
Manage featured projects in the db |
23 |
|
14600.1.12
by Curtis Hovey
Move i18n to lp. |
24 |
from lp import _ |
7944.3.18
by Francis J. Lacoste
Rename lp.apps.answers to lp.answers. |
25 |
from lp.answers.interfaces.questioncollection import IQuestionSet |
11929.9.1
by Tim Penhey
Move launchpadform into lp.app.browser. |
26 |
from lp.app.browser.launchpadform import ( |
27 |
action, |
|
28 |
LaunchpadFormView, |
|
29 |
safe_action, |
|
30 |
)
|
|
11270.1.3
by Tim Penhey
Changed NotFoundError imports - gee there were a lot of them. |
31 |
from lp.app.errors import NotFoundError |
13130.1.6
by Curtis Hovey
Move ILaunchpadCelebrity to lp.app. |
32 |
from lp.app.interfaces.launchpad import ILaunchpadCelebrities |
12442.2.9
by j.c.sackett
Ran import reformatter per review. |
33 |
from lp.app.validators.name import sanitize_name |
11138.1.4
by Francis J. Lacoste
Fetch recent blog posts dynamically. |
34 |
from lp.blueprints.interfaces.specification import ISpecificationSet |
11403.1.4
by Henning Eggers
Reformatted imports using format-imports script r32. |
35 |
from lp.bugs.interfaces.bug import IBugSet |
11138.1.4
by Francis J. Lacoste
Fetch recent blog posts dynamically. |
36 |
from lp.code.interfaces.branchcollection import IAllBranches |
37 |
from lp.registry.browser.announcement import HasAnnouncementsView |
|
38 |
from lp.registry.interfaces.person import IPersonSet |
|
39 |
from lp.registry.interfaces.pillar import IPillarNameSet |
|
40 |
from lp.registry.interfaces.product import IProductSet |
|
14606.4.14
by William Grant
More stuff. |
41 |
from lp.services.config import config |
13130.1.12
by Curtis Hovey
Sorted imports. |
42 |
from lp.services.googlesearch.interfaces import ( |
43 |
GoogleResponseError, |
|
44 |
ISearchService, |
|
45 |
)
|
|
11382.6.34
by Gavin Panella
Reformat imports in all files touched so far. |
46 |
from lp.services.propertycache import cachedproperty |
14606.4.14
by William Grant
More stuff. |
47 |
from lp.services.statistics.interfaces.statistic import ILaunchpadStatisticSet |
48 |
from lp.services.timeout import urlfetch |
|
49 |
from lp.services.webapp import LaunchpadView |
|
50 |
from lp.services.webapp.authorization import check_permission |
|
51 |
from lp.services.webapp.batching import BatchNavigator |
|
52 |
from lp.services.webapp.publisher import canonical_url |
|
53 |
from lp.services.webapp.vhosts import allvhosts |
|
11138.1.4
by Francis J. Lacoste
Fetch recent blog posts dynamically. |
54 |
|
7944.3.3
by Francis J. Lacoste
Moved interfaces. |
55 |
|
8310.1.20
by Guilherme Salgado
Remove two more dependencies of lib/lp/* on shipit |
56 |
shipit_faq_url = 'http://www.ubuntu.com/getubuntu/shipit-faq' |
57 |
||
58 |
||
5151.1.49
by Mark Shuttleworth
Additional review feedback and test fixes |
59 |
class LaunchpadRootIndexView(HasAnnouncementsView, LaunchpadView): |
60 |
"""An view for the default view of the LaunchpadRoot."""
|
|
61 |
||
13980.3.10
by Curtis Hovey
Moved launchpad root page_title into view. |
62 |
page_title = 'Launchpad' |
9550.3.1
by Henning Eggers
Implemented 'project of the day' algorithm to select top featured project on home page. Bumped number of featured project rows back up to 10, like it was before. |
63 |
featured_projects = [] |
64 |
featured_projects_top = None |
|
65 |
||
10129.5.5
by Francis J. Lacoste
Don't display the lp-arcana on the front page. |
66 |
# Used by the footer to display the lp-arcana section.
|
67 |
is_root_page = True |
|
68 |
||
9550.3.1
by Henning Eggers
Implemented 'project of the day' algorithm to select top featured project on home page. Bumped number of featured project rows back up to 10, like it was before. |
69 |
@staticmethod
|
70 |
def _get_day_of_year(): |
|
71 |
"""Calculate the number of the current day.
|
|
72 |
||
73 |
This method gets overridden in tests to make the selection of the
|
|
74 |
top featured project deterministic.
|
|
75 |
"""
|
|
76 |
return time.gmtime()[7] |
|
77 |
||
78 |
def initialize(self): |
|
79 |
"""Set up featured projects list and the top featured project."""
|
|
80 |
super(LaunchpadRootIndexView, self).initialize() |
|
81 |
# The maximum number of projects to be displayed as defined by the
|
|
9874.2.5
by Curtis Hovey
Removed unused var. |
82 |
# number of items plus one top featured project.
|
9550.3.1
by Henning Eggers
Implemented 'project of the day' algorithm to select top featured project on home page. Bumped number of featured project rows back up to 10, like it was before. |
83 |
self.featured_projects = list( |
9826.10.2
by Henning Eggers
Fixed the bug. |
84 |
getUtility(IPillarNameSet).featured_projects) |
9874.2.1
by Curtis Hovey
Fixed OOP that could occur if all the featured projects were removed. |
85 |
self._setFeaturedProjectsTop() |
86 |
||
87 |
def _setFeaturedProjectsTop(self): |
|
88 |
"""Set the top featured project and remove it from the list."""
|
|
89 |
project_count = len(self.featured_projects) |
|
90 |
if project_count > 0: |
|
91 |
top_project = self._get_day_of_year() % project_count |
|
92 |
self.featured_projects_top = self.featured_projects.pop( |
|
93 |
top_project) |
|
6721.2.9
by Christian Reis
Put featured projects in two columns, and fix the display of the search bar, which I finger-fumbled when fixing the search target. |
94 |
|
9481.3.3
by Henning Eggers
Logged-in check, apphomes, featured project, page width. |
95 |
@cachedproperty
|
96 |
def apphomes(self): |
|
97 |
return { |
|
98 |
'answers': canonical_url(self.context, rootsite='answers'), |
|
99 |
'blueprints': canonical_url(self.context, rootsite='blueprints'), |
|
100 |
'bugs': canonical_url(self.context, rootsite='bugs'), |
|
101 |
'code': canonical_url(self.context, rootsite='code'), |
|
102 |
'translations': canonical_url(self.context, |
|
103 |
rootsite='translations'), |
|
104 |
'ubuntu': canonical_url( |
|
105 |
getUtility(ILaunchpadCelebrities).ubuntu), |
|
106 |
}
|
|
107 |
||
108 |
@property
|
|
7334.6.5
by Martin Albisetti
Add bug and branch stats |
109 |
def branch_count(self): |
8028.1.2
by Jonathan Lange
Remove IBranchSet.count, change IAllBranches.count invocations to count |
110 |
"""The total branch count of public branches in all of Launchpad."""
|
111 |
return getUtility(IAllBranches).visibleByUser(None).count() |
|
7334.6.5
by Martin Albisetti
Add bug and branch stats |
112 |
|
113 |
@property
|
|
114 |
def bug_count(self): |
|
7334.6.21
by Martin Albisetti
* Tweak docstrings |
115 |
"""The total bug count in all of Launchpad."""
|
7334.6.5
by Martin Albisetti
Add bug and branch stats |
116 |
return getUtility(ILaunchpadStatisticSet).value('bug_count') |
117 |
||
7334.6.7
by Martin Albisetti
Added count for projects, UI tweaks |
118 |
@property
|
119 |
def project_count(self): |
|
7334.6.21
by Martin Albisetti
* Tweak docstrings |
120 |
"""The total project count in all of Launchpad."""
|
7334.6.7
by Martin Albisetti
Added count for projects, UI tweaks |
121 |
return getUtility(IProductSet).count_all() |
122 |
||
7334.6.8
by Martin Albisetti
* Add statistics |
123 |
@property
|
124 |
def translation_count(self): |
|
7334.6.21
by Martin Albisetti
* Tweak docstrings |
125 |
"""The total count of translatable strings in all of Launchpad """
|
7334.6.8
by Martin Albisetti
* Add statistics |
126 |
return getUtility(ILaunchpadStatisticSet).value('pomsgid_count') |
127 |
||
128 |
@property
|
|
129 |
def blueprint_count(self): |
|
7334.6.21
by Martin Albisetti
* Tweak docstrings |
130 |
"""The total blueprint count in all of Launchpad."""
|
7334.6.8
by Martin Albisetti
* Add statistics |
131 |
return getUtility(ISpecificationSet).specification_count |
132 |
||
133 |
@property
|
|
134 |
def answer_count(self): |
|
7334.6.21
by Martin Albisetti
* Tweak docstrings |
135 |
"""The total blueprint count in all of Launchpad."""
|
7334.6.8
by Martin Albisetti
* Add statistics |
136 |
return getUtility(ILaunchpadStatisticSet).value('question_count') |
6721.2.9
by Christian Reis
Put featured projects in two columns, and fix the display of the search bar, which I finger-fumbled when fixing the search target. |
137 |
|
11138.1.4
by Francis J. Lacoste
Fetch recent blog posts dynamically. |
138 |
def getRecentBlogPosts(self): |
139 |
"""Return the parsed feed of the most recent blog posts.
|
|
140 |
||
141 |
It returns a list of dict with keys title, description, link and date.
|
|
142 |
||
143 |
The date is formatted and the description which may contain HTML is
|
|
144 |
sanitized.
|
|
145 |
||
146 |
The number of blog posts to display is controlled through
|
|
147 |
launchpad.homepage_recent_posts_count. The posts are fetched
|
|
148 |
from the feed specified in launchpad.homepage_recent_posts_feed.
|
|
149 |
||
150 |
Since the feed is parsed everytime, the template should cache this
|
|
151 |
through memcached.
|
|
152 |
||
153 |
FeedParser takes care of sanitizing the HTML contained in the feed.
|
|
154 |
"""
|
|
155 |
# Use urlfetch which supports timeout
|
|
11138.1.5
by Francis J. Lacoste
Don't OOPS on IOError. |
156 |
try: |
157 |
data = urlfetch(config.launchpad.homepage_recent_posts_feed) |
|
158 |
except IOError: |
|
159 |
return [] |
|
11138.1.4
by Francis J. Lacoste
Fetch recent blog posts dynamically. |
160 |
feed = feedparser.parse(data) |
161 |
posts = [] |
|
162 |
max_count = config.launchpad.homepage_recent_posts_count |
|
163 |
# FeedParser takes care of HTML sanitisation.
|
|
164 |
for entry in feed.entries[:max_count]: |
|
165 |
posts.append({ |
|
166 |
'title': entry.title, |
|
167 |
'description': entry.description, |
|
168 |
'link': entry.link, |
|
169 |
'date': time.strftime('%d %b %Y', entry.updated_parsed), |
|
170 |
})
|
|
171 |
return posts |
|
172 |
||
173 |
||
6421.3.4
by Curtis Hovey
Fixes per review. The shipit rules may need additional changes. |
174 |
class LaunchpadSearchFormView(LaunchpadView): |
175 |
"""A view to display the global search form in any page."""
|
|
176 |
id_suffix = '-secondary' |
|
177 |
text = None |
|
178 |
focusedElementScript = None |
|
179 |
form_wide_errors = None |
|
180 |
errors = None |
|
181 |
error_count = None |
|
182 |
error = None |
|
183 |
error_class = None |
|
184 |
||
185 |
@property
|
|
186 |
def rooturl(self): |
|
187 |
"""Return the site's root url."""
|
|
188 |
return allvhosts.configs['mainsite'].rooturl |
|
189 |
||
190 |
||
191 |
class LaunchpadPrimarySearchFormView(LaunchpadSearchFormView): |
|
192 |
"""A view to display the global search form in the page."""
|
|
193 |
id_suffix = '' |
|
194 |
||
195 |
@property
|
|
196 |
def text(self): |
|
197 |
"""The search text submitted to the context view."""
|
|
198 |
return self.context.text |
|
199 |
||
200 |
@property
|
|
201 |
def focusedElementScript(self): |
|
202 |
"""The context view's focusedElementScript."""
|
|
203 |
return self.context.focusedElementScript |
|
204 |
||
205 |
@property
|
|
206 |
def form_wide_errors(self): |
|
207 |
"""The context view's form_wide_errors."""
|
|
208 |
return self.context.form_wide_errors |
|
209 |
||
210 |
@property
|
|
211 |
def errors(self): |
|
212 |
"""The context view's errors."""
|
|
213 |
return self.context.errors |
|
214 |
||
215 |
@property
|
|
216 |
def error_count(self): |
|
217 |
"""The context view's error_count."""
|
|
218 |
return self.context.error_count |
|
219 |
||
220 |
@property
|
|
221 |
def error(self): |
|
222 |
"""The context view's text field error."""
|
|
223 |
return self.context.getFieldError('text') |
|
224 |
||
225 |
@property
|
|
226 |
def error_class(self): |
|
227 |
"""Return the 'error' if there is an error, or None."""
|
|
228 |
if self.error: |
|
229 |
return 'error' |
|
230 |
return None |
|
231 |
||
232 |
||
13130.1.6
by Curtis Hovey
Move ILaunchpadCelebrity to lp.app. |
233 |
class ILaunchpadSearch(Interface): |
234 |
"""The Schema for performing searches across all Launchpad."""
|
|
235 |
||
236 |
text = TextLine( |
|
237 |
title=_('Search text'), required=False, max_length=250) |
|
238 |
||
239 |
||
6314.4.2
by Curtis Hovey
Added the launchpadSearchView, helper objects and doc test. |
240 |
class LaunchpadSearchView(LaunchpadFormView): |
241 |
"""A view to search for Launchpad pages and objects."""
|
|
242 |
schema = ILaunchpadSearch |
|
6342.1.7
by Curtis Hovey
Switched back to the original search field name to avoid conflicts with other forms. |
243 |
field_names = ['text'] |
6314.4.2
by Curtis Hovey
Added the launchpadSearchView, helper objects and doc test. |
244 |
|
6421.3.3
by Curtis Hovey
Search layout and behaviour fixes from bugs 237351, 235513, 235503, 236290. |
245 |
shipit_keywords = set([ |
246 |
'ubuntu', 'kubuntu', 'edubuntu', |
|
6421.3.4
by Curtis Hovey
Fixes per review. The shipit rules may need additional changes. |
247 |
'ship', 'shipit', 'send', 'get', 'mail', 'free', |
6421.3.3
by Curtis Hovey
Search layout and behaviour fixes from bugs 237351, 235513, 235503, 236290. |
248 |
'cd', 'cds', 'dvd', 'dvds', 'disc']) |
6421.3.5
by Curtis Hovey
Added shipit_anit_keywords to reduce false matches to shipit. |
249 |
shipit_anti_keywords = set([ |
250 |
'burn', 'burning', 'enable', 'error', 'errors', 'image', 'iso', |
|
251 |
'read', 'rip', 'write']) |
|
6421.3.3
by Curtis Hovey
Search layout and behaviour fixes from bugs 237351, 235513, 235503, 236290. |
252 |
|
6314.4.2
by Curtis Hovey
Added the launchpadSearchView, helper objects and doc test. |
253 |
def __init__(self, context, request): |
254 |
"""Initialize the view.
|
|
255 |
||
256 |
Set the state of the search_params and matches.
|
|
257 |
"""
|
|
258 |
super(LaunchpadSearchView, self).__init__(context, request) |
|
7322.2.8
by Curtis Hovey
Revisions per review. |
259 |
self.has_page_service = True |
6314.4.2
by Curtis Hovey
Added the launchpadSearchView, helper objects and doc test. |
260 |
self._bug = None |
261 |
self._question = None |
|
262 |
self._person_or_team = None |
|
263 |
self._pillar = None |
|
264 |
self._pages = None |
|
265 |
self.search_params = self._getDefaultSearchParams() |
|
266 |
# The Search Action should always run.
|
|
267 |
self.request.form['field.actions.search'] = 'Search' |
|
268 |
||
269 |
def _getDefaultSearchParams(self): |
|
270 |
"""Return a dict of the search param set to their default state."""
|
|
271 |
return { |
|
6342.1.7
by Curtis Hovey
Switched back to the original search field name to avoid conflicts with other forms. |
272 |
'text': None, |
6314.4.3
by Curtis Hovey
Changes per review. I removed 4 functions, and mad a crucial revision the |
273 |
'start': 0, |
6314.4.2
by Curtis Hovey
Added the launchpadSearchView, helper objects and doc test. |
274 |
}
|
275 |
||
6314.4.3
by Curtis Hovey
Changes per review. I removed 4 functions, and mad a crucial revision the |
276 |
def _updateSearchParams(self): |
277 |
"""Sanitize the search_params and add the BatchNavigator params."""
|
|
6342.1.7
by Curtis Hovey
Switched back to the original search field name to avoid conflicts with other forms. |
278 |
if self.search_params['text'] is not None: |
279 |
text = self.search_params['text'].strip() |
|
280 |
if text == '': |
|
281 |
self.search_params['text'] = None |
|
6314.4.3
by Curtis Hovey
Changes per review. I removed 4 functions, and mad a crucial revision the |
282 |
else: |
6342.1.7
by Curtis Hovey
Switched back to the original search field name to avoid conflicts with other forms. |
283 |
self.search_params['text'] = text |
6314.4.2
by Curtis Hovey
Added the launchpadSearchView, helper objects and doc test. |
284 |
request_start = self.request.get('start', self.search_params['start']) |
285 |
try: |
|
286 |
start = int(request_start) |
|
287 |
except (ValueError, TypeError): |
|
288 |
return
|
|
289 |
self.search_params['start'] = start |
|
290 |
||
291 |
@property
|
|
6342.1.7
by Curtis Hovey
Switched back to the original search field name to avoid conflicts with other forms. |
292 |
def text(self): |
293 |
"""Return the text or None."""
|
|
294 |
return self.search_params['text'] |
|
6330.2.17
by Maris Fogels
Added Curtis' patch that adds more search view properties and sets the page title. |
295 |
|
296 |
@property
|
|
297 |
def start(self): |
|
298 |
"""Return the start index of the batch."""
|
|
299 |
return self.search_params['start'] |
|
300 |
||
301 |
@property
|
|
302 |
def page_title(self): |
|
303 |
"""Page title."""
|
|
304 |
return self.page_heading |
|
305 |
||
306 |
@property
|
|
307 |
def page_heading(self): |
|
308 |
"""Heading to display above the search results."""
|
|
6342.1.7
by Curtis Hovey
Switched back to the original search field name to avoid conflicts with other forms. |
309 |
if self.text is None: |
6330.2.17
by Maris Fogels
Added Curtis' patch that adds more search view properties and sets the page title. |
310 |
return 'Search Launchpad' |
311 |
else: |
|
6342.1.7
by Curtis Hovey
Switched back to the original search field name to avoid conflicts with other forms. |
312 |
return 'Pages matching "%s" in Launchpad' % self.text |
6330.2.17
by Maris Fogels
Added Curtis' patch that adds more search view properties and sets the page title. |
313 |
|
314 |
@property
|
|
6421.3.3
by Curtis Hovey
Search layout and behaviour fixes from bugs 237351, 235513, 235503, 236290. |
315 |
def batch_heading(self): |
316 |
"""Heading to display in the batch navigation."""
|
|
317 |
if self.has_exact_matches: |
|
318 |
return ('other page matching "%s"' % self.text, |
|
319 |
'other pages matching "%s"' % self.text) |
|
320 |
else: |
|
321 |
return ('page matching "%s"' % self.text, |
|
322 |
'pages matching "%s"' % self.text) |
|
323 |
||
324 |
@property
|
|
6409.1.1
by Curtis Hovey
Global search form fixes. |
325 |
def focusedElementScript(self): |
326 |
"""Focus the first widget when there are no matches."""
|
|
327 |
if self.has_matches: |
|
6409.1.3
by Curtis Hovey
Corrected to logic to match to documentation. |
328 |
return None |
329 |
return super(LaunchpadSearchView, self).focusedElementScript() |
|
6409.1.1
by Curtis Hovey
Global search form fixes. |
330 |
|
331 |
@property
|
|
6314.4.2
by Curtis Hovey
Added the launchpadSearchView, helper objects and doc test. |
332 |
def bug(self): |
333 |
"""Return the bug that matched the terms, or None."""
|
|
334 |
return self._bug |
|
335 |
||
336 |
@property
|
|
337 |
def question(self): |
|
338 |
"""Return the question that matched the terms, or None."""
|
|
339 |
return self._question |
|
340 |
||
341 |
@property
|
|
342 |
def pillar(self): |
|
343 |
"""Return the project that matched the terms, or None."""
|
|
344 |
return self._pillar |
|
345 |
||
346 |
@property
|
|
347 |
def person_or_team(self): |
|
348 |
"""Return the person or team that matched the terms, or None."""
|
|
349 |
return self._person_or_team |
|
350 |
||
351 |
@property
|
|
352 |
def pages(self): |
|
353 |
"""Return the pages that matched the terms, or None."""
|
|
354 |
return self._pages |
|
355 |
||
356 |
@property
|
|
6421.3.3
by Curtis Hovey
Search layout and behaviour fixes from bugs 237351, 235513, 235503, 236290. |
357 |
def has_shipit(self): |
358 |
"""Return True is the search text contains shipit keywords."""
|
|
359 |
if self.text is None: |
|
360 |
return False |
|
361 |
terms = set(self.text.lower().split()) |
|
6421.3.5
by Curtis Hovey
Added shipit_anit_keywords to reduce false matches to shipit. |
362 |
anti_matches = self.shipit_anti_keywords.intersection(terms) |
363 |
if len(anti_matches) >= 1: |
|
364 |
return False |
|
6421.3.3
by Curtis Hovey
Search layout and behaviour fixes from bugs 237351, 235513, 235503, 236290. |
365 |
matches = self.shipit_keywords.intersection(terms) |
366 |
return len(matches) >= 2 |
|
367 |
||
368 |
@property
|
|
6330.2.30
by Maris Fogels
Applied Curtis' patch that fixes the page styling and markup. |
369 |
def has_exact_matches(self): |
370 |
"""Return True if something exactly matched the search terms."""
|
|
6421.3.3
by Curtis Hovey
Search layout and behaviour fixes from bugs 237351, 235513, 235503, 236290. |
371 |
kinds = (self.bug, self.question, self.pillar, |
372 |
self.person_or_team, self.has_shipit) |
|
6330.2.30
by Maris Fogels
Applied Curtis' patch that fixes the page styling and markup. |
373 |
return self.containsMatchingKind(kinds) |
374 |
||
375 |
@property
|
|
6421.3.4
by Curtis Hovey
Fixes per review. The shipit rules may need additional changes. |
376 |
def shipit_faq_url(self): |
377 |
"""The shipit FAQ URL."""
|
|
8310.1.20
by Guilherme Salgado
Remove two more dependencies of lib/lp/* on shipit |
378 |
return shipit_faq_url |
6421.3.4
by Curtis Hovey
Fixes per review. The shipit rules may need additional changes. |
379 |
|
380 |
@property
|
|
6314.4.2
by Curtis Hovey
Added the launchpadSearchView, helper objects and doc test. |
381 |
def has_matches(self): |
382 |
"""Return True if something matched the search terms, or False."""
|
|
383 |
kinds = (self.bug, self.question, self.pillar, |
|
6421.3.3
by Curtis Hovey
Search layout and behaviour fixes from bugs 237351, 235513, 235503, 236290. |
384 |
self.person_or_team, self.has_shipit, self.pages) |
6330.2.30
by Maris Fogels
Applied Curtis' patch that fixes the page styling and markup. |
385 |
return self.containsMatchingKind(kinds) |
386 |
||
7322.2.8
by Curtis Hovey
Revisions per review. |
387 |
@property
|
388 |
def url(self): |
|
389 |
"""Return the requested URL."""
|
|
390 |
if 'QUERY_STRING' in self.request: |
|
391 |
query_string = self.request['QUERY_STRING'] |
|
392 |
else: |
|
393 |
query_string = '' |
|
394 |
return self.request.getURL() + '?' + query_string |
|
395 |
||
6330.2.30
by Maris Fogels
Applied Curtis' patch that fixes the page styling and markup. |
396 |
def containsMatchingKind(self, kinds): |
397 |
"""Return True if one of the items in kinds is not None, or False."""
|
|
6314.4.2
by Curtis Hovey
Added the launchpadSearchView, helper objects and doc test. |
398 |
for kind in kinds: |
6421.3.3
by Curtis Hovey
Search layout and behaviour fixes from bugs 237351, 235513, 235503, 236290. |
399 |
if kind is not None and kind is not False: |
6314.4.2
by Curtis Hovey
Added the launchpadSearchView, helper objects and doc test. |
400 |
return True |
6330.2.39
by Maris Fogels
Fixed a bug in the SearchView.has_exact_matches code, and fixed its associated doctest. |
401 |
return False |
6314.4.2
by Curtis Hovey
Added the launchpadSearchView, helper objects and doc test. |
402 |
|
6409.1.1
by Curtis Hovey
Global search form fixes. |
403 |
def validate(self, data): |
404 |
"""See `LaunchpadFormView`"""
|
|
405 |
errors = list(self.errors) |
|
406 |
for error in errors: |
|
14056.1.1
by Steve Kowalik
Deal with unicode errors, and handle ConversionError in +search validation. |
407 |
if isinstance(error, ConversionError): |
408 |
self.setFieldError( |
|
409 |
'text', 'Can not convert your search term.') |
|
410 |
elif isinstance(error, unicode): |
|
411 |
continue
|
|
412 |
elif (error.field_name == 'text' |
|
6409.1.2
by Curtis Hovey
Revisions per review. |
413 |
and isinstance(error.errors, TooLong)): |
6409.1.1
by Curtis Hovey
Global search form fixes. |
414 |
self.setFieldError( |
6409.1.2
by Curtis Hovey
Revisions per review. |
415 |
'text', 'The search text cannot exceed 250 characters.') |
6409.1.1
by Curtis Hovey
Global search form fixes. |
416 |
|
6314.4.2
by Curtis Hovey
Added the launchpadSearchView, helper objects and doc test. |
417 |
@safe_action
|
418 |
@action(u'Search', name='search') |
|
419 |
def search_action(self, action, data): |
|
420 |
"""The Action executed when the user uses the search button.
|
|
421 |
||
422 |
Saves the user submitted search parameters in an instance
|
|
423 |
attribute.
|
|
424 |
"""
|
|
425 |
self.search_params.update(**data) |
|
6314.4.3
by Curtis Hovey
Changes per review. I removed 4 functions, and mad a crucial revision the |
426 |
self._updateSearchParams() |
6342.1.7
by Curtis Hovey
Switched back to the original search field name to avoid conflicts with other forms. |
427 |
if self.text is None: |
6314.4.2
by Curtis Hovey
Added the launchpadSearchView, helper objects and doc test. |
428 |
return
|
429 |
||
6330.2.17
by Maris Fogels
Added Curtis' patch that adds more search view properties and sets the page title. |
430 |
if self.start == 0: |
6342.1.7
by Curtis Hovey
Switched back to the original search field name to avoid conflicts with other forms. |
431 |
numeric_token = self._getNumericToken(self.text) |
6330.2.17
by Maris Fogels
Added Curtis' patch that adds more search view properties and sets the page title. |
432 |
if numeric_token is not None: |
6330.2.25
by Maris Fogels
Fixed a problem where searching for a non-existant bug would cause an OOPS. |
433 |
try: |
6882.1.1
by Curtis Hovey
Added a guard to LaunchpadSearchView ensure not to show private bugs to |
434 |
bug = getUtility(IBugSet).get(numeric_token) |
435 |
if check_permission("launchpad.View", bug): |
|
436 |
self._bug = bug |
|
6330.2.25
by Maris Fogels
Fixed a problem where searching for a non-existant bug would cause an OOPS. |
437 |
except NotFoundError: |
6882.1.2
by Curtis Hovey
revised the LaunchpadSearchView exception to use pass instead of setting |
438 |
# Let self._bug remain None.
|
439 |
pass
|
|
6330.2.17
by Maris Fogels
Added Curtis' patch that adds more search view properties and sets the page title. |
440 |
self._question = getUtility(IQuestionSet).get(numeric_token) |
441 |
||
6342.1.7
by Curtis Hovey
Switched back to the original search field name to avoid conflicts with other forms. |
442 |
name_token = self._getNameToken(self.text) |
6330.2.17
by Maris Fogels
Added Curtis' patch that adds more search view properties and sets the page title. |
443 |
if name_token is not None: |
6421.3.3
by Curtis Hovey
Search layout and behaviour fixes from bugs 237351, 235513, 235503, 236290. |
444 |
self._person_or_team = self._getPersonOrTeam(name_token) |
10724.1.6
by Henning Eggers
Vocabulary names. |
445 |
self._pillar = self._getDistributionOrProductOrProjectGroup( |
6330.2.17
by Maris Fogels
Added Curtis' patch that adds more search view properties and sets the page title. |
446 |
name_token) |
447 |
||
6342.1.7
by Curtis Hovey
Switched back to the original search field name to avoid conflicts with other forms. |
448 |
self._pages = self.searchPages(self.text, start=self.start) |
6314.4.3
by Curtis Hovey
Changes per review. I removed 4 functions, and mad a crucial revision the |
449 |
|
6342.1.7
by Curtis Hovey
Switched back to the original search field name to avoid conflicts with other forms. |
450 |
def _getNumericToken(self, text): |
6314.4.2
by Curtis Hovey
Added the launchpadSearchView, helper objects and doc test. |
451 |
"""Return the first group of numbers in the search text, or None."""
|
452 |
numeric_pattern = re.compile(r'(\d+)') |
|
6342.1.7
by Curtis Hovey
Switched back to the original search field name to avoid conflicts with other forms. |
453 |
match = numeric_pattern.search(text) |
6314.4.2
by Curtis Hovey
Added the launchpadSearchView, helper objects and doc test. |
454 |
if match is None: |
455 |
return None |
|
456 |
return match.group(1) |
|
457 |
||
6342.1.7
by Curtis Hovey
Switched back to the original search field name to avoid conflicts with other forms. |
458 |
def _getNameToken(self, text): |
6314.4.2
by Curtis Hovey
Added the launchpadSearchView, helper objects and doc test. |
459 |
"""Return the search text as a Launchpad name.
|
460 |
||
6314.4.3
by Curtis Hovey
Changes per review. I removed 4 functions, and mad a crucial revision the |
461 |
Launchpad names may contain ^[a-z0-9][a-z0-9\+\.\-]+$.
|
462 |
See `valid_name_pattern`.
|
|
6314.4.2
by Curtis Hovey
Added the launchpadSearchView, helper objects and doc test. |
463 |
"""
|
6314.4.3
by Curtis Hovey
Changes per review. I removed 4 functions, and mad a crucial revision the |
464 |
hypen_pattern = re.compile(r'[ _]') |
6342.1.7
by Curtis Hovey
Switched back to the original search field name to avoid conflicts with other forms. |
465 |
name = hypen_pattern.sub('-', text.strip().lower()) |
6314.4.3
by Curtis Hovey
Changes per review. I removed 4 functions, and mad a crucial revision the |
466 |
return sanitize_name(name) |
6314.4.2
by Curtis Hovey
Added the launchpadSearchView, helper objects and doc test. |
467 |
|
6421.3.3
by Curtis Hovey
Search layout and behaviour fixes from bugs 237351, 235513, 235503, 236290. |
468 |
def _getPersonOrTeam(self, name): |
469 |
"""Return the matching active person or team."""
|
|
470 |
person_or_team = getUtility(IPersonSet).getByName(name) |
|
471 |
if (person_or_team is not None |
|
8585.1.2
by Brad Crittenden
Do not show private teams to unauthorized users if the team is an exact match in site search. |
472 |
and person_or_team.is_valid_person_or_team |
473 |
and check_permission('launchpad.View', person_or_team)): |
|
6421.3.3
by Curtis Hovey
Search layout and behaviour fixes from bugs 237351, 235513, 235503, 236290. |
474 |
return person_or_team |
475 |
return None |
|
476 |
||
10724.1.6
by Henning Eggers
Vocabulary names. |
477 |
def _getDistributionOrProductOrProjectGroup(self, name): |
6314.4.2
by Curtis Hovey
Added the launchpadSearchView, helper objects and doc test. |
478 |
"""Return the matching distribution, product or project, or None."""
|
479 |
vocabulary_registry = getVocabularyRegistry() |
|
480 |
vocab = vocabulary_registry.get( |
|
10724.1.6
by Henning Eggers
Vocabulary names. |
481 |
None, 'DistributionOrProductOrProjectGroup') |
6314.4.2
by Curtis Hovey
Added the launchpadSearchView, helper objects and doc test. |
482 |
try: |
483 |
return vocab.getTermByToken(name).value |
|
484 |
except LookupError: |
|
485 |
return None |
|
486 |
||
487 |
def searchPages(self, query_terms, start=0): |
|
6314.4.3
by Curtis Hovey
Changes per review. I removed 4 functions, and mad a crucial revision the |
488 |
"""Return the up to 20 pages that match the query_terms, or None.
|
6314.4.2
by Curtis Hovey
Added the launchpadSearchView, helper objects and doc test. |
489 |
|
490 |
:param query_terms: The unescaped terms to query Google.
|
|
6314.4.3
by Curtis Hovey
Changes per review. I removed 4 functions, and mad a crucial revision the |
491 |
:param start: The index of the page that starts the set of pages.
|
492 |
:return: A GooglBatchNavigator or None.
|
|
6314.4.2
by Curtis Hovey
Added the launchpadSearchView, helper objects and doc test. |
493 |
"""
|
9874.2.1
by Curtis Hovey
Fixed OOP that could occur if all the featured projects were removed. |
494 |
if query_terms in [None, '']: |
6314.4.2
by Curtis Hovey
Added the launchpadSearchView, helper objects and doc test. |
495 |
return None |
496 |
google_search = getUtility(ISearchService) |
|
7322.2.1
by Curtis Hovey
Added tGoogleResponseError and caught the exception in the view. Add |
497 |
try: |
7322.2.3
by Curtis Hovey
Added error reporting and a explanation in the page when a GoogleResponseError |
498 |
page_matches = google_search.search( |
499 |
terms=query_terms, start=start) |
|
7322.2.1
by Curtis Hovey
Added tGoogleResponseError and caught the exception in the view. Add |
500 |
except GoogleResponseError: |
12528.2.3
by Curtis Hovey
Removed the code that reports an oops for GoogleResponseError because it never |
501 |
# There was a connectivity or Google service issue that means
|
502 |
# there is no data available at this moment.
|
|
7322.2.8
by Curtis Hovey
Revisions per review. |
503 |
self.has_page_service = False |
7322.2.1
by Curtis Hovey
Added tGoogleResponseError and caught the exception in the view. Add |
504 |
return None |
7322.2.5
by Curtis Hovey
Revised the no parsed matches rule only raise an error when there are more than 20 |
505 |
if len(page_matches) == 0: |
6314.4.2
by Curtis Hovey
Added the launchpadSearchView, helper objects and doc test. |
506 |
return None |
6421.3.3
by Curtis Hovey
Search layout and behaviour fixes from bugs 237351, 235513, 235503, 236290. |
507 |
navigator = GoogleBatchNavigator( |
508 |
page_matches, self.request, start=start) |
|
509 |
navigator.setHeadings(*self.batch_heading) |
|
510 |
return navigator |
|
6314.4.2
by Curtis Hovey
Added the launchpadSearchView, helper objects and doc test. |
511 |
|
512 |
||
513 |
class WindowedList: |
|
6314.4.3
by Curtis Hovey
Changes per review. I removed 4 functions, and mad a crucial revision the |
514 |
"""A list that contains a subset of items (a window) of a virtual list."""
|
6314.4.2
by Curtis Hovey
Added the launchpadSearchView, helper objects and doc test. |
515 |
|
516 |
def __init__(self, window, start, total): |
|
517 |
"""Create a WindowedList from a smaller list.
|
|
518 |
||
519 |
:param window: The list with real items.
|
|
520 |
:param start: An int, the list's starting index in the virtual list.
|
|
6314.4.3
by Curtis Hovey
Changes per review. I removed 4 functions, and mad a crucial revision the |
521 |
:param total: An int, the total number of items in the virtual list.
|
6314.4.2
by Curtis Hovey
Added the launchpadSearchView, helper objects and doc test. |
522 |
"""
|
523 |
self._window = window |
|
524 |
self._start = start |
|
525 |
self._total = total |
|
526 |
self._end = start + len(window) |
|
527 |
||
528 |
def __len__(self): |
|
529 |
"""Return the length of the virtual list."""
|
|
530 |
return self._total |
|
531 |
||
532 |
def __getitem__(self, key): |
|
6314.4.3
by Curtis Hovey
Changes per review. I removed 4 functions, and mad a crucial revision the |
533 |
"""Return the key item or None if key belongs to the virtual list."""
|
6314.4.2
by Curtis Hovey
Added the launchpadSearchView, helper objects and doc test. |
534 |
# When the key is a slice, return a list of items.
|
535 |
if isinstance(key, (tuple, slice)): |
|
536 |
if isinstance(key, (slice)): |
|
537 |
indices = key.indices(len(self)) |
|
538 |
else: |
|
539 |
indices = key |
|
540 |
return [self[index] for index in range(*indices)] |
|
6314.4.3
by Curtis Hovey
Changes per review. I removed 4 functions, and mad a crucial revision the |
541 |
# If the index belongs to the window return a real item.
|
6314.4.2
by Curtis Hovey
Added the launchpadSearchView, helper objects and doc test. |
542 |
if key >= self._start and key < self._end: |
543 |
window_index = key - self._start |
|
544 |
return self._window[window_index] |
|
545 |
# Otherwise the index belongs to the virtual list.
|
|
546 |
return None |
|
547 |
||
548 |
def __iter__(self): |
|
549 |
"""Yield each item, or None if the index is virtual."""
|
|
550 |
for index in range(0, self._total): |
|
551 |
yield self[index] |
|
552 |
||
553 |
||
7872.3.1
by Gary Poster
move to lazr.batchnavigator |
554 |
class WindowedListBatch(batch._Batch): |
6375.1.2
by Curtis Hovey
Added code to fix the case where a results has no title because Google indexed a bad Launchpad page. |
555 |
"""A batch class that does not include None objects when iterating."""
|
556 |
||
557 |
def __iter__(self): |
|
558 |
"""Iterate over objects that are not None."""
|
|
559 |
for item in super(WindowedListBatch, self).__iter__(): |
|
560 |
if item is not None: |
|
561 |
# Never yield None
|
|
562 |
yield item |
|
563 |
||
564 |
def endNumber(self): |
|
565 |
"""Return the end index of the batch, not including None objects."""
|
|
6421.3.4
by Curtis Hovey
Fixes per review. The shipit rules may need additional changes. |
566 |
# This class should know about the private _window attribute.
|
567 |
# pylint: disable-msg=W0212
|
|
6375.1.2
by Curtis Hovey
Added code to fix the case where a results has no title because Google indexed a bad Launchpad page. |
568 |
return self.start + len(self.list._window) |
569 |
||
570 |
||
6314.4.2
by Curtis Hovey
Added the launchpadSearchView, helper objects and doc test. |
571 |
class GoogleBatchNavigator(BatchNavigator): |
572 |
"""A batch navigator with a fixed size of 20 items per batch."""
|
|
573 |
||
13404.2.1
by Robert Collins
Merge 13405 cancelling the reversion. |
574 |
_batch_factory = WindowedListBatch |
6330.2.28
by Maris Fogels
Removed the 'Last' link from the search results page. |
575 |
# Searches generally don't show the 'Last' link when there is a
|
576 |
# good chance of getting over 100,000 results.
|
|
6330.2.30
by Maris Fogels
Applied Curtis' patch that fixes the page styling and markup. |
577 |
show_last_link = False |
6330.2.28
by Maris Fogels
Removed the 'Last' link from the search results page. |
578 |
|
6421.3.3
by Curtis Hovey
Search layout and behaviour fixes from bugs 237351, 235513, 235503, 236290. |
579 |
singular_heading = 'page' |
580 |
plural_heading = 'pages' |
|
581 |
||
13404.2.1
by Robert Collins
Merge 13405 cancelling the reversion. |
582 |
def __init__(self, results, request, start=0, size=20, callback=None, |
583 |
transient_parameters=None, force_start=False, |
|
584 |
range_factory=None): |
|
12757.2.6
by Robert Collins
Fix test failures - restore the adapting to WindowedList of GooglePageMatches. |
585 |
"""See `BatchNavigator`.
|
586 |
||
587 |
:param results: A `PageMatches` object that contains the matching
|
|
588 |
pages to iterate over.
|
|
589 |
:param request: An `IBrowserRequest` that contains the form
|
|
590 |
parameters.
|
|
591 |
:param start: an int that represents the start of the current batch.
|
|
592 |
:param size: The batch size is fixed to 20, The param is not used.
|
|
593 |
:param callback: Not used.
|
|
594 |
"""
|
|
595 |
results = WindowedList(results, start, results.total) |
|
13404.2.1
by Robert Collins
Merge 13405 cancelling the reversion. |
596 |
super(GoogleBatchNavigator, self).__init__(results, request, |
597 |
start=start, size=size, callback=callback, |
|
13980.3.10
by Curtis Hovey
Moved launchpad root page_title into view. |
598 |
transient_parameters=transient_parameters, |
599 |
force_start=force_start, range_factory=range_factory) |
|
12757.2.6
by Robert Collins
Fix test failures - restore the adapting to WindowedList of GooglePageMatches. |
600 |
|
13404.2.1
by Robert Collins
Merge 13405 cancelling the reversion. |
601 |
def determineSize(self, size, batch_params_source): |
602 |
# Force the default and users requested sizes to 20.
|
|
6314.4.2
by Curtis Hovey
Added the launchpadSearchView, helper objects and doc test. |
603 |
self.default_size = 20 |
13404.2.1
by Robert Collins
Merge 13405 cancelling the reversion. |
604 |
return 20 |