~launchpad-pqm/launchpad/devel

« back to all changes in this revision

Viewing changes to lib/lp/bugs/browser/bugtask.py

  • Committer: Launchpad Patch Queue Manager
  • Date: 2011-12-09 09:23:38 UTC
  • mfrom: (14333.2.13 history-model)
  • Revision ID: launchpad@pqm.canonical.com-20111209092338-se7u5l0skqzaes1v
[r=jcsackett][bug=295214, 894836,
 898200] Keep sort button ob bug listing pages in sync with the
 displayed data

Show diffs side-by-side

added added

removed removed

Lines of Context:
55
55
from operator import attrgetter
56
56
import os.path
57
57
import re
 
58
import transaction
58
59
import urllib
59
60
import urlparse
60
61
 
78
79
from pytz import utc
79
80
from simplejson import dumps
80
81
from simplejson.encoder import JSONEncoderForHTML
81
 
import transaction
82
82
from z3c.pt.pagetemplate import ViewPageTemplateFile
83
83
from zope import (
84
84
    component,
107
107
    providedBy,
108
108
    )
109
109
from zope.schema import Choice
110
 
from zope.schema.interfaces import IContextSourceBinder
 
110
from zope.schema.interfaces import (
 
111
    IContextSourceBinder,
 
112
    )
