~launchpad-pqm/launchpad/devel

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