111
113
from zope.schema.vocabulary import (
112
114
    getVocabularyRegistry,
113
115
    SimpleVocabulary,
120
122
from zope.traversing.browser import absoluteURL
121
123
from zope.traversing.interfaces import IPathAdapter
122
124
 
123
 
from lp import _
 
125
from canonical.config import config
 
126
from canonical.launchpad import (
 
127
    _,
 
128
    helpers,
 
129
    )
 
130
from canonical.launchpad.browser.feeds import (
 
131
    BugTargetLatestBugsFeedLink,
 
132
    FeedsMixin,
 
133
    )
 
134
from canonical.launchpad.interfaces.launchpad import IHasExternalBugTracker
 
135
from canonical.launchpad.mailnotification import get_unified_diff
 
136
from canonical.launchpad.searchbuilder import (
 
137
    all,
 
138
    any,
 
139
    NULL,
 
140
    )
 
141
from canonical.launchpad.webapp import (
 
142
    canonical_url,
 
143
    enabled_with_permission,
 
144
    GetitemNavigation,
 
145
    LaunchpadView,
 
146
    Link,
 
147
    Navigation,
 
148
    NavigationMenu,
 
149
    redirection,
 
150
    stepthrough,
 
151
    )
 
152
from canonical.launchpad.webapp.authorization import (
 
153
    check_permission,
 
154
    precache_permission_for_objects,
 
155
    )
 
156
from canonical.launchpad.webapp.batching import TableBatchNavigator
 
157
from canonical.launchpad.webapp.breadcrumb import Breadcrumb
 
158
from canonical.launchpad.webapp.interfaces import ILaunchBag
 
159
from canonical.launchpad.webapp.menu import structured
 
160
from canonical.lazr.interfaces import IObjectPrivacy
124
161
from lp.answers.interfaces.questiontarget import IQuestionTarget
125
162
from lp.app.browser.launchpad import iter_view_registrations
126
163
from lp.app.browser.launchpadform import (
215
252
    UNRESOLVED_BUGTASK_STATUSES,
216
253
    UserCannotEditBugTaskStatus,
217
254
    )
218
 
from lp.bugs.interfaces.bugtracker import (
219
 
    BugTrackerType,
220
 
    IHasExternalBugTracker,
221
 
    )
 
255
from lp.bugs.interfaces.bugtracker import BugTrackerType
222
256
from lp.bugs.interfaces.bugwatch import BugWatchActivityStatus
223
257
from lp.bugs.interfaces.cve import ICveSet
224
258
from lp.bugs.interfaces.malone import IMaloneApplication
244
278
from lp.registry.interfaces.sourcepackage import ISourcePackage
245
279
from lp.registry.model.personroles import PersonRoles
246
280
from lp.registry.vocabularies import MilestoneVocabulary
247
 
from lp.services.config import config
248
281
from lp.services.features import getFeatureFlag
249
 
from lp.services.feeds.browser import (
250
 
    BugTargetLatestBugsFeedLink,
251
 
    FeedsMixin,
252
 
    )
253
282
from lp.services.fields import PersonChoice
254
 
from lp.services.helpers import shortlist
255
 
from lp.services.mail.notification import get_unified_diff
256
 
from lp.services.privacy.interfaces import IObjectPrivacy
257
283
from lp.services.propertycache import (
258
284
    cachedproperty,
259
285
    get_property_cache,
260
286
    )
261
 
from lp.services.searchbuilder import (
262
 
    all,
263
 
    any,
264
 
    NULL,
265
 
    )
266
287
from lp.services.utils import obfuscate_structure
267
 
from lp.services.webapp import (
268
 
    canonical_url,
269
 
    enabled_with_permission,
270
 
    GetitemNavigation,
271
 
    LaunchpadView,
272
 
    Link,
273
 
    Navigation,
274
 
    NavigationMenu,
275
 
    redirection,
276
 
    stepthrough,
277
 
    )
278
 
from lp.services.webapp.authorization import (
279
 
    check_permission,
280
 
    precache_permission_for_objects,
281
 
    )
282
 
from lp.services.webapp.batching import TableBatchNavigator
283
 
from lp.services.webapp.breadcrumb import Breadcrumb
284
 
from lp.services.webapp.interfaces import ILaunchBag
285
 
from lp.services.webapp.menu import structured
286
 
 
287
288
 
288
289
vocabulary_registry = getVocabularyRegistry()
289
290
 
420
421
def get_sortorder_from_request(request):
421
422
    """Get the sortorder from the request.
422
423
 
423
 
    >>> from lp.services.webapp.servers import LaunchpadTestRequest
 
424
    >>> from canonical.launchpad.webapp.servers import LaunchpadTestRequest
424
425
    >>> get_sortorder_from_request(LaunchpadTestRequest(form={}))
425
426
    ['-importance']
426
427
    >>> get_sortorder_from_request(
558
559
                # Security proxy this object on the way out.
559
560
                return getUtility(IBugTaskSet).get(bugtask.id)
560
561
 
561
 
        # If we've come this far, there's no task for the requested context.
562
 
        # If we are attempting to navigate past the non-existent bugtask,
563
 
        # we raise NotFound error. eg +delete or +edit etc.
564
 
        # Otherwise we are simply navigating to a non-existent task and so we
565
 
        # redirect to one that exists.
566
 
        travseral_stack = self.request.getTraversalStack()
567
 
        if len(travseral_stack) > 0:
568
 
            raise NotFoundError
 
562
        # If we've come this far, there's no task for the requested
 
563
        # context. Redirect to one that exists.
569
564
        return self.redirectSubTree(canonical_url(bug.default_bugtask))
570
565
 
571
566
 
1290
1285
 
1291
1286
        If yes, return True, otherwise return False.
1292
1287
        """
1293
 
        return self.context.userHasBugSupervisorPrivileges(self.user)
 
1288
        return self.context.userHasPrivileges(self.user)
1294
1289
 
1295
1290
 
1296
1291
class BugTaskEditView(LaunchpadEditFormView, BugTaskBugWatchMixin,
1789
1784
    def next_url(self):
1790
1785
        """Return the next URL to call when this call completes."""
1791
1786
        if not self.request.is_ajax:
1792
 
            return self._next_url or self._return_url
 
1787
            return super(BugTaskDeletionView, self).next_url
1793
1788
        return None
1794
1789
 
1795
1790
    @action('Delete', name='delete_bugtask')
1800
1795
        success_message = ("This bug no longer affects %s."
1801
1796
                    % bugtask.bugtargetdisplayname)
1802
1797
        error_message = None
1803
 
        # We set the next_url here before the bugtask is deleted since later
1804
 
        # the bugtask will not be available if required to construct the url.
1805
 
        self._next_url = self._return_url
1806
1798
 
1807
1799
        try:
1808
1800
            bugtask.delete()
2075
2067
        The bugtarget may be an `IDistribution`, `IDistroSeries`, `IProduct`,
2076
2068
        or `IProductSeries`.
2077
2069
        """
 
2070
        days_old = config.malone.days_before_expiration
 
2071
 
2078
2072
        if target_has_expirable_bugs_listing(self.context):
2079
2073
            return getUtility(IBugTaskSet).findExpirableBugTasks(
2080
 
                0, user=self.user, target=self.context).count()
 
2074
                days_old, user=self.user, target=self.context).count()
2081
2075
        else:
2082
2076
            return None
2083
2077
 
2234
2228
        assignee = None
2235
2229
        if self.assignee is not None:
2236
2230
            assignee = self.assignee.displayname
2237
 
 
2238
 
        base_tag_url = "%s/?field.tag=" % canonical_url(
2239
 
            self.bugtask.target,
2240
 
            view_name="+bugs")
2241
 
 
2242
 
        flattened = {
 
2231
        return {
2243
2232
            'age': age,
2244
2233
            'assignee': assignee,
2245
2234
            'bug_url': canonical_url(self.bugtask),
2255
2244
            'reporter': self.bug.owner.displayname,
2256
2245
            'status': self.status.title,
2257
2246
            'status_class': 'status' + self.status.name,
2258
 
            'tags': [{'url': base_tag_url + tag, 'tag': tag}
2259
 
                for tag in self.bug.tags],
 
2247
            'tags': ' '.join(self.bug.tags),
2260
2248
            'title': self.bug.title,
2261
2249
            }
2262
2250
 
2263
 
        # This is a total hack, but pystache will run both truth/false values
2264
 
        # for an empty list for some reason, and it "works" if it's just a
2265
 
        # flag like this. We need this value for the mustache template to be
2266
 
        # able to tell that there are no tags without looking at the list.
2267
 
        flattened['has_tags'] = True if len(flattened['tags']) else False
2268
 
        return flattened
2269
 
 
2270
2251
 
2271
2252
class BugListingBatchNavigator(TableBatchNavigator):
2272
2253
    """A specialised batch navigator to load smartly extra bug information."""
2379
2360
 
2380
2361
    @property
2381
2362
    def model(self):
2382
 
        items = [bugtask.model for bugtask in self.getBugListingItems()]
2383
 
        for item in items:
2384
 
            item.update(self.field_visibility)
2385
 
        return {'items': items}
 
2363
        bugtasks = [bugtask.model for bugtask in self.getBugListingItems()]
 
2364
        for bugtask in bugtasks:
 
2365
            bugtask.update(self.field_visibility)
 
2366
        return {'bugtasks': bugtasks}
2386
2367
 
2387
2368
 
2388
2369
class NominatedBugReviewAction(EnumeratedType):
2499
2480
        return Link('+nominations', 'Review nominations', icon='bug')
2500
2481
 
2501
2482
 
2502
 
# All sort orders supported by BugTaskSet.search() and a title for
2503
 
# them.
2504
 
SORT_KEYS = [
2505
 
    ('importance', 'Importance', 'desc'),
2506
 
    ('status', 'Status', 'asc'),
2507
 
    ('id', 'Number', 'desc'),
2508
 
    ('title', 'Title', 'asc'),
2509
 
    ('targetname', 'Package/Project/Series name', 'asc'),
2510
 
    ('milestone_name', 'Milestone', 'asc'),
2511
 
    ('date_last_updated', 'Date last updated', 'desc'),
2512
 
    ('assignee', 'Assignee', 'asc'),
2513
 
    ('reporter', 'Reporter', 'asc'),
2514
 
    ('datecreated', 'Age', 'desc'),
2515
 
    ('tag', 'Tags', 'asc'),
2516
 
    ('heat', 'Heat', 'desc'),
2517
 
    ('date_closed', 'Date closed', 'desc'),
2518
 
    ('dateassigned', 'Date when the bug task was assigned', 'desc'),
2519
 
    ('number_of_duplicates', 'Number of duplicates', 'desc'),
2520
 
    ('latest_patch_uploaded', 'Date latest patch uploaded', 'desc'),
2521
 
    ('message_count', 'Number of comments', 'desc'),
2522
 
    ('milestone', 'Milestone ID', 'desc'),
2523
 
    ('specification', 'Linked blueprint', 'asc'),
2524
 
    ('task', 'Bug task ID', 'desc'),
2525
 
    ('users_affected_count', 'Number of affected users', 'desc'),
2526
 
    ]
2527
 
 
2528
 
 
2529
2483
class BugTaskSearchListingView(LaunchpadFormView, FeedsMixin, BugsInfoMixin):
2530
2484
    """View that renders a list of bugs for a given set of search criteria."""
2531
2485
 
2555
2509
    custom_widget('structural_subscriber', PersonPickerWidget)
2556
2510
    custom_widget('subscriber', PersonPickerWidget)
2557
2511
 
2558
 
    _batch_navigator = None
2559
 
 
2560
2512
    @cachedproperty
2561
2513
    def bug_tracking_usage(self):
2562
2514
        """Whether the context tracks bugs in Launchpad.
2722
2674
            last_batch = batch_navigator.batch.lastBatch()
2723
2675
            cache.objects['last_start'] = last_batch.startNumber() - 1
2724
2676
            cache.objects.update(_getBatchInfo(batch_navigator.batch))
2725
 
            cache.objects['sort_keys'] = SORT_KEYS
2726
2677
 
2727
2678
    @property
2728
2679
    def show_config_portlet(self):
2794
2745
                orderby_col = orderby_col[1:]
2795
2746
 
2796
2747
            try:
2797
 
                bugset.orderby_expression[orderby_col]
 
2748
                bugset.getOrderByColumnDBName(orderby_col)
2798
2749
            except KeyError:
2799
2750
                raise UnexpectedFormData(
2800
2751
                    "Unknown sort column '%s'" % orderby_col)
3021
2972
            the search criteria taken from the request. Params in
3022
2973
            `extra_params` take precedence over request params.
3023
2974
        """
3024
 
        if self._batch_navigator is None:
3025
 
            unbatchedTasks = self.searchUnbatched(
3026
 
                searchtext, context, extra_params)
3027
 
            self._batch_navigator = self._getBatchNavigator(unbatchedTasks)
3028
 
        return self._batch_navigator
 
2975
        unbatchedTasks = self.searchUnbatched(
 
2976
            searchtext, context, extra_params)
 
2977
        return self._getBatchNavigator(unbatchedTasks)
3029
2978
 
3030
2979
    def searchUnbatched(self, searchtext=None, context=None,
3031
2980
                        extra_params=None, prejoins=[]):
3071
3020
                dict(
3072
3021
                    value=term.token, title=term.title or term.token,
3073
3022
                    checked=term.value in default_values))
3074
 
        return shortlist(widget_values, longest_expected=12)
 
3023
        return helpers.shortlist(widget_values, longest_expected=12)
3075
3024
 
3076
3025
    def getStatusWidgetValues(self):
3077
3026
        """Return data used to render the status checkboxes."""
3159
3108
    @property
3160
3109
    def structural_subscriber_label(self):
3161
3110
        if IDistribution.providedBy(self.context):
3162
 
            return 'Package or series subscriber'
 
3111
            return 'Package, or series subscriber'
3163
3112
        elif IDistroSeries.providedBy(self.context):
3164
3113
            return 'Package subscriber'
3165
3114
        elif IProduct.providedBy(self.context):
3576
3525
 
3577
3526
        # If we have made it to here then the logged in user can see the
3578
3527
        # bug, hence they can see any assignees.
3579
 
        # The security adaptor will do the job also but we don't want or need
3580
 
        # the expense of running several complex SQL queries.
3581
3528
        authorised_people = [task.assignee for task in self.bugtasks
3582
3529
                             if task.assignee is not None]
3583
3530
        precache_permission_for_objects(
4159
4106
            (user is None or user.teams_participated_in.count() == 0))
4160
4107
        cx = self.context
4161
4108
        return dict(
4162
 
            id=cx.id,
4163
4109
            row_id=self.data['row_id'],
4164
4110
            form_row_id=self.data['form_row_id'],
4165
4111
            bugtask_path='/'.join([''] + self.data['link'].split('/')[3:]),
4233
4179
        """Return the heading to search all Bugs."""
4234
4180
        return "Search all bug reports"
4235
4181
 
4236
 
    def search_macro_title(self):
4237
 
        return u'Search all bugs'
4238
 
 
4239
4182
    @property
4240
4183
    def label(self):
4241
4184
        return self.getSearchPageHeading()
4362
4305
    page_title = label
4363
4306
 
4364
4307
 
4365
 
class BugTaskExpirableListingView(BugTaskSearchListingView):
 
4308
class BugTaskExpirableListingView(LaunchpadView):
4366
4309
    """View for listing Incomplete bugs that can expire."""
4367
4310
 
4368
4311
    @property
4385
4328
        else:
4386
4329
            return ['id', 'summary', 'date_last_updated', 'heat']
4387
4330
 
 
4331
    @property
4388
4332
    def search(self):
4389
4333
        """Return an `ITableBatchNavigator` for the expirable bugtasks."""
 
4334
        days_old = config.malone.days_before_expiration
4390
4335
        bugtaskset = getUtility(IBugTaskSet)
4391
4336
        bugtasks = bugtaskset.findExpirableBugTasks(
4392
 
            user=self.user, target=self.context, min_days_old=0)
 
4337
            days_old, user=self.user, target=self.context)
4393
4338
        return BugListingBatchNavigator(
4394
4339
            bugtasks, self.request, columns_to_show=self.columns_to_show,
4395
4340
            size=config.malone.buglist_batch_size)