launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #17607
[Merge] lp:~wgrant/launchpad/split-browser-bugtask into lp:launchpad
William Grant has proposed merging lp:~wgrant/launchpad/split-browser-bugtask into lp:launchpad with lp:~wgrant/launchpad/sanitise-labels as a prerequisite.
Commit message:
Split buglisting out of lp.bugs.browser.bugtask. 4KLOC module no more.
Requested reviews:
William Grant (wgrant): code
For more details, see:
https://code.launchpad.net/~wgrant/launchpad/split-browser-bugtask/+merge/243203
Split buglisting out of lp.bugs.browser.bugtask. 4KLOC module no more.
--
Your team Launchpad code reviewers is subscribed to branch lp:launchpad.
=== modified file 'lib/lp/app/browser/configure.zcml'
--- lib/lp/app/browser/configure.zcml 2014-11-17 00:03:41 +0000
+++ lib/lp/app/browser/configure.zcml 2014-11-29 05:04:02 +0000
@@ -539,7 +539,7 @@
/>
<adapter
- for="lp.bugs.browser.bugtask.BugTaskListingItem"
+ for="lp.bugs.browser.buglisting.BugTaskListingItem"
provides="zope.traversing.interfaces.IPathAdapter"
factory="lp.app.browser.tales.BugTaskListingItemImageDisplayAPI"
name="image"
=== modified file 'lib/lp/bugs/browser/buglinktarget.py'
--- lib/lp/bugs/browser/buglinktarget.py 2012-08-07 02:31:56 +0000
+++ lib/lp/bugs/browser/buglinktarget.py 2014-11-29 05:04:02 +0000
@@ -28,7 +28,7 @@
LaunchpadFormView,
)
from lp.app.widgets.itemswidgets import LabeledMultiCheckBoxWidget
-from lp.bugs.browser.bugtask import BugListingBatchNavigator
+from lp.bugs.browser.buglisting import BugListingBatchNavigator
from lp.bugs.interfaces.buglink import (
IBugLinkForm,
IUnlinkBugsForm,
=== added file 'lib/lp/bugs/browser/buglisting.py'
--- lib/lp/bugs/browser/buglisting.py 1970-01-01 00:00:00 +0000
+++ lib/lp/bugs/browser/buglisting.py 2014-11-29 05:04:02 +0000
@@ -0,0 +1,1827 @@
+# Copyright 2009-2014 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""IBugTask-related browser views."""
+
+__metaclass__ = type
+
+__all__ = [
+ 'BugNominationsView',
+ 'BugListingBatchNavigator',
+ 'BugListingPortletInfoView',
+ 'BugListingPortletStatsView',
+ 'BugsBugTaskSearchListingView',
+ 'BugTargetView',
+ 'BugTaskExpirableListingView',
+ 'BugTaskListingItem',
+ 'BugTaskListingView',
+ 'BugTaskSearchListingView',
+ 'get_buglisting_search_filter_url',
+ 'get_sortorder_from_request',
+ 'TextualBugTaskSearchListingView',
+ ]
+
+import cgi
+import os.path
+import urllib
+import urlparse
+
+from lazr.delegates import delegates
+from lazr.restful.interfaces import IJSONRequestCache
+from lazr.uri import URI
+import pystache
+from simplejson import dumps
+from simplejson.encoder import JSONEncoderForHTML
+from z3c.pt.pagetemplate import ViewPageTemplateFile
+from zope.authentication.interfaces import IUnauthenticatedPrincipal
+from zope.component import (
+ getAdapter,
+ getUtility,
+ queryMultiAdapter,
+ )
+from zope.formlib.interfaces import InputErrors
+from zope.formlib.itemswidgets import RadioWidget
+from zope.interface import (
+ implements,
+ Interface,
+ )
+from zope.schema.vocabulary import getVocabularyRegistry
+from zope.security.proxy import isinstance as zope_isinstance
+from zope.traversing.interfaces import IPathAdapter
+
+from lp import _
+from lp.answers.interfaces.questiontarget import IQuestionTarget
+from lp.app.browser.launchpad import iter_view_registrations
+from lp.app.browser.launchpadform import (
+ custom_widget,
+ LaunchpadFormView,
+ )
+from lp.app.browser.tales import (
+ BugTrackerFormatterAPI,
+ DateTimeFormatterAPI,
+ PersonFormatterAPI,
+ )
+from lp.app.enums import (
+ InformationType,
+ ServiceUsage,
+ )
+from lp.app.errors import (
+ NotFoundError,
+ UnexpectedFormData,
+ )
+from lp.app.interfaces.launchpad import (
+ IPrivacy,
+ IServiceUsage,
+ )
+from lp.app.vocabularies import InformationTypeVocabulary
+from lp.app.widgets.itemswidgets import LabeledMultiCheckBoxWidget
+from lp.app.widgets.popup import PersonPickerWidget
+from lp.app.widgets.project import ProjectScopeWidget
+from lp.bugs.browser.structuralsubscription import (
+ expose_structural_subscription_data_to_js,
+ )
+from lp.bugs.browser.widgets.bug import BugTagsWidget
+from lp.bugs.browser.widgets.bugtask import NewLineToSpacesWidget
+from lp.bugs.interfaces.bug import IBugSet
+from lp.bugs.interfaces.bugattachment import BugAttachmentType
+from lp.bugs.interfaces.bugtask import (
+ BugTaskImportance,
+ BugTaskStatus,
+ BugTaskStatusSearch,
+ BugTaskStatusSearchDisplay,
+ IBugTask,
+ IBugTaskSet,
+ UNRESOLVED_BUGTASK_STATUSES,
+ )
+from lp.bugs.interfaces.bugtasksearch import (
+ BugBlueprintSearch,
+ BugBranchSearch,
+ BugTagsSearchCombinator,
+ BugTaskSearchParams,
+ DEFAULT_SEARCH_BUGTASK_STATUSES_FOR_DISPLAY,
+ IBugTaskSearch,
+ IFrontPageBugTaskSearch,
+ IPersonBugTaskSearch,
+ IUpstreamProductBugTaskSearch,
+ )
+from lp.bugs.interfaces.bugtracker import IHasExternalBugTracker
+from lp.bugs.interfaces.malone import IMaloneApplication
+from lp.bugs.model.bugtasksearch import orderby_expression
+from lp.layers import FeedsLayer
+from lp.registry.interfaces.distribution import IDistribution
+from lp.registry.interfaces.distributionsourcepackage import (
+ IDistributionSourcePackage,
+ )
+from lp.registry.interfaces.distroseries import IDistroSeries
+from lp.registry.interfaces.person import IPerson
+from lp.registry.interfaces.product import IProduct
+from lp.registry.interfaces.productseries import IProductSeries
+from lp.registry.interfaces.projectgroup import IProjectGroup
+from lp.registry.interfaces.sourcepackage import ISourcePackage
+from lp.services.config import config
+from lp.services.feeds.browser import (
+ BugTargetLatestBugsFeedLink,
+ FeedsMixin,
+ )
+from lp.services.helpers import shortlist
+from lp.services.propertycache import cachedproperty
+from lp.services.searchbuilder import (
+ all,
+ any,
+ NULL,
+ )
+from lp.services.utils import obfuscate_structure
+from lp.services.webapp import (
+ canonical_url,
+ enabled_with_permission,
+ LaunchpadView,
+ Link,
+ NavigationMenu,
+ )
+from lp.services.webapp.authorization import check_permission
+from lp.services.webapp.batching import TableBatchNavigator
+from lp.services.webapp.interfaces import ILaunchBag
+
+
+vocabulary_registry = getVocabularyRegistry()
+
+DISPLAY_BUG_STATUS_FOR_PATCHES = {
+ BugTaskStatus.NEW: True,
+ BugTaskStatus.INCOMPLETE: True,
+ BugTaskStatus.INVALID: False,
+ BugTaskStatus.WONTFIX: False,
+ BugTaskStatus.CONFIRMED: True,
+ BugTaskStatus.TRIAGED: True,
+ BugTaskStatus.INPROGRESS: True,
+ BugTaskStatus.FIXCOMMITTED: True,
+ BugTaskStatus.FIXRELEASED: False,
+ BugTaskStatus.UNKNOWN: False,
+ BugTaskStatus.EXPIRED: False,
+ BugTaskStatusSearch.INCOMPLETE_WITHOUT_RESPONSE: True,
+ BugTaskStatusSearch.INCOMPLETE_WITH_RESPONSE: True,
+ }
+
+
+def get_sortorder_from_request(request):
+ """Get the sortorder from the request.
+
+ >>> from lp.services.webapp.servers import LaunchpadTestRequest
+ >>> get_sortorder_from_request(LaunchpadTestRequest(form={}))
+ ['-importance']
+ >>> get_sortorder_from_request(
+ ... LaunchpadTestRequest(form={'orderby': '-status'}))
+ ['-status']
+ >>> get_sortorder_from_request(LaunchpadTestRequest(
+ ... form={'orderby': 'status,-severity,importance'}))
+ ['status', 'importance']
+ >>> get_sortorder_from_request(
+ ... LaunchpadTestRequest(form={'orderby': 'priority,-severity'}))
+ ['-importance']
+ """
+ order_by_string = request.get("orderby", '')
+ if order_by_string:
+ if not zope_isinstance(order_by_string, list):
+ order_by = order_by_string.split(',')
+ else:
+ order_by = order_by_string
+ else:
+ order_by = []
+ # Remove old order_by values that people might have in bookmarks.
+ for old_order_by_column in ['priority', 'severity']:
+ if old_order_by_column in order_by:
+ order_by.remove(old_order_by_column)
+ if '-' + old_order_by_column in order_by:
+ order_by.remove('-' + old_order_by_column)
+ if order_by:
+ return order_by
+ else:
+ # No sort ordering specified, so use a reasonable default.
+ return ["-importance"]
+
+
+def get_default_search_params(user):
+ """Return a BugTaskSearchParams instance with default values.
+
+ By default, a search includes any bug that is unresolved and not a
+ duplicate of another bug.
+
+ If this search will be used to display a list of bugs to the user
+ it may be a good idea to set the orderby attribute using
+ get_sortorder_from_request():
+
+ params = get_default_search_params(user)
+ params.orderby = get_sortorder_from_request(request)
+
+ """
+ return BugTaskSearchParams(
+ user=user, status=any(*UNRESOLVED_BUGTASK_STATUSES), omit_dupes=True)
+
+
+OLD_BUGTASK_STATUS_MAP = {
+ 'Unconfirmed': 'New',
+ 'Needs Info': 'Incomplete',
+ 'Rejected': 'Invalid',
+ }
+
+
+def rewrite_old_bugtask_status_query_string(query_string):
+ """Return a query string with old status names replaced with new.
+
+ If an old status string has been used in the query, construct a
+ corrected query string for the search, else return the original
+ query string.
+ """
+ query_elements = cgi.parse_qsl(
+ query_string, keep_blank_values=True, strict_parsing=False)
+ query_elements_mapped = []
+
+ for name, value in query_elements:
+ if name == 'field.status:list':
+ value = OLD_BUGTASK_STATUS_MAP.get(value, value)
+ query_elements_mapped.append((name, value))
+
+ if query_elements == query_elements_mapped:
+ return query_string
+ else:
+ return urllib.urlencode(query_elements_mapped, doseq=True)
+
+
+def target_has_expirable_bugs_listing(target):
+ """Return True or False if the target has the expirable-bugs listing.
+
+ The target must be a Distribution, DistroSeries, Product, or
+ ProductSeries, and the pillar must have enabled bug expiration.
+ """
+ if IDistribution.providedBy(target) or IProduct.providedBy(target):
+ return target.enable_bug_expiration
+ elif IProductSeries.providedBy(target):
+ return target.product.enable_bug_expiration
+ elif IDistroSeries.providedBy(target):
+ return target.distribution.enable_bug_expiration
+ else:
+ # This context is not a supported bugtarget.
+ return False
+
+
+class BugTaskListingView(LaunchpadView):
+ """A view designed for displaying bug tasks in lists."""
+ # Note that this right now is only used in tests and to render
+ # status in the CVEReportView. It may be a candidate for refactoring
+ # or removal.
+ @property
+ def status(self):
+ """Return an HTML representation of the bugtask status.
+
+ The assignee is included.
+ """
+ bugtask = self.context
+ assignee = bugtask.assignee
+ status = bugtask.status
+ status_title = status.title.capitalize()
+
+ if not assignee:
+ return status_title + ' (unassigned)'
+ assignee_html = PersonFormatterAPI(assignee).link('+assignedbugs')
+
+ if status in (BugTaskStatus.INVALID,
+ BugTaskStatus.FIXCOMMITTED):
+ return '%s by %s' % (status_title, assignee_html)
+ else:
+ return '%s, assigned to %s' % (status_title, assignee_html)
+
+ @property
+ def status_elsewhere(self):
+ """Return human-readable representation of the status of this bug
+ in other contexts for which it's reported.
+ """
+ bugtask = self.context
+ related_tasks = bugtask.related_tasks
+ if not related_tasks:
+ return "not filed elsewhere"
+
+ fixes_found = len(
+ [task for task in related_tasks
+ if task.status in (BugTaskStatus.FIXCOMMITTED,
+ BugTaskStatus.FIXRELEASED)])
+ if fixes_found:
+ return "fixed in %d of %d places" % (
+ fixes_found, len(bugtask.bug.bugtasks))
+ elif len(related_tasks) == 1:
+ return "filed in 1 other place"
+ else:
+ return "filed in %d other places" % len(related_tasks)
+
+ def render(self):
+ """Make rendering this template-less view not crash."""
+ return u""
+
+
+class BugsInfoMixin:
+ """Contains properties giving URLs to bug information."""
+
+ @property
+ def bugs_fixed_elsewhere_url(self):
+ """A URL to a list of bugs fixed elsewhere."""
+ return "%s?field.status_upstream=resolved_upstream" % (
+ canonical_url(self.context, view_name='+bugs'))
+
+ @property
+ def open_cve_bugs_url(self):
+ """A URL to a list of open bugs linked to CVEs."""
+ return "%s?field.has_cve=on" % (
+ canonical_url(self.context, view_name='+bugs'))
+
+ @property
+ def open_cve_bugs_has_report(self):
+ """Whether or not the context has a CVE report page."""
+ return queryMultiAdapter(
+ (self.context, self.request), name='+cve') is not None
+
+ @property
+ def pending_bugwatches_url(self):
+ """A URL to a list of bugs that need a bugwatch.
+
+ None is returned if the context is not an upstream product.
+ """
+ if not IProduct.providedBy(self.context):
+ return None
+ if self.context.bug_tracking_usage == ServiceUsage.LAUNCHPAD:
+ return None
+ return "%s?field.status_upstream=pending_bugwatch" % (
+ canonical_url(self.context, view_name='+bugs'))
+
+ @property
+ def expirable_bugs_url(self):
+ """A URL to a list of bugs that can expire, or None.
+
+ If the bugtarget is not a supported implementation, or its pillar
+ does not have enable_bug_expiration set to True, None is returned.
+ The bugtarget may be an `IDistribution`, `IDistroSeries`, `IProduct`,
+ or `IProductSeries`.
+ """
+ if target_has_expirable_bugs_listing(self.context):
+ return canonical_url(self.context, view_name='+expirable-bugs')
+ else:
+ return None
+
+ @property
+ def new_bugs_url(self):
+ """A URL to a page of new bugs."""
+ return get_buglisting_search_filter_url(
+ status=BugTaskStatus.NEW.title)
+
+ @property
+ def inprogress_bugs_url(self):
+ """A URL to a page of inprogress bugs."""
+ return get_buglisting_search_filter_url(
+ status=BugTaskStatus.INPROGRESS.title)
+
+ @property
+ def open_bugs_url(self):
+ """A URL to a list of open bugs."""
+ return canonical_url(self.context, view_name='+bugs')
+
+ @property
+ def critical_bugs_url(self):
+ """A URL to a list of critical bugs."""
+ return get_buglisting_search_filter_url(
+ status=[status.title for status in UNRESOLVED_BUGTASK_STATUSES],
+ importance=BugTaskImportance.CRITICAL.title)
+
+ @property
+ def high_bugs_url(self):
+ """A URL to a list of high priority bugs."""
+ return get_buglisting_search_filter_url(
+ status=[status.title for status in UNRESOLVED_BUGTASK_STATUSES],
+ importance=BugTaskImportance.HIGH.title)
+
+ @property
+ def my_bugs_url(self):
+ """A URL to a list of bugs assigned to the user, or None."""
+ if self.user is None:
+ return None
+ else:
+ return get_buglisting_search_filter_url(assignee=self.user.name)
+
+ @property
+ def my_affecting_bugs_url(self):
+ """A URL to a list of bugs affecting the current user, or None if
+ there is no current user.
+ """
+ if self.user is None:
+ return None
+ return get_buglisting_search_filter_url(
+ affecting_me=True,
+ orderby='-date_last_updated')
+
+ @property
+ def my_reported_bugs_url(self):
+ """A URL to a list of bugs reported by the user, or None."""
+ if self.user is None:
+ return None
+ return get_buglisting_search_filter_url(bug_reporter=self.user.name)
+
+
+class BugsStatsMixin(BugsInfoMixin):
+ """Contains properties giving bug stats.
+
+ These can be expensive to obtain.
+ """
+
+ @cachedproperty
+ def _bug_stats(self):
+ # Circular fail.
+ from lp.bugs.model.bugsummary import BugSummary
+ bug_task_set = getUtility(IBugTaskSet)
+ groups = (
+ BugSummary.status, BugSummary.importance, BugSummary.has_patch)
+ counts = bug_task_set.countBugs(self.user, [self.context], groups)
+ # Sum the split out aggregates.
+ new = 0
+ open = 0
+ inprogress = 0
+ critical = 0
+ high = 0
+ with_patch = 0
+ for metadata, count in counts.items():
+ status = metadata[0]
+ importance = metadata[1]
+ has_patch = metadata[2]
+ if status == BugTaskStatus.NEW:
+ new += count
+ elif status == BugTaskStatus.INPROGRESS:
+ inprogress += count
+ if importance == BugTaskImportance.CRITICAL:
+ critical += count
+ elif importance == BugTaskImportance.HIGH:
+ high += count
+ if has_patch and DISPLAY_BUG_STATUS_FOR_PATCHES[status]:
+ with_patch += count
+ open += count
+ result = dict(
+ new=new, open=open, inprogress=inprogress, high=high,
+ critical=critical, with_patch=with_patch)
+ return result
+
+ @property
+ def open_cve_bugs_count(self):
+ """A count of open bugs linked to CVEs."""
+ params = get_default_search_params(self.user)
+ params.has_cve = True
+ return self.context.searchTasks(params).count()
+
+ @property
+ def pending_bugwatches_count(self):
+ """A count of bugs that need a bugwatch.
+
+ None is returned if the context is not an upstream product.
+ """
+ if not IProduct.providedBy(self.context):
+ return None
+ if self.context.bug_tracking_usage == ServiceUsage.LAUNCHPAD:
+ return None
+ params = get_default_search_params(self.user)
+ params.pending_bugwatch_elsewhere = True
+ return self.context.searchTasks(params).count()
+
+ @property
+ def expirable_bugs_count(self):
+ """A count of bugs that can expire, or None.
+
+ If the bugtarget is not a supported implementation, or its pillar
+ does not have enable_bug_expiration set to True, None is returned.
+ The bugtarget may be an `IDistribution`, `IDistroSeries`, `IProduct`,
+ or `IProductSeries`.
+ """
+ if target_has_expirable_bugs_listing(self.context):
+ return getUtility(IBugTaskSet).findExpirableBugTasks(
+ 0, user=self.user, target=self.context).count()
+ else:
+ return None
+
+ @property
+ def new_bugs_count(self):
+ """A count of new bugs."""
+ return self._bug_stats['new']
+
+ @property
+ def open_bugs_count(self):
+ """A count of open bugs."""
+ return self._bug_stats['open']
+
+ @property
+ def inprogress_bugs_count(self):
+ """A count of in-progress bugs."""
+ return self._bug_stats['inprogress']
+
+ @property
+ def critical_bugs_count(self):
+ """A count of critical bugs."""
+ return self._bug_stats['critical']
+
+ @property
+ def high_bugs_count(self):
+ """A count of high priority bugs."""
+ return self._bug_stats['high']
+
+ @property
+ def my_bugs_count(self):
+ """A count of bugs assigned to the user, or None."""
+ if self.user is None:
+ return None
+ else:
+ params = get_default_search_params(self.user)
+ params.assignee = self.user
+ return self.context.searchTasks(params).count()
+
+ @property
+ def my_reported_bugs_count(self):
+ """A count of bugs reported by the user, or None."""
+ if self.user is None:
+ return None
+ params = get_default_search_params(self.user)
+ params.bug_reporter = self.user
+ return self.context.searchTasks(params).count()
+
+ @property
+ def my_affecting_bugs_count(self):
+ """A count of bugs affecting the user, or None."""
+ if self.user is None:
+ return None
+ params = get_default_search_params(self.user)
+ params.affects_me = True
+ return self.context.searchTasks(params).count()
+
+ @property
+ def bugs_with_patches_count(self):
+ """A count of unresolved bugs with patches."""
+ return self._bug_stats['with_patch']
+
+
+class BugListingPortletInfoView(LaunchpadView, BugsInfoMixin):
+ """Portlet containing available bug listings without stats."""
+
+
+class BugListingPortletStatsView(LaunchpadView, BugsStatsMixin):
+ """Portlet containing available bug listings with stats."""
+
+
+def get_buglisting_search_filter_url(
+ assignee=None, importance=None, status=None, status_upstream=None,
+ has_patches=None, bug_reporter=None,
+ affecting_me=None,
+ orderby=None):
+ """Return the given URL with the search parameters specified."""
+ search_params = []
+
+ if assignee is not None:
+ search_params.append(('field.assignee', assignee))
+ if importance is not None:
+ search_params.append(('field.importance', importance))
+ if status is not None:
+ search_params.append(('field.status', status))
+ if status_upstream is not None:
+ search_params.append(('field.status_upstream', status_upstream))
+ if has_patches is not None:
+ search_params.append(('field.has_patch', 'on'))
+ if bug_reporter is not None:
+ search_params.append(('field.bug_reporter', bug_reporter))
+ if affecting_me is not None:
+ search_params.append(('field.affects_me', 'on'))
+ if orderby is not None:
+ search_params.append(('orderby', orderby))
+
+ query_string = urllib.urlencode(search_params, doseq=True)
+
+ search_filter_url = "+bugs?search=Search"
+ if query_string != '':
+ search_filter_url += "&" + query_string
+
+ return search_filter_url
+
+
+class BugTaskListingItem:
+ """A decorated bug task.
+
+ Some attributes that we want to display are too convoluted or expensive
+ to get on the fly for each bug task in the listing. These items are
+ prefetched by the view and decorate the bug task.
+ """
+ delegates(IBugTask, 'bugtask')
+
+ def __init__(self, bugtask, has_bug_branch,
+ has_specification, has_patch, tags,
+ people, request=None, target_context=None):
+ self.bugtask = bugtask
+ self.review_action_widget = None
+ self.has_bug_branch = has_bug_branch
+ self.has_specification = has_specification
+ self.has_patch = has_patch
+ self.tags = tags
+ self.people = people
+ self.request = request
+ self.target_context = target_context
+
+ @property
+ def last_significant_change_date(self):
+ """The date of the last significant change."""
+ return (self.bugtask.date_closed or self.bugtask.date_fix_committed or
+ self.bugtask.date_inprogress or self.bugtask.date_left_new or
+ self.bugtask.datecreated)
+
+ @property
+ def bug_heat_html(self):
+ """Returns the bug heat flames HTML."""
+ return (
+ '<span class="sprite flame">%d</span>'
+ % self.bugtask.bug.heat)
+
+ @property
+ def model(self):
+ """Provide flattened data about bugtask for simple templaters."""
+ age = DateTimeFormatterAPI(self.bug.datecreated).durationsince()
+ age += ' old'
+ date_last_updated = self.bug.date_last_message
+ if (date_last_updated is None or
+ self.bug.date_last_updated > date_last_updated):
+ date_last_updated = self.bug.date_last_updated
+ last_updated_formatter = DateTimeFormatterAPI(date_last_updated)
+ last_updated = last_updated_formatter.displaydate()
+ badges = getAdapter(self, IPathAdapter, 'image').badges()
+ target_image = getAdapter(self.target, IPathAdapter, 'image')
+ if self.bugtask.milestone is not None:
+ milestone_name = self.bugtask.milestone.displayname
+ else:
+ milestone_name = None
+ assignee = None
+ if self.assigneeID is not None:
+ assignee = self.people[self.assigneeID].displayname
+ reporter = self.people[self.bug.ownerID]
+
+ # the case that there is no target context (e.g. viewing bug that
+ # are related to a user account) is intercepted
+ if self.target_context is None:
+ base_tag_url = "%s/?field.tag=" % canonical_url(
+ self.bugtask.target,
+ view_name="+bugs")
+ else:
+ base_tag_url = "%s/?field.tag=" % canonical_url(
+ self.target_context,
+ view_name="+bugs")
+
+ flattened = {
+ 'age': age,
+ 'assignee': assignee,
+ 'bug_url': canonical_url(self.bugtask),
+ 'bugtarget': self.bugtargetdisplayname,
+ 'bugtarget_css': target_image.sprite_css(),
+ 'bug_heat_html': self.bug_heat_html,
+ 'badges': badges,
+ 'id': self.bug.id,
+ 'importance': self.importance.title,
+ 'importance_class': 'importance' + self.importance.name,
+ 'information_type': self.bug.information_type.title,
+ 'last_updated': last_updated,
+ 'milestone_name': milestone_name,
+ 'reporter': reporter.displayname,
+ 'status': self.status.title,
+ 'status_class': 'status' + self.status.name,
+ 'tags': [{'url': base_tag_url + urllib.quote(tag), 'tag': tag}
+ for tag in self.tags],
+ 'title': self.bug.title,
+ }
+
+ # This is a total hack, but pystache will run both truth/false values
+ # for an empty list for some reason, and it "works" if it's just a
+ # flag like this. We need this value for the mustache template to be
+ # able to tell that there are no tags without looking at the list.
+ flattened['has_tags'] = True if len(flattened['tags']) else False
+ return flattened
+
+
+class BugListingBatchNavigator(TableBatchNavigator):
+ """A specialised batch navigator to load smartly extra bug information."""
+
+ def __init__(self, tasks, request, columns_to_show, size,
+ target_context=None):
+ self.request = request
+ self.target_context = target_context
+ self.user = getUtility(ILaunchBag).user
+ self.field_visibility_defaults = {
+ 'show_datecreated': False,
+ 'show_assignee': False,
+ 'show_targetname': True,
+ 'show_heat': True,
+ 'show_id': True,
+ 'show_importance': True,
+ 'show_information_type': False,
+ 'show_date_last_updated': False,
+ 'show_milestone_name': False,
+ 'show_reporter': False,
+ 'show_status': True,
+ 'show_tag': False,
+ }
+ self.field_visibility = None
+ self._setFieldVisibility()
+ TableBatchNavigator.__init__(
+ self, tasks, request, columns_to_show=columns_to_show, size=size)
+
+ @cachedproperty
+ def bug_badge_properties(self):
+ return getUtility(IBugTaskSet).getBugTaskBadgeProperties(
+ self.currentBatch())
+
+ @cachedproperty
+ def tags_for_batch(self):
+ """Return a dict matching bugtask to it's tags."""
+ return getUtility(IBugTaskSet).getBugTaskTags(self.currentBatch())
+
+ @cachedproperty
+ def bugtask_people(self):
+ """Return mapping of people related to this bugtask set."""
+ return getUtility(IBugTaskSet).getBugTaskPeople(self.currentBatch())
+
+ def getCookieName(self):
+ """Return the cookie name used in bug listings js code."""
+ cookie_name_template = '%s-buglist-fields'
+ cookie_name = ''
+ if self.user is not None:
+ cookie_name = cookie_name_template % self.user.name
+ else:
+ cookie_name = cookie_name_template % 'anon'
+ return cookie_name
+
+ def _setFieldVisibility(self):
+ """Set field_visibility for the page load.
+
+ If a cookie of the form $USER-buglist-fields is found,
+ we set field_visibility from this cookie; otherwise,
+ field_visibility will match the defaults.
+ """
+ cookie_name = self.getCookieName()
+ cookie = self.request.cookies.get(cookie_name)
+ self.field_visibility = dict(self.field_visibility_defaults)
+ # "cookie" looks like a URL query string, so we split
+ # on '&' to get items, and then split on '=' to get
+ # field/value pairs.
+ if cookie is None:
+ return
+ for field, value in urlparse.parse_qsl(cookie):
+ # Skip unsupported fields (from old cookies).
+ if field not in self.field_visibility:
+ continue
+ # We only record True or False for field values.
+ self.field_visibility[field] = (value == 'true')
+
+ def _getListingItem(self, bugtask):
+ """Return a decorated bugtask for the bug listing."""
+ badge_property = self.bug_badge_properties[bugtask]
+ tags = self.tags_for_batch.get(bugtask.id, ())
+ if (IMaloneApplication.providedBy(self.target_context) or
+ IPerson.providedBy(self.target_context)):
+ # XXX Tom Berger bug=529846
+ # When we have a specific interface for things that have bug heat
+ # it would be better to use that for the check here instead.
+ target_context = None
+ else:
+ target_context = self.target_context
+ return BugTaskListingItem(
+ bugtask,
+ badge_property['has_branch'],
+ badge_property['has_specification'],
+ badge_property['has_patch'],
+ tags,
+ self.bugtask_people,
+ request=self.request,
+ target_context=target_context)
+
+ def getBugListingItems(self):
+ """Return a decorated list of visible bug tasks."""
+ return [self._getListingItem(bugtask) for bugtask in self.batch]
+
+ @cachedproperty
+ def mustache_template(self):
+ template_path = os.path.join(
+ config.root, 'lib/lp/bugs/templates/buglisting.mustache')
+ with open(template_path) as template_file:
+ return template_file.read()
+
+ @property
+ def mustache_listings(self):
+ return 'LP.mustache_listings = %s;' % dumps(
+ self.mustache_template, cls=JSONEncoderForHTML)
+
+ @property
+ def mustache(self):
+ """The rendered mustache template."""
+ objects = IJSONRequestCache(self.request).objects
+ if IUnauthenticatedPrincipal.providedBy(self.request.principal):
+ objects = obfuscate_structure(objects)
+ model = dict(objects['mustache_model'])
+ model.update(self.field_visibility)
+ return pystache.render(self.mustache_template, model)
+
+ @property
+ def model(self):
+ items = [bugtask.model for bugtask in self.getBugListingItems()]
+ return {'items': items}
+
+
+class IBugTaskSearchListingMenu(Interface):
+ """A marker interface for the search listing navigation menu."""
+
+
+class BugTaskSearchListingMenu(NavigationMenu):
+ """The search listing navigation menu."""
+ usedfor = IBugTaskSearchListingMenu
+ facet = 'bugs'
+
+ @property
+ def links(self):
+ bug_target = self.context.context
+ if IDistroSeries.providedBy(bug_target):
+ return (
+ 'nominations',
+ )
+ if IProductSeries.providedBy(bug_target):
+ return (
+ 'nominations',
+ )
+ else:
+ return ()
+
+ @enabled_with_permission('launchpad.Edit')
+ def bugsupervisor(self):
+ return Link('+bugsupervisor', 'Change bug supervisor', icon='edit')
+
+ def nominations(self):
+ return Link('+nominations', 'Review nominations', icon='bug')
+
+
+# All sort orders supported by BugTaskSet.search() and a title for
+# them.
+SORT_KEYS = [
+ ('importance', 'Importance', 'desc'),
+ ('status', 'Status', 'asc'),
+ ('information_type', 'Information Type', 'asc'),
+ ('id', 'Number', 'desc'),
+ ('title', 'Title', 'asc'),
+ ('targetname', 'Package/Project/Series name', 'asc'),
+ ('milestone_name', 'Milestone', 'asc'),
+ ('date_last_updated', 'Date last updated', 'desc'),
+ ('assignee', 'Assignee', 'asc'),
+ ('reporter', 'Reporter', 'asc'),
+ ('datecreated', 'Age', 'desc'),
+ ('tag', 'Tags', 'asc'),
+ ('heat', 'Heat', 'desc'),
+ ('date_closed', 'Date closed', 'desc'),
+ ('dateassigned', 'Date when the bug task was assigned', 'desc'),
+ ('number_of_duplicates', 'Number of duplicates', 'desc'),
+ ('latest_patch_uploaded', 'Date latest patch uploaded', 'desc'),
+ ('message_count', 'Number of comments', 'desc'),
+ ('milestone', 'Milestone ID', 'desc'),
+ ('specification', 'Linked blueprint', 'asc'),
+ ('task', 'Bug task ID', 'desc'),
+ ('users_affected_count', 'Number of affected users', 'desc'),
+ ]
+
+
+class BugTaskSearchListingView(LaunchpadFormView, FeedsMixin, BugsInfoMixin):
+ """View that renders a list of bugs for a given set of search criteria."""
+
+ implements(IBugTaskSearchListingMenu)
+
+ related_features = {
+ 'bugs.dynamic_bug_listings.pre_fetch': False
+ }
+
+ # Only include <link> tags for bug feeds when using this view.
+ feed_types = (
+ BugTargetLatestBugsFeedLink,
+ )
+
+ # These widgets are customised so as to keep the presentation of this view
+ # and its descendants consistent after refactoring to use
+ # LaunchpadFormView as a parent.
+ custom_widget('searchtext', NewLineToSpacesWidget)
+ custom_widget('status_upstream', LabeledMultiCheckBoxWidget)
+ custom_widget('tag', BugTagsWidget)
+ custom_widget('tags_combinator', RadioWidget)
+ custom_widget('component', LabeledMultiCheckBoxWidget)
+ custom_widget('assignee', PersonPickerWidget)
+ custom_widget('bug_reporter', PersonPickerWidget)
+ custom_widget('bug_commenter', PersonPickerWidget)
+ custom_widget('structural_subscriber', PersonPickerWidget)
+ custom_widget('subscriber', PersonPickerWidget)
+
+ _batch_navigator = None
+
+ @cachedproperty
+ def bug_tracking_usage(self):
+ """Whether the context tracks bugs in Launchpad.
+
+ :returns: ServiceUsage enum value
+ """
+ service_usage = IServiceUsage(self.context)
+ return service_usage.bug_tracking_usage
+
+ @cachedproperty
+ def external_bugtracker(self):
+ """External bug tracking system designated for the context.
+
+ :returns: `IBugTracker` or None
+ """
+ has_external_bugtracker = IHasExternalBugTracker(self.context, None)
+ if has_external_bugtracker is None:
+ return None
+ else:
+ return has_external_bugtracker.getExternalBugTracker()
+
+ @property
+ def has_bugtracker(self):
+ """Does the `IBugTarget` have a bug tracker or use Launchpad?"""
+ usage = IServiceUsage(self.context)
+ uses_lp = usage.bug_tracking_usage == ServiceUsage.LAUNCHPAD
+ if self.external_bugtracker or uses_lp:
+ return True
+ return False
+
+ @property
+ def can_have_external_bugtracker(self):
+ return (IProduct.providedBy(self.context)
+ or IProductSeries.providedBy(self.context))
+
+ @property
+ def bugtracker(self):
+ """Description of the context's bugtracker.
+
+ :returns: str which may contain HTML.
+ """
+ if self.bug_tracking_usage == ServiceUsage.LAUNCHPAD:
+ return 'Launchpad'
+ elif self.external_bugtracker:
+ return BugTrackerFormatterAPI(self.external_bugtracker).link(None)
+ else:
+ return 'None specified'
+
+ @cachedproperty
+ def upstream_project(self):
+ """The linked upstream `IProduct` for the package.
+
+ If this `IBugTarget` is a `IDistributionSourcePackage` or an
+ `ISourcePackage` and it is linked to an upstream project, return
+ the `IProduct`. Otherwise, return None
+
+ :returns: `IProduct` or None
+ """
+ if self._sourcePackageContext():
+ sp = self.context
+ elif self._distroSourcePackageContext():
+ sp = self.context.development_version
+ else:
+ sp = None
+ if sp is not None:
+ packaging = sp.packaging
+ if packaging is not None:
+ return packaging.productseries.product
+ return None
+
+ @cachedproperty
+ def upstream_launchpad_project(self):
+ """The linked upstream `IProduct` for the package.
+
+ If this `IBugTarget` is a `IDistributionSourcePackage` or an
+ `ISourcePackage` and it is linked to an upstream project that uses
+ Launchpad to track bugs, return the `IProduct`. Otherwise,
+ return None
+
+ :returns: `IProduct` or None
+ """
+ product = self.upstream_project
+ if (product is not None and
+ product.bug_tracking_usage == ServiceUsage.LAUNCHPAD):
+ return product
+ return None
+
+ @property
+ def page_title(self):
+ return "Bugs for %s" % self.context.displayname
+
+ label = page_title
+
+ @property
+ def schema(self):
+ """Return the schema that defines the form."""
+ if self._personContext():
+ return IPersonBugTaskSearch
+ elif self.isUpstreamProduct:
+ return IUpstreamProductBugTaskSearch
+ else:
+ return IBugTaskSearch
+
+ @property
+ def feed_links(self):
+ """Prevent conflicts between the page and the atom feed.
+
+ The latest-bugs atom feed matches the default output of this
+ view, but it does not match this view's bug listing when
+ any search parameters are passed in.
+ """
+ if self.request.get('QUERY_STRING', '') == '':
+ # There is no query in this request, so it's okay for this page to
+ # have its feed links.
+ return super(BugTaskSearchListingView, self).feed_links
+ else:
+ # The query changes the results so that they would not match the
+ # feed. In this case, suppress the feed links.
+ return []
+
+ def initialize(self):
+ """Initialize the view with the request.
+
+ Look for old status names and redirect to a new location if found.
+ """
+ query_string = self.request.get('QUERY_STRING')
+ if query_string:
+ query_string_rewritten = (
+ rewrite_old_bugtask_status_query_string(query_string))
+ if query_string_rewritten != query_string:
+ redirect_uri = URI(self.request.getURL()).replace(
+ query=query_string_rewritten)
+ self.request.response.redirect(str(redirect_uri), status=301)
+ return
+
+ self._migrateOldUpstreamStatus()
+ LaunchpadFormView.initialize(self)
+
+ # We call self._validate() here because LaunchpadFormView only
+ # validates the form if an action is submitted but, because this form
+ # can be called through a query string, we don't want to require an
+ # action. We pass an empty dict to _validate() because all the data
+ # needing validation is already available internally to self.
+ self._validate(None, {})
+
+ expose_structural_subscription_data_to_js(
+ self.context, self.request, self.user)
+ can_view = (IPrivacy(self.context, None) is None
+ or check_permission('launchpad.View', self.context))
+ if (can_view and
+ not FeedsLayer.providedBy(self.request) and
+ not self.request.form.get('advanced')):
+ cache = IJSONRequestCache(self.request)
+ view_names = set(reg.name for reg
+ in iter_view_registrations(self.__class__))
+ if len(view_names) != 1:
+ raise AssertionError("Ambiguous view name.")
+ cache.objects['view_name'] = view_names.pop()
+ batch_navigator = self.search()
+ cache.objects['mustache_model'] = batch_navigator.model
+ cache.objects['field_visibility'] = (
+ batch_navigator.field_visibility)
+ cache.objects['field_visibility_defaults'] = (
+ batch_navigator.field_visibility_defaults)
+ cache.objects['cbl_cookie_name'] = (
+ batch_navigator.getCookieName())
+
+ def _getBatchInfo(batch):
+ if batch is None:
+ return None
+ return {'memo': batch.range_memo,
+ 'start': batch.startNumber() - 1}
+
+ next_batch = batch_navigator.batch.nextBatch()
+ cache.objects['next'] = _getBatchInfo(next_batch)
+ prev_batch = batch_navigator.batch.prevBatch()
+ cache.objects['prev'] = _getBatchInfo(prev_batch)
+ cache.objects['total'] = batch_navigator.batch.total()
+ cache.objects['order_by'] = ','.join(
+ get_sortorder_from_request(self.request))
+ cache.objects['forwards'] = (
+ batch_navigator.batch.range_forwards)
+ last_batch = batch_navigator.batch.lastBatch()
+ cache.objects['last_start'] = last_batch.startNumber() - 1
+ cache.objects.update(_getBatchInfo(batch_navigator.batch))
+ cache.objects['sort_keys'] = SORT_KEYS
+
+ @property
+ def show_config_portlet(self):
+ if (IDistribution.providedBy(self.context) or
+ IProduct.providedBy(self.context)):
+ return True
+ else:
+ return False
+
+ @property
+ def columns_to_show(self):
+ """Returns a sequence of column names to be shown in the listing."""
+ upstream_context = self._upstreamContext()
+ productseries_context = self._productSeriesContext()
+ project_context = self._projectContext()
+ distribution_context = self._distributionContext()
+ distroseries_context = self._distroSeriesContext()
+ distrosourcepackage_context = self._distroSourcePackageContext()
+ sourcepackage_context = self._sourcePackageContext()
+
+ if (upstream_context or productseries_context or
+ distrosourcepackage_context or sourcepackage_context):
+ return ["id", "summary", "importance", "status", "heat"]
+ elif distribution_context or distroseries_context:
+ return [
+ "id", "summary", "packagename", "importance", "status",
+ "heat"]
+ elif project_context:
+ return [
+ "id", "summary", "productname", "importance", "status",
+ "heat"]
+ else:
+ raise AssertionError(
+ "Unrecognized context; don't know which report "
+ "columns to show.")
+
+ bugtask_table_template = ViewPageTemplateFile(
+ '../templates/bugs-table-include.pt')
+
+ @property
+ def template(self):
+ query_string = self.request.get('QUERY_STRING') or ''
+ query_params = urlparse.parse_qs(query_string)
+ if 'batch_request' in query_params:
+ return self.bugtask_table_template
+ else:
+ return super(BugTaskSearchListingView, self).template
+
+ def validate_search_params(self):
+ """Validate the params passed for the search.
+
+ An UnexpectedFormData exception is raised if the user submitted a URL
+ that could not have been created from the UI itself.
+ """
+ # The only way the user should get these field values incorrect is
+ # through a stale bookmark or a hand-hacked URL.
+ for field_name in ("status", "importance", "milestone", "component",
+ "status_upstream"):
+ if self.getFieldError(field_name):
+ raise UnexpectedFormData(
+ "Unexpected value for field '%s'. Perhaps your bookmarks "
+ "are out of date or you changed the URL by hand?" %
+ field_name)
+
+ orderby = get_sortorder_from_request(self.request)
+ for orderby_col in orderby:
+ if orderby_col.startswith("-"):
+ orderby_col = orderby_col[1:]
+
+ try:
+ orderby_expression[orderby_col]
+ except KeyError:
+ raise UnexpectedFormData(
+ "Unknown sort column '%s'" % orderby_col)
+
+ def setUpWidgets(self):
+ """Customize the onKeyPress event of the assignee chooser."""
+ LaunchpadFormView.setUpWidgets(self)
+
+ self.widgets["assignee"].onKeyPress = (
+ "selectWidget('assignee_option', event)")
+
+ def validate(self, data):
+ """Validates the form."""
+ self.validateVocabulariesAdvancedForm()
+ self.validate_search_params()
+
+ def _migrateOldUpstreamStatus(self):
+ """Converts old upstream status value parameters to new ones.
+
+ Before Launchpad version 1.1.6 (build 4412), the upstream parameter
+ in the request was a single string value, coming from a set of
+ radio buttons. From that version on, the user can select multiple
+ values in the web UI. In order to keep old bookmarks working,
+ convert the old string parameter into a list.
+ """
+ old_upstream_status_values_to_new_values = {
+ 'only_resolved_upstream': 'resolved_upstream'}
+
+ status_upstream = self.request.get('field.status_upstream')
+ if status_upstream in old_upstream_status_values_to_new_values.keys():
+ self.request.form['field.status_upstream'] = [
+ old_upstream_status_values_to_new_values[status_upstream]]
+ elif status_upstream == '':
+ del self.request.form['field.status_upstream']
+ else:
+ # The value of status_upstream is either correct, so nothing to
+ # do, or it has some other error, which is handled in
+ # LaunchpadFormView's own validation.
+ pass
+
+ def buildSearchParams(self, searchtext=None, extra_params=None):
+ """Build the BugTaskSearchParams object for the given arguments and
+ values specified by the user on this form's widgets.
+ """
+ # Calling _validate populates the data dictionary as a side-effect
+ # of validation.
+ data = {}
+ self._validate(None, data)
+
+ if extra_params:
+ data.update(extra_params)
+
+ if data:
+ searchtext = data.get("searchtext")
+ if searchtext and searchtext.isdigit():
+ try:
+ bug = getUtility(IBugSet).get(searchtext)
+ except NotFoundError:
+ pass
+ else:
+ self.request.response.redirect(canonical_url(bug))
+
+ assignee_option = self.request.form.get("assignee_option")
+ if assignee_option == "none":
+ data['assignee'] = NULL
+
+ has_patch = data.pop("has_patch", False)
+ if has_patch:
+ data["attachmenttype"] = BugAttachmentType.PATCH
+
+ has_branches = data.get('has_branches', True)
+ has_no_branches = data.get('has_no_branches', True)
+ if has_branches and not has_no_branches:
+ data['linked_branches'] = BugBranchSearch.BUGS_WITH_BRANCHES
+ elif not has_branches and has_no_branches:
+ data['linked_branches'] = (
+ BugBranchSearch.BUGS_WITHOUT_BRANCHES)
+ else:
+ data['linked_branches'] = BugBranchSearch.ALL
+
+ has_blueprints = data.get('has_blueprints', True)
+ has_no_blueprints = data.get('has_no_blueprints', True)
+ if has_blueprints and not has_no_blueprints:
+ data['linked_blueprints'] = (
+ BugBlueprintSearch.BUGS_WITH_BLUEPRINTS)
+ elif not has_blueprints and has_no_blueprints:
+ data['linked_blueprints'] = (
+ BugBlueprintSearch.BUGS_WITHOUT_BLUEPRINTS)
+ else:
+ data['linked_blueprints'] = BugBlueprintSearch.ALL
+
+ # Filter appropriately if the user wants to restrict the
+ # search to only bugs with no package information.
+ has_no_package = data.pop("has_no_package", False)
+ if has_no_package:
+ data["sourcepackagename"] = NULL
+
+ self._buildUpstreamStatusParams(data)
+
+ # "Normalize" the form data into search arguments.
+ form_values = {}
+ for key, value in data.items():
+ if key in ('tag'):
+ # Skip tag-related parameters, they
+ # are handled later on.
+ continue
+ if zope_isinstance(value, (list, tuple)):
+ if len(value) > 0:
+ form_values[key] = any(*value)
+ else:
+ form_values[key] = value
+
+ if 'tag' in data:
+ # Tags require special handling, since they can be used
+ # to search either inclusively or exclusively.
+ # We take a look at the `tags_combinator` field, and wrap
+ # the tag list in the appropriate search directive (either
+ # `any` or `all`). If no value is supplied, we assume `any`,
+ # in order to remain compatible with old saved search URLs.
+ tags = data['tag']
+ tags_combinator_all = (
+ 'tags_combinator' in data and
+ data['tags_combinator'] == BugTagsSearchCombinator.ALL)
+ if zope_isinstance(tags, (list, tuple)) and len(tags) > 0:
+ if tags_combinator_all:
+ form_values['tag'] = all(*tags)
+ else:
+ form_values['tag'] = any(*tags)
+ else:
+ form_values['tag'] = tags
+
+ search_params = get_default_search_params(self.user)
+ search_params.orderby = get_sortorder_from_request(self.request)
+ for name, value in form_values.items():
+ setattr(search_params, name, value)
+ return search_params
+
+ def _buildUpstreamStatusParams(self, data):
+ """ Convert the status_upstream value to parameters we can
+ send to BugTaskSet.search().
+ """
+ if 'status_upstream' in data:
+ status_upstream = data['status_upstream']
+ if 'pending_bugwatch' in status_upstream:
+ data['pending_bugwatch_elsewhere'] = True
+ if 'resolved_upstream' in status_upstream:
+ data['resolved_upstream'] = True
+ if 'open_upstream' in status_upstream:
+ data['open_upstream'] = True
+ if 'hide_upstream' in status_upstream:
+ data['has_no_upstream_bugtask'] = True
+ del data['status_upstream']
+
+ def _getBatchNavigator(self, tasks):
+ """Return the batch navigator to be used to batch the bugtasks."""
+ return BugListingBatchNavigator(
+ tasks, self.request, columns_to_show=self.columns_to_show,
+ size=config.malone.buglist_batch_size,
+ target_context=self.context)
+
+ def buildBugTaskSearchParams(self, searchtext=None, extra_params=None):
+ """Build the parameters to submit to the `searchTasks` method.
+
+ Use the data submitted in the form to populate a dictionary
+ which, when expanded (using **params notation) can serve as the
+ input for searchTasks().
+ """
+
+ # We force the view to populate the data dictionary by calling
+ # _validate here.
+ data = {}
+ self._validate(None, data)
+
+ searchtext = data.get("searchtext")
+ if searchtext and searchtext.isdigit():
+ try:
+ bug = getUtility(IBugSet).get(searchtext)
+ except NotFoundError:
+ pass
+ else:
+ self.request.response.redirect(canonical_url(bug))
+
+ if extra_params:
+ data.update(extra_params)
+
+ params = {}
+
+ # A mapping of parameters that appear in the destination
+ # with a different name, or are being dropped altogether.
+ param_names_map = {
+ 'searchtext': 'search_text',
+ 'omit_dupes': 'omit_duplicates',
+ 'subscriber': 'bug_subscriber',
+ 'tag': 'tags',
+ # The correct value is being retrieved
+ # using get_sortorder_from_request()
+ 'orderby': None,
+ }
+
+ for key, value in data.items():
+ if key in param_names_map:
+ param_name = param_names_map[key]
+ if param_name is not None:
+ params[param_name] = value
+ else:
+ params[key] = value
+
+ assignee_option = self.request.form.get("assignee_option")
+ if assignee_option == "none":
+ params['assignee'] = NULL
+
+ params['order_by'] = get_sortorder_from_request(self.request)
+
+ return params
+
+ def search(self, searchtext=None, context=None, extra_params=None):
+ """Return an `ITableBatchNavigator` for the GET search criteria.
+
+ :param searchtext: Text that must occur in the bug report. If
+ searchtext is None, the search text will be gotten from the
+ request.
+
+ :param extra_params: A dict that provides search params added to
+ the search criteria taken from the request. Params in
+ `extra_params` take precedence over request params.
+ """
+ if self._batch_navigator is None:
+ unbatchedTasks = self.searchUnbatched(
+ searchtext, context, extra_params)
+ self._batch_navigator = self._getBatchNavigator(unbatchedTasks)
+ return self._batch_navigator
+
+ def searchUnbatched(self, searchtext=None, context=None,
+ extra_params=None):
+ """Return a `SelectResults` object for the GET search criteria.
+
+ :param searchtext: Text that must occur in the bug report. If
+ searchtext is None, the search text will be gotten from the
+ request.
+
+ :param extra_params: A dict that provides search params added to
+ the search criteria taken from the request. Params in
+ `extra_params` take precedence over request params.
+ """
+ # Base classes can provide an explicit search context.
+ if not context:
+ context = self.context
+
+ search_params = self.buildSearchParams(
+ searchtext=searchtext, extra_params=extra_params)
+ search_params.user = self.user
+ try:
+ tasks = context.searchTasks(search_params)
+ except ValueError as e:
+ self.request.response.addErrorNotification(str(e))
+ self.request.response.redirect(canonical_url(
+ self.context, rootsite='bugs', view_name='+bugs'))
+ tasks = None
+ return tasks
+
+ def getWidgetValues(
+ self, vocabulary_name=None, vocabulary=None, default_values=()):
+ """Return data used to render a field's widget.
+
+ Either `vocabulary_name` or `vocabulary` must be supplied."""
+ widget_values = []
+
+ if vocabulary is None:
+ assert vocabulary_name is not None, 'No vocabulary specified.'
+ vocabulary = vocabulary_registry.get(
+ self.context, vocabulary_name)
+ for term in vocabulary:
+ widget_values.append(
+ dict(
+ value=term.token, title=term.title or term.token,
+ checked=term.value in default_values))
+ return shortlist(widget_values, longest_expected=12)
+
+ def getStatusWidgetValues(self):
+ """Return data used to render the status checkboxes."""
+ return self.getWidgetValues(
+ vocabulary=BugTaskStatusSearchDisplay,
+ default_values=DEFAULT_SEARCH_BUGTASK_STATUSES_FOR_DISPLAY)
+
+ def getImportanceWidgetValues(self):
+ """Return data used to render the Importance checkboxes."""
+ return self.getWidgetValues(vocabulary=BugTaskImportance)
+
+ def getInformationTypeWidgetValues(self):
+ """Return data used to render the Information Type checkboxes."""
+ if (IProduct.providedBy(self.context)
+ or IDistribution.providedBy(self.context)):
+ vocab = InformationTypeVocabulary(
+ types=self.context.getAllowedBugInformationTypes())
+ else:
+ vocab = InformationType
+ return self.getWidgetValues(vocabulary=vocab)
+
+ def getMilestoneWidgetValues(self):
+ """Return data used to render the milestone checkboxes."""
+ return self.getWidgetValues("MilestoneWithDateExpected")
+
+ def getSimpleSearchURL(self):
+ """Return a URL that can be used as an href to the simple search."""
+ return canonical_url(self.context) + "/+bugs"
+
+ def shouldShowAssigneeWidget(self):
+ """Should the assignee widget be shown on the advanced search page?"""
+ return True
+
+ def shouldShowCommenterWidget(self):
+ """Show the commenter widget on the advanced search page?"""
+ return True
+
+ def shouldShowComponentWidget(self):
+ """Show the component widget on the advanced search page?"""
+ context = self.context
+ return (
+ (IDistribution.providedBy(context) and
+ context.currentseries is not None) or
+ IDistroSeries.providedBy(context) or
+ ISourcePackage.providedBy(context))
+
+ def shouldShowStructuralSubscriberWidget(self):
+ """Should the structural subscriber widget be shown on the page?
+
+ Show the widget when there are subordinate structures.
+ """
+ return self.structural_subscriber_label is not None
+
+ def shouldShowNoPackageWidget(self):
+ """Should the widget to filter on bugs with no package be shown?
+
+ The widget will be shown only on a distribution or
+ distroseries's advanced search page.
+ """
+ return (IDistribution.providedBy(self.context) or
+ IDistroSeries.providedBy(self.context))
+
+ def shouldShowReporterWidget(self):
+ """Should the reporter widget be shown on the advanced search page?"""
+ return True
+
+ def shouldShowReleaseCriticalPortlet(self):
+ """Should the page include a portlet showing release-critical bugs
+ for different series.
+ """
+ return (
+ IDistribution.providedBy(self.context) and self.context.series
+ or IDistroSeries.providedBy(self.context)
+ or IProduct.providedBy(self.context) and self.context.series
+ or IProductSeries.providedBy(self.context))
+
+ def shouldShowSubscriberWidget(self):
+ """Show the subscriber widget on the advanced search page?"""
+ return True
+
+ def shouldShowUpstreamStatusBox(self):
+ """Should the upstream status filtering widgets be shown?"""
+ return self.isUpstreamProduct or not (
+ IProduct.providedBy(self.context) or
+ IProjectGroup.providedBy(self.context))
+
+ def shouldShowTeamPortlet(self):
+ """Should the User's Teams portlet me shown in the results?"""
+ return False
+
+ @property
+ def structural_subscriber_label(self):
+ if IDistribution.providedBy(self.context):
+ return 'Package or series subscriber'
+ elif IDistroSeries.providedBy(self.context):
+ return 'Package subscriber'
+ elif IProduct.providedBy(self.context):
+ return 'Series subscriber'
+ elif IProjectGroup.providedBy(self.context):
+ return 'Project or series subscriber'
+ elif IPerson.providedBy(self.context):
+ return 'Project, distribution, package, or series subscriber'
+ else:
+ return None
+
+ def shouldShowTargetName(self):
+ """Should the bug target name be displayed in the list of results?
+
+ This is mainly useful for the listview.
+ """
+ # It doesn't make sense to show the target name when viewing product
+ # bugs.
+ if IProduct.providedBy(self.context):
+ return False
+ else:
+ return True
+
+ def shouldShowAdvancedForm(self):
+ """Return True if the advanced form should be shown, or False."""
+ if (self.request.form.get('advanced')
+ or self.form_has_errors):
+ return True
+ else:
+ return False
+
+ @property
+ def should_show_bug_information(self):
+ return self.bug_tracking_usage == ServiceUsage.LAUNCHPAD
+
+ @property
+ def form_has_errors(self):
+ """Return True if the form has errors, otherwise False."""
+ return len(self.errors) > 0
+
+ def validateVocabulariesAdvancedForm(self):
+ """Provides a meaningful message for vocabulary validation errors."""
+ error_message = _(
+ "There's no person with the name or email address '%s'.")
+
+ for name in ('assignee', 'bug_reporter', 'structural_subscriber',
+ 'bug_commenter', 'subscriber'):
+ if self.getFieldError(name):
+ self.setFieldError(
+ name, error_message %
+ self.request.get('field.%s' % name))
+
+ @property
+ def isUpstreamProduct(self):
+ """Is the context a Product that does not use Malone?"""
+ return (
+ IProduct.providedBy(self.context)
+ and self.context.bug_tracking_usage != ServiceUsage.LAUNCHPAD)
+
+ def _upstreamContext(self):
+ """Is this page being viewed in an upstream context?
+
+ Return the IProduct if yes, otherwise return None.
+ """
+ return IProduct(self.context, None)
+
+ def _productSeriesContext(self):
+ """Is this page being viewed in a product series context?
+
+ Return the IProductSeries if yes, otherwise return None.
+ """
+ return IProductSeries(self.context, None)
+
+ def _projectContext(self):
+ """Is this page being viewed in a project context?
+
+ Return the IProjectGroup if yes, otherwise return None.
+ """
+ return IProjectGroup(self.context, None)
+
+ def _personContext(self):
+ """Is this page being viewed in a person context?
+
+ Return the IPerson if yes, otherwise return None.
+ """
+ return IPerson(self.context, None)
+
+ def _distributionContext(self):
+ """Is this page being viewed in a distribution context?
+
+ Return the IDistribution if yes, otherwise return None.
+ """
+ return IDistribution(self.context, None)
+
+ def _distroSeriesContext(self):
+ """Is this page being viewed in a distroseries context?
+
+ Return the IDistroSeries if yes, otherwise return None.
+ """
+ return IDistroSeries(self.context, None)
+
+ def _sourcePackageContext(self):
+ """Is this view in a [distroseries] sourcepackage context?
+
+ Return the ISourcePackage if yes, otherwise return None.
+ """
+ return ISourcePackage(self.context, None)
+
+ def _distroSourcePackageContext(self):
+ """Is this page being viewed in a distribution sourcepackage context?
+
+ Return the IDistributionSourcePackage if yes, otherwise return None.
+ """
+ return IDistributionSourcePackage(self.context, None)
+
+ @property
+ def addquestion_url(self):
+ """Return the URL for the +addquestion view for the context."""
+ if IQuestionTarget.providedBy(self.context):
+ answers_usage = IServiceUsage(self.context).answers_usage
+ if answers_usage == ServiceUsage.LAUNCHPAD:
+ return canonical_url(
+ self.context, rootsite='answers',
+ view_name='+addquestion')
+ else:
+ return None
+
+
+class BugTargetView(LaunchpadView):
+ """Used to grab bugs for a bug target; used by the latest bugs portlet"""
+
+ def latestBugTasks(self, quantity=5):
+ """Return <quantity> latest bugs reported against this target."""
+ params = BugTaskSearchParams(orderby="-datecreated",
+ omit_dupes=True,
+ user=getUtility(ILaunchBag).user)
+
+ tasklist = self.context.searchTasks(params)
+ return tasklist[:quantity]
+
+ def getMostRecentlyUpdatedBugTasks(self, limit=5):
+ """Return the most recently updated bugtasks for this target."""
+ params = BugTaskSearchParams(
+ orderby="-date_last_updated", omit_dupes=True, user=self.user)
+ return list(self.context.searchTasks(params)[:limit])
+
+
+class TextualBugTaskSearchListingView(BugTaskSearchListingView):
+ """View that renders a list of bug IDs for a given set of search criteria.
+ """
+
+ def render(self):
+ """Render the BugTarget for text display."""
+ self.request.response.setHeader(
+ 'Content-type', 'text/plain')
+
+ # This uses the BugTaskSet internal API instead of using the
+ # standard searchTasks() because the latter can retrieve a lot
+ # of bugs and we don't want to load all of that data in memory.
+ # Retrieving only the bug numbers is much more efficient.
+ search_params = self.buildSearchParams()
+
+ # XXX flacoste 2008/04/24 This should be moved to a
+ # BugTaskSearchParams.setTarget().
+ if (IDistroSeries.providedBy(self.context) or
+ IProductSeries.providedBy(self.context)):
+ search_params.setTarget(self.context)
+ elif IDistribution.providedBy(self.context):
+ search_params.setDistribution(self.context)
+ elif IProduct.providedBy(self.context):
+ search_params.setProduct(self.context)
+ elif IProjectGroup.providedBy(self.context):
+ search_params.setProject(self.context)
+ elif (ISourcePackage.providedBy(self.context) or
+ IDistributionSourcePackage.providedBy(self.context)):
+ search_params.setSourcePackage(self.context)
+ else:
+ raise AssertionError('Unknown context type: %s' % self.context)
+
+ return u"".join("%d\n" % bug_id for bug_id in
+ getUtility(IBugTaskSet).searchBugIds(search_params))
+
+
+class BugsBugTaskSearchListingView(BugTaskSearchListingView):
+ """Search all bug reports."""
+
+ columns_to_show = ["id", "summary", "bugtargetdisplayname",
+ "importance", "status", "heat"]
+ schema = IFrontPageBugTaskSearch
+ custom_widget('scope', ProjectScopeWidget)
+ page_title = 'Search'
+
+ def initialize(self):
+ """Initialize the view for the request."""
+ BugTaskSearchListingView.initialize(self)
+ if not self._isRedirected():
+ self._redirectToSearchContext()
+
+ def _redirectToSearchContext(self):
+ """Check whether a target was given and redirect to it.
+
+ All the URL parameters will be passed on to the target's +bugs
+ page.
+
+ If the target widget contains errors, redirect to the front page
+ which will handle the error.
+ """
+ try:
+ search_target = self.widgets['scope'].getInputValue()
+ except InputErrors:
+ query_string = self.request['QUERY_STRING']
+ bugs_url = "%s?%s" % (canonical_url(self.context), query_string)
+ self.request.response.redirect(bugs_url)
+ else:
+ if search_target is not None:
+ query_string = self.request['QUERY_STRING']
+ search_url = "%s/+bugs?%s" % (
+ canonical_url(search_target), query_string)
+ self.request.response.redirect(search_url)
+
+ def getSearchPageHeading(self):
+ """Return the heading to search all Bugs."""
+ return "Search all bug reports"
+
+ @property
+ def label(self):
+ return self.getSearchPageHeading()
+
+
+class BugTaskExpirableListingView(BugTaskSearchListingView):
+ """View for listing Incomplete bugs that can expire."""
+
+ @property
+ def can_show_expirable_bugs(self):
+ """Return True or False if expirable bug listing can be shown."""
+ return target_has_expirable_bugs_listing(self.context)
+
+ @property
+ def inactive_expiration_age(self):
+ """Return the number of days an bug must be inactive to expire."""
+ return config.malone.days_before_expiration
+
+ @property
+ def columns_to_show(self):
+ """Show the columns that summarise expirable bugs."""
+ if (IDistribution.providedBy(self.context)
+ or IDistroSeries.providedBy(self.context)):
+ return [
+ 'id', 'summary', 'packagename', 'date_last_updated', 'heat']
+ else:
+ return ['id', 'summary', 'date_last_updated', 'heat']
+
+ def search(self):
+ """Return an `ITableBatchNavigator` for the expirable bugtasks."""
+ bugtaskset = getUtility(IBugTaskSet)
+ bugtasks = bugtaskset.findExpirableBugTasks(
+ user=self.user, target=self.context, min_days_old=0)
+ return BugListingBatchNavigator(
+ bugtasks, self.request, columns_to_show=self.columns_to_show,
+ size=config.malone.buglist_batch_size)
+
+ @property
+ def page_title(self):
+ return "Bugs that can expire in %s" % self.context.title
+
+
+class BugNominationsView(BugTaskSearchListingView):
+ """View for accepting/declining bug nominations."""
+
+ def search(self):
+ """Return all the nominated tasks for this series."""
+ if IDistroSeries.providedBy(self.context):
+ main_context = self.context.distribution
+ elif IProductSeries.providedBy(self.context):
+ main_context = self.context.product
+ else:
+ raise AssertionError(
+ 'Unknown nomination target: %r' % self.context)
+ return BugTaskSearchListingView.search(
+ self, context=main_context,
+ extra_params=dict(nominated_for=self.context))
=== modified file 'lib/lp/bugs/browser/bugtask.py'
--- lib/lp/bugs/browser/bugtask.py 2014-11-29 05:04:02 +0000
+++ lib/lp/bugs/browser/bugtask.py 2014-11-29 05:04:02 +0000
@@ -6,26 +6,16 @@
__metaclass__ = type
__all__ = [
- 'BugListingBatchNavigator',
- 'BugListingPortletInfoView',
- 'BugListingPortletStatsView',
- 'BugNominationsView',
- 'BugsBugTaskSearchListingView',
'bugtarget_renderer',
'BugTargetTraversalMixin',
- 'BugTargetView',
'BugTaskBreadcrumb',
'BugTaskContextMenu',
'BugTaskCreateQuestionView',
'BugTaskDeletionView',
'BugTaskEditView',
- 'BugTaskExpirableListingView',
- 'BugTaskListingItem',
- 'BugTaskListingView',
'BugTaskNavigation',
'BugTaskPrivacyAdapter',
'BugTaskRemoveQuestionView',
- 'BugTaskSearchListingView',
'BugTasksNominationsView',
'BugTasksTableView',
'BugTaskTableRowView',
@@ -33,14 +23,10 @@
'BugTaskView',
'can_add_package_task_to_bug',
'can_add_project_task_to_bug',
- 'get_buglisting_search_filter_url',
'get_comments_for_bugtask',
- 'get_sortorder_from_request',
'get_visible_comments',
- 'TextualBugTaskSearchListingView',
]
-import cgi
from collections import defaultdict
from datetime import (
datetime,
@@ -48,10 +34,8 @@
)
from itertools import groupby
from operator import attrgetter
-import os.path
import re
import urllib
-import urlparse
from lazr.delegates import delegates
from lazr.lifecycle.event import ObjectModifiedEvent
@@ -64,33 +48,23 @@
IWebServiceClientRequest,
)
from lazr.restful.utils import smartquote
-from lazr.uri import URI
-import pystache
from pytz import utc
from simplejson import dumps
-from simplejson.encoder import JSONEncoderForHTML
import transaction
from z3c.pt.pagetemplate import ViewPageTemplateFile
-from zope import (
- component,
- formlib,
- )
-from zope.authentication.interfaces import IUnauthenticatedPrincipal
+from zope import formlib
from zope.component import (
+ adapter,
ComponentLookupError,
getAdapter,
getMultiAdapter,
getUtility,
- queryMultiAdapter,
)
from zope.event import notify
-from zope.formlib.interfaces import InputErrors
-from zope.formlib.itemswidgets import RadioWidget
from zope.formlib.widget import CustomWidgetFactory
from zope.interface import (
implementer,
implements,
- Interface,
providedBy,
)
from zope.schema import Choice
@@ -99,16 +73,11 @@
SimpleVocabulary,
)
from zope.security.interfaces import Unauthorized
-from zope.security.proxy import (
- isinstance as zope_isinstance,
- removeSecurityProxy,
- )
+from zope.security.proxy import removeSecurityProxy
from zope.traversing.browser import absoluteURL
from zope.traversing.interfaces import IPathAdapter
from lp import _
-from lp.answers.interfaces.questiontarget import IQuestionTarget
-from lp.app.browser.launchpad import iter_view_registrations
from lp.app.browser.launchpadform import (
action,
custom_widget,
@@ -122,31 +91,11 @@
vocabulary_to_choice_edit_items,
)
from lp.app.browser.stringformatter import FormattersAPI
-from lp.app.browser.tales import (
- BugTrackerFormatterAPI,
- DateTimeFormatterAPI,
- ObjectImageDisplayAPI,
- PersonFormatterAPI,
- )
+from lp.app.browser.tales import ObjectImageDisplayAPI
from lp.app.browser.vocabulary import vocabulary_filters
-from lp.app.enums import (
- InformationType,
- PROPRIETARY_INFORMATION_TYPES,
- ServiceUsage,
- )
-from lp.app.errors import (
- NotFoundError,
- UnexpectedFormData,
- )
-from lp.app.interfaces.launchpad import (
- ILaunchpadCelebrities,
- IPrivacy,
- IServiceUsage,
- )
-from lp.app.vocabularies import InformationTypeVocabulary
-from lp.app.widgets.itemswidgets import LabeledMultiCheckBoxWidget
-from lp.app.widgets.popup import PersonPickerWidget
-from lp.app.widgets.project import ProjectScopeWidget
+from lp.app.enums import PROPRIETARY_INFORMATION_TYPES
+from lp.app.errors import NotFoundError
+from lp.app.interfaces.launchpad import ILaunchpadCelebrities
from lp.bugs.browser.bug import (
BugContextMenu,
BugTextView,
@@ -156,10 +105,6 @@
build_comments_from_chunks,
group_comments_with_activity,
)
-from lp.bugs.browser.structuralsubscription import (
- expose_structural_subscription_data_to_js,
- )
-from lp.bugs.browser.widgets.bug import BugTagsWidget
from lp.bugs.browser.widgets.bugtask import (
AssigneeDisplayWidget,
BugTaskAssigneeWidget,
@@ -167,7 +112,6 @@
BugTaskSourcePackageNameWidget,
BugTaskTargetWidget,
DBItemDisplayWidget,
- NewLineToSpacesWidget,
)
from lp.bugs.interfaces.bug import (
IBug,
@@ -186,39 +130,20 @@
from lp.bugs.interfaces.bugtask import (
BugTaskImportance,
BugTaskStatus,
- BugTaskStatusSearch,
- BugTaskStatusSearchDisplay,
CannotDeleteBugtask,
IBugTask,
IBugTaskSet,
ICreateQuestionFromBugTaskForm,
IllegalTarget,
IRemoveQuestionFromBugTaskForm,
- UNRESOLVED_BUGTASK_STATUSES,
UserCannotEditBugTaskStatus,
)
-from lp.bugs.interfaces.bugtasksearch import (
- BugBlueprintSearch,
- BugBranchSearch,
- BugTagsSearchCombinator,
- BugTaskSearchParams,
- DEFAULT_SEARCH_BUGTASK_STATUSES_FOR_DISPLAY,
- IBugTaskSearch,
- IFrontPageBugTaskSearch,
- IPersonBugTaskSearch,
- IUpstreamProductBugTaskSearch,
- )
-from lp.bugs.interfaces.bugtracker import (
- BugTrackerType,
- IHasExternalBugTracker,
- )
+from lp.bugs.interfaces.bugtasksearch import BugTaskSearchParams
+from lp.bugs.interfaces.bugtracker import BugTrackerType
from lp.bugs.interfaces.bugwatch import BugWatchActivityStatus
from lp.bugs.interfaces.cve import ICveSet
-from lp.bugs.interfaces.malone import IMaloneApplication
-from lp.bugs.model.bugtasksearch import orderby_expression
from lp.bugs.vocabularies import BugTaskMilestoneVocabulary
from lp.code.interfaces.branchcollection import IAllBranches
-from lp.layers import FeedsLayer
from lp.registry.interfaces.distribution import (
IDistribution,
IDistributionSet,
@@ -230,42 +155,25 @@
IDistroSeries,
IDistroSeriesSet,
)
-from lp.registry.interfaces.person import (
- IPerson,
- IPersonSet,
- )
+from lp.registry.interfaces.person import IPersonSet
from lp.registry.interfaces.product import IProduct
from lp.registry.interfaces.productseries import IProductSeries
-from lp.registry.interfaces.projectgroup import IProjectGroup
from lp.registry.interfaces.sourcepackage import ISourcePackage
from lp.registry.model.personroles import PersonRoles
from lp.services.config import config
from lp.services.features import getFeatureFlag
-from lp.services.feeds.browser import (
- BugTargetLatestBugsFeedLink,
- FeedsMixin,
- )
+from lp.services.feeds.browser import FeedsMixin
from lp.services.fields import PersonChoice
-from lp.services.helpers import shortlist
from lp.services.mail.notification import get_unified_diff
from lp.services.privacy.interfaces import IObjectPrivacy
from lp.services.propertycache import (
cachedproperty,
get_property_cache,
)
-from lp.services.searchbuilder import (
- all,
- any,
- NULL,
- )
-from lp.services.utils import obfuscate_structure
from lp.services.webapp import (
canonical_url,
- enabled_with_permission,
LaunchpadView,
- Link,
Navigation,
- NavigationMenu,
redirection,
stepthrough,
)
@@ -273,7 +181,6 @@
check_permission,
precache_permission_for_objects,
)
-from lp.services.webapp.batching import TableBatchNavigator
from lp.services.webapp.breadcrumb import Breadcrumb
from lp.services.webapp.escaping import (
html_escape,
@@ -284,24 +191,8 @@
vocabulary_registry = getVocabularyRegistry()
-DISPLAY_BUG_STATUS_FOR_PATCHES = {
- BugTaskStatus.NEW: True,
- BugTaskStatus.INCOMPLETE: True,
- BugTaskStatus.INVALID: False,
- BugTaskStatus.WONTFIX: False,
- BugTaskStatus.CONFIRMED: True,
- BugTaskStatus.TRIAGED: True,
- BugTaskStatus.INPROGRESS: True,
- BugTaskStatus.FIXCOMMITTED: True,
- BugTaskStatus.FIXRELEASED: False,
- BugTaskStatus.UNKNOWN: False,
- BugTaskStatus.EXPIRED: False,
- BugTaskStatusSearch.INCOMPLETE_WITHOUT_RESPONSE: True,
- BugTaskStatusSearch.INCOMPLETE_WITH_RESPONSE: True,
- }
-
-
-@component.adapter(IBugTask, IReference, IWebServiceClientRequest)
+
+@adapter(IBugTask, IReference, IWebServiceClientRequest)
@implementer(IFieldHTMLRenderer)
def bugtarget_renderer(context, field, request):
"""Render a bugtarget as a link."""
@@ -412,107 +303,6 @@
return visible_comments
-def get_sortorder_from_request(request):
- """Get the sortorder from the request.
-
- >>> from lp.services.webapp.servers import LaunchpadTestRequest
- >>> get_sortorder_from_request(LaunchpadTestRequest(form={}))
- ['-importance']
- >>> get_sortorder_from_request(
- ... LaunchpadTestRequest(form={'orderby': '-status'}))
- ['-status']
- >>> get_sortorder_from_request(LaunchpadTestRequest(
- ... form={'orderby': 'status,-severity,importance'}))
- ['status', 'importance']
- >>> get_sortorder_from_request(
- ... LaunchpadTestRequest(form={'orderby': 'priority,-severity'}))
- ['-importance']
- """
- order_by_string = request.get("orderby", '')
- if order_by_string:
- if not zope_isinstance(order_by_string, list):
- order_by = order_by_string.split(',')
- else:
- order_by = order_by_string
- else:
- order_by = []
- # Remove old order_by values that people might have in bookmarks.
- for old_order_by_column in ['priority', 'severity']:
- if old_order_by_column in order_by:
- order_by.remove(old_order_by_column)
- if '-' + old_order_by_column in order_by:
- order_by.remove('-' + old_order_by_column)
- if order_by:
- return order_by
- else:
- # No sort ordering specified, so use a reasonable default.
- return ["-importance"]
-
-
-def get_default_search_params(user):
- """Return a BugTaskSearchParams instance with default values.
-
- By default, a search includes any bug that is unresolved and not a
- duplicate of another bug.
-
- If this search will be used to display a list of bugs to the user
- it may be a good idea to set the orderby attribute using
- get_sortorder_from_request():
-
- params = get_default_search_params(user)
- params.orderby = get_sortorder_from_request(request)
-
- """
- return BugTaskSearchParams(
- user=user, status=any(*UNRESOLVED_BUGTASK_STATUSES), omit_dupes=True)
-
-
-OLD_BUGTASK_STATUS_MAP = {
- 'Unconfirmed': 'New',
- 'Needs Info': 'Incomplete',
- 'Rejected': 'Invalid',
- }
-
-
-def rewrite_old_bugtask_status_query_string(query_string):
- """Return a query string with old status names replaced with new.
-
- If an old status string has been used in the query, construct a
- corrected query string for the search, else return the original
- query string.
- """
- query_elements = cgi.parse_qsl(
- query_string, keep_blank_values=True, strict_parsing=False)
- query_elements_mapped = []
-
- for name, value in query_elements:
- if name == 'field.status:list':
- value = OLD_BUGTASK_STATUS_MAP.get(value, value)
- query_elements_mapped.append((name, value))
-
- if query_elements == query_elements_mapped:
- return query_string
- else:
- return urllib.urlencode(query_elements_mapped, doseq=True)
-
-
-def target_has_expirable_bugs_listing(target):
- """Return True or False if the target has the expirable-bugs listing.
-
- The target must be a Distribution, DistroSeries, Product, or
- ProductSeries, and the pillar must have enabled bug expiration.
- """
- if IDistribution.providedBy(target) or IProduct.providedBy(target):
- return target.enable_bug_expiration
- elif IProductSeries.providedBy(target):
- return target.product.enable_bug_expiration
- elif IDistroSeries.providedBy(target):
- return target.distribution.enable_bug_expiration
- else:
- # This context is not a supported bugtarget.
- return False
-
-
class BugTargetTraversalMixin:
"""Mix-in in class that provides .../+bug/NNN traversal."""
@@ -1829,1487 +1619,6 @@
return view.render()
-class BugTaskListingView(LaunchpadView):
- """A view designed for displaying bug tasks in lists."""
- # Note that this right now is only used in tests and to render
- # status in the CVEReportView. It may be a candidate for refactoring
- # or removal.
- @property
- def status(self):
- """Return an HTML representation of the bugtask status.
-
- The assignee is included.
- """
- bugtask = self.context
- assignee = bugtask.assignee
- status = bugtask.status
- status_title = status.title.capitalize()
-
- if not assignee:
- return status_title + ' (unassigned)'
- assignee_html = PersonFormatterAPI(assignee).link('+assignedbugs')
-
- if status in (BugTaskStatus.INVALID,
- BugTaskStatus.FIXCOMMITTED):
- return '%s by %s' % (status_title, assignee_html)
- else:
- return '%s, assigned to %s' % (status_title, assignee_html)
-
- @property
- def status_elsewhere(self):
- """Return human-readable representation of the status of this bug
- in other contexts for which it's reported.
- """
- bugtask = self.context
- related_tasks = bugtask.related_tasks
- if not related_tasks:
- return "not filed elsewhere"
-
- fixes_found = len(
- [task for task in related_tasks
- if task.status in (BugTaskStatus.FIXCOMMITTED,
- BugTaskStatus.FIXRELEASED)])
- if fixes_found:
- return "fixed in %d of %d places" % (
- fixes_found, len(bugtask.bug.bugtasks))
- elif len(related_tasks) == 1:
- return "filed in 1 other place"
- else:
- return "filed in %d other places" % len(related_tasks)
-
- def render(self):
- """Make rendering this template-less view not crash."""
- return u""
-
-
-class BugsInfoMixin:
- """Contains properties giving URLs to bug information."""
-
- @property
- def bugs_fixed_elsewhere_url(self):
- """A URL to a list of bugs fixed elsewhere."""
- return "%s?field.status_upstream=resolved_upstream" % (
- canonical_url(self.context, view_name='+bugs'))
-
- @property
- def open_cve_bugs_url(self):
- """A URL to a list of open bugs linked to CVEs."""
- return "%s?field.has_cve=on" % (
- canonical_url(self.context, view_name='+bugs'))
-
- @property
- def open_cve_bugs_has_report(self):
- """Whether or not the context has a CVE report page."""
- return queryMultiAdapter(
- (self.context, self.request), name='+cve') is not None
-
- @property
- def pending_bugwatches_url(self):
- """A URL to a list of bugs that need a bugwatch.
-
- None is returned if the context is not an upstream product.
- """
- if not IProduct.providedBy(self.context):
- return None
- if self.context.bug_tracking_usage == ServiceUsage.LAUNCHPAD:
- return None
- return "%s?field.status_upstream=pending_bugwatch" % (
- canonical_url(self.context, view_name='+bugs'))
-
- @property
- def expirable_bugs_url(self):
- """A URL to a list of bugs that can expire, or None.
-
- If the bugtarget is not a supported implementation, or its pillar
- does not have enable_bug_expiration set to True, None is returned.
- The bugtarget may be an `IDistribution`, `IDistroSeries`, `IProduct`,
- or `IProductSeries`.
- """
- if target_has_expirable_bugs_listing(self.context):
- return canonical_url(self.context, view_name='+expirable-bugs')
- else:
- return None
-
- @property
- def new_bugs_url(self):
- """A URL to a page of new bugs."""
- return get_buglisting_search_filter_url(
- status=BugTaskStatus.NEW.title)
-
- @property
- def inprogress_bugs_url(self):
- """A URL to a page of inprogress bugs."""
- return get_buglisting_search_filter_url(
- status=BugTaskStatus.INPROGRESS.title)
-
- @property
- def open_bugs_url(self):
- """A URL to a list of open bugs."""
- return canonical_url(self.context, view_name='+bugs')
-
- @property
- def critical_bugs_url(self):
- """A URL to a list of critical bugs."""
- return get_buglisting_search_filter_url(
- status=[status.title for status in UNRESOLVED_BUGTASK_STATUSES],
- importance=BugTaskImportance.CRITICAL.title)
-
- @property
- def high_bugs_url(self):
- """A URL to a list of high priority bugs."""
- return get_buglisting_search_filter_url(
- status=[status.title for status in UNRESOLVED_BUGTASK_STATUSES],
- importance=BugTaskImportance.HIGH.title)
-
- @property
- def my_bugs_url(self):
- """A URL to a list of bugs assigned to the user, or None."""
- if self.user is None:
- return None
- else:
- return get_buglisting_search_filter_url(assignee=self.user.name)
-
- @property
- def my_affecting_bugs_url(self):
- """A URL to a list of bugs affecting the current user, or None if
- there is no current user.
- """
- if self.user is None:
- return None
- return get_buglisting_search_filter_url(
- affecting_me=True,
- orderby='-date_last_updated')
-
- @property
- def my_reported_bugs_url(self):
- """A URL to a list of bugs reported by the user, or None."""
- if self.user is None:
- return None
- return get_buglisting_search_filter_url(bug_reporter=self.user.name)
-
-
-class BugsStatsMixin(BugsInfoMixin):
- """Contains properties giving bug stats.
-
- These can be expensive to obtain.
- """
-
- @cachedproperty
- def _bug_stats(self):
- # Circular fail.
- from lp.bugs.model.bugsummary import BugSummary
- bug_task_set = getUtility(IBugTaskSet)
- groups = (
- BugSummary.status, BugSummary.importance, BugSummary.has_patch)
- counts = bug_task_set.countBugs(self.user, [self.context], groups)
- # Sum the split out aggregates.
- new = 0
- open = 0
- inprogress = 0
- critical = 0
- high = 0
- with_patch = 0
- for metadata, count in counts.items():
- status = metadata[0]
- importance = metadata[1]
- has_patch = metadata[2]
- if status == BugTaskStatus.NEW:
- new += count
- elif status == BugTaskStatus.INPROGRESS:
- inprogress += count
- if importance == BugTaskImportance.CRITICAL:
- critical += count
- elif importance == BugTaskImportance.HIGH:
- high += count
- if has_patch and DISPLAY_BUG_STATUS_FOR_PATCHES[status]:
- with_patch += count
- open += count
- result = dict(
- new=new, open=open, inprogress=inprogress, high=high,
- critical=critical, with_patch=with_patch)
- return result
-
- @property
- def open_cve_bugs_count(self):
- """A count of open bugs linked to CVEs."""
- params = get_default_search_params(self.user)
- params.has_cve = True
- return self.context.searchTasks(params).count()
-
- @property
- def pending_bugwatches_count(self):
- """A count of bugs that need a bugwatch.
-
- None is returned if the context is not an upstream product.
- """
- if not IProduct.providedBy(self.context):
- return None
- if self.context.bug_tracking_usage == ServiceUsage.LAUNCHPAD:
- return None
- params = get_default_search_params(self.user)
- params.pending_bugwatch_elsewhere = True
- return self.context.searchTasks(params).count()
-
- @property
- def expirable_bugs_count(self):
- """A count of bugs that can expire, or None.
-
- If the bugtarget is not a supported implementation, or its pillar
- does not have enable_bug_expiration set to True, None is returned.
- The bugtarget may be an `IDistribution`, `IDistroSeries`, `IProduct`,
- or `IProductSeries`.
- """
- if target_has_expirable_bugs_listing(self.context):
- return getUtility(IBugTaskSet).findExpirableBugTasks(
- 0, user=self.user, target=self.context).count()
- else:
- return None
-
- @property
- def new_bugs_count(self):
- """A count of new bugs."""
- return self._bug_stats['new']
-
- @property
- def open_bugs_count(self):
- """A count of open bugs."""
- return self._bug_stats['open']
-
- @property
- def inprogress_bugs_count(self):
- """A count of in-progress bugs."""
- return self._bug_stats['inprogress']
-
- @property
- def critical_bugs_count(self):
- """A count of critical bugs."""
- return self._bug_stats['critical']
-
- @property
- def high_bugs_count(self):
- """A count of high priority bugs."""
- return self._bug_stats['high']
-
- @property
- def my_bugs_count(self):
- """A count of bugs assigned to the user, or None."""
- if self.user is None:
- return None
- else:
- params = get_default_search_params(self.user)
- params.assignee = self.user
- return self.context.searchTasks(params).count()
-
- @property
- def my_reported_bugs_count(self):
- """A count of bugs reported by the user, or None."""
- if self.user is None:
- return None
- params = get_default_search_params(self.user)
- params.bug_reporter = self.user
- return self.context.searchTasks(params).count()
-
- @property
- def my_affecting_bugs_count(self):
- """A count of bugs affecting the user, or None."""
- if self.user is None:
- return None
- params = get_default_search_params(self.user)
- params.affects_me = True
- return self.context.searchTasks(params).count()
-
- @property
- def bugs_with_patches_count(self):
- """A count of unresolved bugs with patches."""
- return self._bug_stats['with_patch']
-
-
-class BugListingPortletInfoView(LaunchpadView, BugsInfoMixin):
- """Portlet containing available bug listings without stats."""
-
-
-class BugListingPortletStatsView(LaunchpadView, BugsStatsMixin):
- """Portlet containing available bug listings with stats."""
-
-
-def get_buglisting_search_filter_url(
- assignee=None, importance=None, status=None, status_upstream=None,
- has_patches=None, bug_reporter=None,
- affecting_me=None,
- orderby=None):
- """Return the given URL with the search parameters specified."""
- search_params = []
-
- if assignee is not None:
- search_params.append(('field.assignee', assignee))
- if importance is not None:
- search_params.append(('field.importance', importance))
- if status is not None:
- search_params.append(('field.status', status))
- if status_upstream is not None:
- search_params.append(('field.status_upstream', status_upstream))
- if has_patches is not None:
- search_params.append(('field.has_patch', 'on'))
- if bug_reporter is not None:
- search_params.append(('field.bug_reporter', bug_reporter))
- if affecting_me is not None:
- search_params.append(('field.affects_me', 'on'))
- if orderby is not None:
- search_params.append(('orderby', orderby))
-
- query_string = urllib.urlencode(search_params, doseq=True)
-
- search_filter_url = "+bugs?search=Search"
- if query_string != '':
- search_filter_url += "&" + query_string
-
- return search_filter_url
-
-
-class BugTaskListingItem:
- """A decorated bug task.
-
- Some attributes that we want to display are too convoluted or expensive
- to get on the fly for each bug task in the listing. These items are
- prefetched by the view and decorate the bug task.
- """
- delegates(IBugTask, 'bugtask')
-
- def __init__(self, bugtask, has_bug_branch,
- has_specification, has_patch, tags,
- people, request=None, target_context=None):
- self.bugtask = bugtask
- self.review_action_widget = None
- self.has_bug_branch = has_bug_branch
- self.has_specification = has_specification
- self.has_patch = has_patch
- self.tags = tags
- self.people = people
- self.request = request
- self.target_context = target_context
-
- @property
- def last_significant_change_date(self):
- """The date of the last significant change."""
- return (self.bugtask.date_closed or self.bugtask.date_fix_committed or
- self.bugtask.date_inprogress or self.bugtask.date_left_new or
- self.bugtask.datecreated)
-
- @property
- def bug_heat_html(self):
- """Returns the bug heat flames HTML."""
- return (
- '<span class="sprite flame">%d</span>'
- % self.bugtask.bug.heat)
-
- @property
- def model(self):
- """Provide flattened data about bugtask for simple templaters."""
- age = DateTimeFormatterAPI(self.bug.datecreated).durationsince()
- age += ' old'
- date_last_updated = self.bug.date_last_message
- if (date_last_updated is None or
- self.bug.date_last_updated > date_last_updated):
- date_last_updated = self.bug.date_last_updated
- last_updated_formatter = DateTimeFormatterAPI(date_last_updated)
- last_updated = last_updated_formatter.displaydate()
- badges = getAdapter(self, IPathAdapter, 'image').badges()
- target_image = getAdapter(self.target, IPathAdapter, 'image')
- if self.bugtask.milestone is not None:
- milestone_name = self.bugtask.milestone.displayname
- else:
- milestone_name = None
- assignee = None
- if self.assigneeID is not None:
- assignee = self.people[self.assigneeID].displayname
- reporter = self.people[self.bug.ownerID]
-
- # the case that there is no target context (e.g. viewing bug that
- # are related to a user account) is intercepted
- if self.target_context is None:
- base_tag_url = "%s/?field.tag=" % canonical_url(
- self.bugtask.target,
- view_name="+bugs")
- else:
- base_tag_url = "%s/?field.tag=" % canonical_url(
- self.target_context,
- view_name="+bugs")
-
- flattened = {
- 'age': age,
- 'assignee': assignee,
- 'bug_url': canonical_url(self.bugtask),
- 'bugtarget': self.bugtargetdisplayname,
- 'bugtarget_css': target_image.sprite_css(),
- 'bug_heat_html': self.bug_heat_html,
- 'badges': badges,
- 'id': self.bug.id,
- 'importance': self.importance.title,
- 'importance_class': 'importance' + self.importance.name,
- 'information_type': self.bug.information_type.title,
- 'last_updated': last_updated,
- 'milestone_name': milestone_name,
- 'reporter': reporter.displayname,
- 'status': self.status.title,
- 'status_class': 'status' + self.status.name,
- 'tags': [{'url': base_tag_url + urllib.quote(tag), 'tag': tag}
- for tag in self.tags],
- 'title': self.bug.title,
- }
-
- # This is a total hack, but pystache will run both truth/false values
- # for an empty list for some reason, and it "works" if it's just a
- # flag like this. We need this value for the mustache template to be
- # able to tell that there are no tags without looking at the list.
- flattened['has_tags'] = True if len(flattened['tags']) else False
- return flattened
-
-
-class BugListingBatchNavigator(TableBatchNavigator):
- """A specialised batch navigator to load smartly extra bug information."""
-
- def __init__(self, tasks, request, columns_to_show, size,
- target_context=None):
- self.request = request
- self.target_context = target_context
- self.user = getUtility(ILaunchBag).user
- self.field_visibility_defaults = {
- 'show_datecreated': False,
- 'show_assignee': False,
- 'show_targetname': True,
- 'show_heat': True,
- 'show_id': True,
- 'show_importance': True,
- 'show_information_type': False,
- 'show_date_last_updated': False,
- 'show_milestone_name': False,
- 'show_reporter': False,
- 'show_status': True,
- 'show_tag': False,
- }
- self.field_visibility = None
- self._setFieldVisibility()
- TableBatchNavigator.__init__(
- self, tasks, request, columns_to_show=columns_to_show, size=size)
-
- @cachedproperty
- def bug_badge_properties(self):
- return getUtility(IBugTaskSet).getBugTaskBadgeProperties(
- self.currentBatch())
-
- @cachedproperty
- def tags_for_batch(self):
- """Return a dict matching bugtask to it's tags."""
- return getUtility(IBugTaskSet).getBugTaskTags(self.currentBatch())
-
- @cachedproperty
- def bugtask_people(self):
- """Return mapping of people related to this bugtask set."""
- return getUtility(IBugTaskSet).getBugTaskPeople(self.currentBatch())
-
- def getCookieName(self):
- """Return the cookie name used in bug listings js code."""
- cookie_name_template = '%s-buglist-fields'
- cookie_name = ''
- if self.user is not None:
- cookie_name = cookie_name_template % self.user.name
- else:
- cookie_name = cookie_name_template % 'anon'
- return cookie_name
-
- def _setFieldVisibility(self):
- """Set field_visibility for the page load.
-
- If a cookie of the form $USER-buglist-fields is found,
- we set field_visibility from this cookie; otherwise,
- field_visibility will match the defaults.
- """
- cookie_name = self.getCookieName()
- cookie = self.request.cookies.get(cookie_name)
- self.field_visibility = dict(self.field_visibility_defaults)
- # "cookie" looks like a URL query string, so we split
- # on '&' to get items, and then split on '=' to get
- # field/value pairs.
- if cookie is None:
- return
- for field, value in urlparse.parse_qsl(cookie):
- # Skip unsupported fields (from old cookies).
- if field not in self.field_visibility:
- continue
- # We only record True or False for field values.
- self.field_visibility[field] = (value == 'true')
-
- def _getListingItem(self, bugtask):
- """Return a decorated bugtask for the bug listing."""
- badge_property = self.bug_badge_properties[bugtask]
- tags = self.tags_for_batch.get(bugtask.id, ())
- if (IMaloneApplication.providedBy(self.target_context) or
- IPerson.providedBy(self.target_context)):
- # XXX Tom Berger bug=529846
- # When we have a specific interface for things that have bug heat
- # it would be better to use that for the check here instead.
- target_context = None
- else:
- target_context = self.target_context
- return BugTaskListingItem(
- bugtask,
- badge_property['has_branch'],
- badge_property['has_specification'],
- badge_property['has_patch'],
- tags,
- self.bugtask_people,
- request=self.request,
- target_context=target_context)
-
- def getBugListingItems(self):
- """Return a decorated list of visible bug tasks."""
- return [self._getListingItem(bugtask) for bugtask in self.batch]
-
- @cachedproperty
- def mustache_template(self):
- template_path = os.path.join(
- config.root, 'lib/lp/bugs/templates/buglisting.mustache')
- with open(template_path) as template_file:
- return template_file.read()
-
- @property
- def mustache_listings(self):
- return 'LP.mustache_listings = %s;' % dumps(
- self.mustache_template, cls=JSONEncoderForHTML)
-
- @property
- def mustache(self):
- """The rendered mustache template."""
- objects = IJSONRequestCache(self.request).objects
- if IUnauthenticatedPrincipal.providedBy(self.request.principal):
- objects = obfuscate_structure(objects)
- model = dict(objects['mustache_model'])
- model.update(self.field_visibility)
- return pystache.render(self.mustache_template, model)
-
- @property
- def model(self):
- items = [bugtask.model for bugtask in self.getBugListingItems()]
- return {'items': items}
-
-
-class IBugTaskSearchListingMenu(Interface):
- """A marker interface for the search listing navigation menu."""
-
-
-class BugTaskSearchListingMenu(NavigationMenu):
- """The search listing navigation menu."""
- usedfor = IBugTaskSearchListingMenu
- facet = 'bugs'
-
- @property
- def links(self):
- bug_target = self.context.context
- if IDistroSeries.providedBy(bug_target):
- return (
- 'nominations',
- )
- if IProductSeries.providedBy(bug_target):
- return (
- 'nominations',
- )
- else:
- return ()
-
- @enabled_with_permission('launchpad.Edit')
- def bugsupervisor(self):
- return Link('+bugsupervisor', 'Change bug supervisor', icon='edit')
-
- def nominations(self):
- return Link('+nominations', 'Review nominations', icon='bug')
-
-
-# All sort orders supported by BugTaskSet.search() and a title for
-# them.
-SORT_KEYS = [
- ('importance', 'Importance', 'desc'),
- ('status', 'Status', 'asc'),
- ('information_type', 'Information Type', 'asc'),
- ('id', 'Number', 'desc'),
- ('title', 'Title', 'asc'),
- ('targetname', 'Package/Project/Series name', 'asc'),
- ('milestone_name', 'Milestone', 'asc'),
- ('date_last_updated', 'Date last updated', 'desc'),
- ('assignee', 'Assignee', 'asc'),
- ('reporter', 'Reporter', 'asc'),
- ('datecreated', 'Age', 'desc'),
- ('tag', 'Tags', 'asc'),
- ('heat', 'Heat', 'desc'),
- ('date_closed', 'Date closed', 'desc'),
- ('dateassigned', 'Date when the bug task was assigned', 'desc'),
- ('number_of_duplicates', 'Number of duplicates', 'desc'),
- ('latest_patch_uploaded', 'Date latest patch uploaded', 'desc'),
- ('message_count', 'Number of comments', 'desc'),
- ('milestone', 'Milestone ID', 'desc'),
- ('specification', 'Linked blueprint', 'asc'),
- ('task', 'Bug task ID', 'desc'),
- ('users_affected_count', 'Number of affected users', 'desc'),
- ]
-
-
-class BugTaskSearchListingView(LaunchpadFormView, FeedsMixin, BugsInfoMixin):
- """View that renders a list of bugs for a given set of search criteria."""
-
- implements(IBugTaskSearchListingMenu)
-
- related_features = {
- 'bugs.dynamic_bug_listings.pre_fetch': False
- }
-
- # Only include <link> tags for bug feeds when using this view.
- feed_types = (
- BugTargetLatestBugsFeedLink,
- )
-
- # These widgets are customised so as to keep the presentation of this view
- # and its descendants consistent after refactoring to use
- # LaunchpadFormView as a parent.
- custom_widget('searchtext', NewLineToSpacesWidget)
- custom_widget('status_upstream', LabeledMultiCheckBoxWidget)
- custom_widget('tag', BugTagsWidget)
- custom_widget('tags_combinator', RadioWidget)
- custom_widget('component', LabeledMultiCheckBoxWidget)
- custom_widget('assignee', PersonPickerWidget)
- custom_widget('bug_reporter', PersonPickerWidget)
- custom_widget('bug_commenter', PersonPickerWidget)
- custom_widget('structural_subscriber', PersonPickerWidget)
- custom_widget('subscriber', PersonPickerWidget)
-
- _batch_navigator = None
-
- @cachedproperty
- def bug_tracking_usage(self):
- """Whether the context tracks bugs in Launchpad.
-
- :returns: ServiceUsage enum value
- """
- service_usage = IServiceUsage(self.context)
- return service_usage.bug_tracking_usage
-
- @cachedproperty
- def external_bugtracker(self):
- """External bug tracking system designated for the context.
-
- :returns: `IBugTracker` or None
- """
- has_external_bugtracker = IHasExternalBugTracker(self.context, None)
- if has_external_bugtracker is None:
- return None
- else:
- return has_external_bugtracker.getExternalBugTracker()
-
- @property
- def has_bugtracker(self):
- """Does the `IBugTarget` have a bug tracker or use Launchpad?"""
- usage = IServiceUsage(self.context)
- uses_lp = usage.bug_tracking_usage == ServiceUsage.LAUNCHPAD
- if self.external_bugtracker or uses_lp:
- return True
- return False
-
- @property
- def can_have_external_bugtracker(self):
- return (IProduct.providedBy(self.context)
- or IProductSeries.providedBy(self.context))
-
- @property
- def bugtracker(self):
- """Description of the context's bugtracker.
-
- :returns: str which may contain HTML.
- """
- if self.bug_tracking_usage == ServiceUsage.LAUNCHPAD:
- return 'Launchpad'
- elif self.external_bugtracker:
- return BugTrackerFormatterAPI(self.external_bugtracker).link(None)
- else:
- return 'None specified'
-
- @cachedproperty
- def upstream_project(self):
- """The linked upstream `IProduct` for the package.
-
- If this `IBugTarget` is a `IDistributionSourcePackage` or an
- `ISourcePackage` and it is linked to an upstream project, return
- the `IProduct`. Otherwise, return None
-
- :returns: `IProduct` or None
- """
- if self._sourcePackageContext():
- sp = self.context
- elif self._distroSourcePackageContext():
- sp = self.context.development_version
- else:
- sp = None
- if sp is not None:
- packaging = sp.packaging
- if packaging is not None:
- return packaging.productseries.product
- return None
-
- @cachedproperty
- def upstream_launchpad_project(self):
- """The linked upstream `IProduct` for the package.
-
- If this `IBugTarget` is a `IDistributionSourcePackage` or an
- `ISourcePackage` and it is linked to an upstream project that uses
- Launchpad to track bugs, return the `IProduct`. Otherwise,
- return None
-
- :returns: `IProduct` or None
- """
- product = self.upstream_project
- if (product is not None and
- product.bug_tracking_usage == ServiceUsage.LAUNCHPAD):
- return product
- return None
-
- @property
- def page_title(self):
- return "Bugs for %s" % self.context.displayname
-
- label = page_title
-
- @property
- def schema(self):
- """Return the schema that defines the form."""
- if self._personContext():
- return IPersonBugTaskSearch
- elif self.isUpstreamProduct:
- return IUpstreamProductBugTaskSearch
- else:
- return IBugTaskSearch
-
- @property
- def feed_links(self):
- """Prevent conflicts between the page and the atom feed.
-
- The latest-bugs atom feed matches the default output of this
- view, but it does not match this view's bug listing when
- any search parameters are passed in.
- """
- if self.request.get('QUERY_STRING', '') == '':
- # There is no query in this request, so it's okay for this page to
- # have its feed links.
- return super(BugTaskSearchListingView, self).feed_links
- else:
- # The query changes the results so that they would not match the
- # feed. In this case, suppress the feed links.
- return []
-
- def initialize(self):
- """Initialize the view with the request.
-
- Look for old status names and redirect to a new location if found.
- """
- query_string = self.request.get('QUERY_STRING')
- if query_string:
- query_string_rewritten = (
- rewrite_old_bugtask_status_query_string(query_string))
- if query_string_rewritten != query_string:
- redirect_uri = URI(self.request.getURL()).replace(
- query=query_string_rewritten)
- self.request.response.redirect(str(redirect_uri), status=301)
- return
-
- self._migrateOldUpstreamStatus()
- LaunchpadFormView.initialize(self)
-
- # We call self._validate() here because LaunchpadFormView only
- # validates the form if an action is submitted but, because this form
- # can be called through a query string, we don't want to require an
- # action. We pass an empty dict to _validate() because all the data
- # needing validation is already available internally to self.
- self._validate(None, {})
-
- expose_structural_subscription_data_to_js(
- self.context, self.request, self.user)
- can_view = (IPrivacy(self.context, None) is None
- or check_permission('launchpad.View', self.context))
- if (can_view and
- not FeedsLayer.providedBy(self.request) and
- not self.request.form.get('advanced')):
- cache = IJSONRequestCache(self.request)
- view_names = set(reg.name for reg
- in iter_view_registrations(self.__class__))
- if len(view_names) != 1:
- raise AssertionError("Ambiguous view name.")
- cache.objects['view_name'] = view_names.pop()
- batch_navigator = self.search()
- cache.objects['mustache_model'] = batch_navigator.model
- cache.objects['field_visibility'] = (
- batch_navigator.field_visibility)
- cache.objects['field_visibility_defaults'] = (
- batch_navigator.field_visibility_defaults)
- cache.objects['cbl_cookie_name'] = (
- batch_navigator.getCookieName())
-
- def _getBatchInfo(batch):
- if batch is None:
- return None
- return {'memo': batch.range_memo,
- 'start': batch.startNumber() - 1}
-
- next_batch = batch_navigator.batch.nextBatch()
- cache.objects['next'] = _getBatchInfo(next_batch)
- prev_batch = batch_navigator.batch.prevBatch()
- cache.objects['prev'] = _getBatchInfo(prev_batch)
- cache.objects['total'] = batch_navigator.batch.total()
- cache.objects['order_by'] = ','.join(
- get_sortorder_from_request(self.request))
- cache.objects['forwards'] = (
- batch_navigator.batch.range_forwards)
- last_batch = batch_navigator.batch.lastBatch()
- cache.objects['last_start'] = last_batch.startNumber() - 1
- cache.objects.update(_getBatchInfo(batch_navigator.batch))
- cache.objects['sort_keys'] = SORT_KEYS
-
- @property
- def show_config_portlet(self):
- if (IDistribution.providedBy(self.context) or
- IProduct.providedBy(self.context)):
- return True
- else:
- return False
-
- @property
- def columns_to_show(self):
- """Returns a sequence of column names to be shown in the listing."""
- upstream_context = self._upstreamContext()
- productseries_context = self._productSeriesContext()
- project_context = self._projectContext()
- distribution_context = self._distributionContext()
- distroseries_context = self._distroSeriesContext()
- distrosourcepackage_context = self._distroSourcePackageContext()
- sourcepackage_context = self._sourcePackageContext()
-
- if (upstream_context or productseries_context or
- distrosourcepackage_context or sourcepackage_context):
- return ["id", "summary", "importance", "status", "heat"]
- elif distribution_context or distroseries_context:
- return [
- "id", "summary", "packagename", "importance", "status",
- "heat"]
- elif project_context:
- return [
- "id", "summary", "productname", "importance", "status",
- "heat"]
- else:
- raise AssertionError(
- "Unrecognized context; don't know which report "
- "columns to show.")
-
- bugtask_table_template = ViewPageTemplateFile(
- '../templates/bugs-table-include.pt')
-
- @property
- def template(self):
- query_string = self.request.get('QUERY_STRING') or ''
- query_params = urlparse.parse_qs(query_string)
- if 'batch_request' in query_params:
- return self.bugtask_table_template
- else:
- return super(BugTaskSearchListingView, self).template
-
- def validate_search_params(self):
- """Validate the params passed for the search.
-
- An UnexpectedFormData exception is raised if the user submitted a URL
- that could not have been created from the UI itself.
- """
- # The only way the user should get these field values incorrect is
- # through a stale bookmark or a hand-hacked URL.
- for field_name in ("status", "importance", "milestone", "component",
- "status_upstream"):
- if self.getFieldError(field_name):
- raise UnexpectedFormData(
- "Unexpected value for field '%s'. Perhaps your bookmarks "
- "are out of date or you changed the URL by hand?" %
- field_name)
-
- orderby = get_sortorder_from_request(self.request)
- for orderby_col in orderby:
- if orderby_col.startswith("-"):
- orderby_col = orderby_col[1:]
-
- try:
- orderby_expression[orderby_col]
- except KeyError:
- raise UnexpectedFormData(
- "Unknown sort column '%s'" % orderby_col)
-
- def setUpWidgets(self):
- """Customize the onKeyPress event of the assignee chooser."""
- LaunchpadFormView.setUpWidgets(self)
-
- self.widgets["assignee"].onKeyPress = (
- "selectWidget('assignee_option', event)")
-
- def validate(self, data):
- """Validates the form."""
- self.validateVocabulariesAdvancedForm()
- self.validate_search_params()
-
- def _migrateOldUpstreamStatus(self):
- """Converts old upstream status value parameters to new ones.
-
- Before Launchpad version 1.1.6 (build 4412), the upstream parameter
- in the request was a single string value, coming from a set of
- radio buttons. From that version on, the user can select multiple
- values in the web UI. In order to keep old bookmarks working,
- convert the old string parameter into a list.
- """
- old_upstream_status_values_to_new_values = {
- 'only_resolved_upstream': 'resolved_upstream'}
-
- status_upstream = self.request.get('field.status_upstream')
- if status_upstream in old_upstream_status_values_to_new_values.keys():
- self.request.form['field.status_upstream'] = [
- old_upstream_status_values_to_new_values[status_upstream]]
- elif status_upstream == '':
- del self.request.form['field.status_upstream']
- else:
- # The value of status_upstream is either correct, so nothing to
- # do, or it has some other error, which is handled in
- # LaunchpadFormView's own validation.
- pass
-
- def buildSearchParams(self, searchtext=None, extra_params=None):
- """Build the BugTaskSearchParams object for the given arguments and
- values specified by the user on this form's widgets.
- """
- # Calling _validate populates the data dictionary as a side-effect
- # of validation.
- data = {}
- self._validate(None, data)
-
- if extra_params:
- data.update(extra_params)
-
- if data:
- searchtext = data.get("searchtext")
- if searchtext and searchtext.isdigit():
- try:
- bug = getUtility(IBugSet).get(searchtext)
- except NotFoundError:
- pass
- else:
- self.request.response.redirect(canonical_url(bug))
-
- assignee_option = self.request.form.get("assignee_option")
- if assignee_option == "none":
- data['assignee'] = NULL
-
- has_patch = data.pop("has_patch", False)
- if has_patch:
- data["attachmenttype"] = BugAttachmentType.PATCH
-
- has_branches = data.get('has_branches', True)
- has_no_branches = data.get('has_no_branches', True)
- if has_branches and not has_no_branches:
- data['linked_branches'] = BugBranchSearch.BUGS_WITH_BRANCHES
- elif not has_branches and has_no_branches:
- data['linked_branches'] = (
- BugBranchSearch.BUGS_WITHOUT_BRANCHES)
- else:
- data['linked_branches'] = BugBranchSearch.ALL
-
- has_blueprints = data.get('has_blueprints', True)
- has_no_blueprints = data.get('has_no_blueprints', True)
- if has_blueprints and not has_no_blueprints:
- data['linked_blueprints'] = (
- BugBlueprintSearch.BUGS_WITH_BLUEPRINTS)
- elif not has_blueprints and has_no_blueprints:
- data['linked_blueprints'] = (
- BugBlueprintSearch.BUGS_WITHOUT_BLUEPRINTS)
- else:
- data['linked_blueprints'] = BugBlueprintSearch.ALL
-
- # Filter appropriately if the user wants to restrict the
- # search to only bugs with no package information.
- has_no_package = data.pop("has_no_package", False)
- if has_no_package:
- data["sourcepackagename"] = NULL
-
- self._buildUpstreamStatusParams(data)
-
- # "Normalize" the form data into search arguments.
- form_values = {}
- for key, value in data.items():
- if key in ('tag'):
- # Skip tag-related parameters, they
- # are handled later on.
- continue
- if zope_isinstance(value, (list, tuple)):
- if len(value) > 0:
- form_values[key] = any(*value)
- else:
- form_values[key] = value
-
- if 'tag' in data:
- # Tags require special handling, since they can be used
- # to search either inclusively or exclusively.
- # We take a look at the `tags_combinator` field, and wrap
- # the tag list in the appropriate search directive (either
- # `any` or `all`). If no value is supplied, we assume `any`,
- # in order to remain compatible with old saved search URLs.
- tags = data['tag']
- tags_combinator_all = (
- 'tags_combinator' in data and
- data['tags_combinator'] == BugTagsSearchCombinator.ALL)
- if zope_isinstance(tags, (list, tuple)) and len(tags) > 0:
- if tags_combinator_all:
- form_values['tag'] = all(*tags)
- else:
- form_values['tag'] = any(*tags)
- else:
- form_values['tag'] = tags
-
- search_params = get_default_search_params(self.user)
- search_params.orderby = get_sortorder_from_request(self.request)
- for name, value in form_values.items():
- setattr(search_params, name, value)
- return search_params
-
- def _buildUpstreamStatusParams(self, data):
- """ Convert the status_upstream value to parameters we can
- send to BugTaskSet.search().
- """
- if 'status_upstream' in data:
- status_upstream = data['status_upstream']
- if 'pending_bugwatch' in status_upstream:
- data['pending_bugwatch_elsewhere'] = True
- if 'resolved_upstream' in status_upstream:
- data['resolved_upstream'] = True
- if 'open_upstream' in status_upstream:
- data['open_upstream'] = True
- if 'hide_upstream' in status_upstream:
- data['has_no_upstream_bugtask'] = True
- del data['status_upstream']
-
- def _getBatchNavigator(self, tasks):
- """Return the batch navigator to be used to batch the bugtasks."""
- return BugListingBatchNavigator(
- tasks, self.request, columns_to_show=self.columns_to_show,
- size=config.malone.buglist_batch_size,
- target_context=self.context)
-
- def buildBugTaskSearchParams(self, searchtext=None, extra_params=None):
- """Build the parameters to submit to the `searchTasks` method.
-
- Use the data submitted in the form to populate a dictionary
- which, when expanded (using **params notation) can serve as the
- input for searchTasks().
- """
-
- # We force the view to populate the data dictionary by calling
- # _validate here.
- data = {}
- self._validate(None, data)
-
- searchtext = data.get("searchtext")
- if searchtext and searchtext.isdigit():
- try:
- bug = getUtility(IBugSet).get(searchtext)
- except NotFoundError:
- pass
- else:
- self.request.response.redirect(canonical_url(bug))
-
- if extra_params:
- data.update(extra_params)
-
- params = {}
-
- # A mapping of parameters that appear in the destination
- # with a different name, or are being dropped altogether.
- param_names_map = {
- 'searchtext': 'search_text',
- 'omit_dupes': 'omit_duplicates',
- 'subscriber': 'bug_subscriber',
- 'tag': 'tags',
- # The correct value is being retrieved
- # using get_sortorder_from_request()
- 'orderby': None,
- }
-
- for key, value in data.items():
- if key in param_names_map:
- param_name = param_names_map[key]
- if param_name is not None:
- params[param_name] = value
- else:
- params[key] = value
-
- assignee_option = self.request.form.get("assignee_option")
- if assignee_option == "none":
- params['assignee'] = NULL
-
- params['order_by'] = get_sortorder_from_request(self.request)
-
- return params
-
- def search(self, searchtext=None, context=None, extra_params=None):
- """Return an `ITableBatchNavigator` for the GET search criteria.
-
- :param searchtext: Text that must occur in the bug report. If
- searchtext is None, the search text will be gotten from the
- request.
-
- :param extra_params: A dict that provides search params added to
- the search criteria taken from the request. Params in
- `extra_params` take precedence over request params.
- """
- if self._batch_navigator is None:
- unbatchedTasks = self.searchUnbatched(
- searchtext, context, extra_params)
- self._batch_navigator = self._getBatchNavigator(unbatchedTasks)
- return self._batch_navigator
-
- def searchUnbatched(self, searchtext=None, context=None,
- extra_params=None):
- """Return a `SelectResults` object for the GET search criteria.
-
- :param searchtext: Text that must occur in the bug report. If
- searchtext is None, the search text will be gotten from the
- request.
-
- :param extra_params: A dict that provides search params added to
- the search criteria taken from the request. Params in
- `extra_params` take precedence over request params.
- """
- # Base classes can provide an explicit search context.
- if not context:
- context = self.context
-
- search_params = self.buildSearchParams(
- searchtext=searchtext, extra_params=extra_params)
- search_params.user = self.user
- try:
- tasks = context.searchTasks(search_params)
- except ValueError as e:
- self.request.response.addErrorNotification(str(e))
- self.request.response.redirect(canonical_url(
- self.context, rootsite='bugs', view_name='+bugs'))
- tasks = None
- return tasks
-
- def getWidgetValues(
- self, vocabulary_name=None, vocabulary=None, default_values=()):
- """Return data used to render a field's widget.
-
- Either `vocabulary_name` or `vocabulary` must be supplied."""
- widget_values = []
-
- if vocabulary is None:
- assert vocabulary_name is not None, 'No vocabulary specified.'
- vocabulary = vocabulary_registry.get(
- self.context, vocabulary_name)
- for term in vocabulary:
- widget_values.append(
- dict(
- value=term.token, title=term.title or term.token,
- checked=term.value in default_values))
- return shortlist(widget_values, longest_expected=12)
-
- def getStatusWidgetValues(self):
- """Return data used to render the status checkboxes."""
- return self.getWidgetValues(
- vocabulary=BugTaskStatusSearchDisplay,
- default_values=DEFAULT_SEARCH_BUGTASK_STATUSES_FOR_DISPLAY)
-
- def getImportanceWidgetValues(self):
- """Return data used to render the Importance checkboxes."""
- return self.getWidgetValues(vocabulary=BugTaskImportance)
-
- def getInformationTypeWidgetValues(self):
- """Return data used to render the Information Type checkboxes."""
- if (IProduct.providedBy(self.context)
- or IDistribution.providedBy(self.context)):
- vocab = InformationTypeVocabulary(
- types=self.context.getAllowedBugInformationTypes())
- else:
- vocab = InformationType
- return self.getWidgetValues(vocabulary=vocab)
-
- def getMilestoneWidgetValues(self):
- """Return data used to render the milestone checkboxes."""
- return self.getWidgetValues("MilestoneWithDateExpected")
-
- def getSimpleSearchURL(self):
- """Return a URL that can be used as an href to the simple search."""
- return canonical_url(self.context) + "/+bugs"
-
- def shouldShowAssigneeWidget(self):
- """Should the assignee widget be shown on the advanced search page?"""
- return True
-
- def shouldShowCommenterWidget(self):
- """Show the commenter widget on the advanced search page?"""
- return True
-
- def shouldShowComponentWidget(self):
- """Show the component widget on the advanced search page?"""
- context = self.context
- return (
- (IDistribution.providedBy(context) and
- context.currentseries is not None) or
- IDistroSeries.providedBy(context) or
- ISourcePackage.providedBy(context))
-
- def shouldShowStructuralSubscriberWidget(self):
- """Should the structural subscriber widget be shown on the page?
-
- Show the widget when there are subordinate structures.
- """
- return self.structural_subscriber_label is not None
-
- def shouldShowNoPackageWidget(self):
- """Should the widget to filter on bugs with no package be shown?
-
- The widget will be shown only on a distribution or
- distroseries's advanced search page.
- """
- return (IDistribution.providedBy(self.context) or
- IDistroSeries.providedBy(self.context))
-
- def shouldShowReporterWidget(self):
- """Should the reporter widget be shown on the advanced search page?"""
- return True
-
- def shouldShowReleaseCriticalPortlet(self):
- """Should the page include a portlet showing release-critical bugs
- for different series.
- """
- return (
- IDistribution.providedBy(self.context) and self.context.series
- or IDistroSeries.providedBy(self.context)
- or IProduct.providedBy(self.context) and self.context.series
- or IProductSeries.providedBy(self.context))
-
- def shouldShowSubscriberWidget(self):
- """Show the subscriber widget on the advanced search page?"""
- return True
-
- def shouldShowUpstreamStatusBox(self):
- """Should the upstream status filtering widgets be shown?"""
- return self.isUpstreamProduct or not (
- IProduct.providedBy(self.context) or
- IProjectGroup.providedBy(self.context))
-
- def shouldShowTeamPortlet(self):
- """Should the User's Teams portlet me shown in the results?"""
- return False
-
- @property
- def structural_subscriber_label(self):
- if IDistribution.providedBy(self.context):
- return 'Package or series subscriber'
- elif IDistroSeries.providedBy(self.context):
- return 'Package subscriber'
- elif IProduct.providedBy(self.context):
- return 'Series subscriber'
- elif IProjectGroup.providedBy(self.context):
- return 'Project or series subscriber'
- elif IPerson.providedBy(self.context):
- return 'Project, distribution, package, or series subscriber'
- else:
- return None
-
- def shouldShowTargetName(self):
- """Should the bug target name be displayed in the list of results?
-
- This is mainly useful for the listview.
- """
- # It doesn't make sense to show the target name when viewing product
- # bugs.
- if IProduct.providedBy(self.context):
- return False
- else:
- return True
-
- def shouldShowAdvancedForm(self):
- """Return True if the advanced form should be shown, or False."""
- if (self.request.form.get('advanced')
- or self.form_has_errors):
- return True
- else:
- return False
-
- @property
- def should_show_bug_information(self):
- return self.bug_tracking_usage == ServiceUsage.LAUNCHPAD
-
- @property
- def form_has_errors(self):
- """Return True if the form has errors, otherwise False."""
- return len(self.errors) > 0
-
- def validateVocabulariesAdvancedForm(self):
- """Provides a meaningful message for vocabulary validation errors."""
- error_message = _(
- "There's no person with the name or email address '%s'.")
-
- for name in ('assignee', 'bug_reporter', 'structural_subscriber',
- 'bug_commenter', 'subscriber'):
- if self.getFieldError(name):
- self.setFieldError(
- name, error_message %
- self.request.get('field.%s' % name))
-
- @property
- def isUpstreamProduct(self):
- """Is the context a Product that does not use Malone?"""
- return (
- IProduct.providedBy(self.context)
- and self.context.bug_tracking_usage != ServiceUsage.LAUNCHPAD)
-
- def _upstreamContext(self):
- """Is this page being viewed in an upstream context?
-
- Return the IProduct if yes, otherwise return None.
- """
- return IProduct(self.context, None)
-
- def _productSeriesContext(self):
- """Is this page being viewed in a product series context?
-
- Return the IProductSeries if yes, otherwise return None.
- """
- return IProductSeries(self.context, None)
-
- def _projectContext(self):
- """Is this page being viewed in a project context?
-
- Return the IProjectGroup if yes, otherwise return None.
- """
- return IProjectGroup(self.context, None)
-
- def _personContext(self):
- """Is this page being viewed in a person context?
-
- Return the IPerson if yes, otherwise return None.
- """
- return IPerson(self.context, None)
-
- def _distributionContext(self):
- """Is this page being viewed in a distribution context?
-
- Return the IDistribution if yes, otherwise return None.
- """
- return IDistribution(self.context, None)
-
- def _distroSeriesContext(self):
- """Is this page being viewed in a distroseries context?
-
- Return the IDistroSeries if yes, otherwise return None.
- """
- return IDistroSeries(self.context, None)
-
- def _sourcePackageContext(self):
- """Is this view in a [distroseries] sourcepackage context?
-
- Return the ISourcePackage if yes, otherwise return None.
- """
- return ISourcePackage(self.context, None)
-
- def _distroSourcePackageContext(self):
- """Is this page being viewed in a distribution sourcepackage context?
-
- Return the IDistributionSourcePackage if yes, otherwise return None.
- """
- return IDistributionSourcePackage(self.context, None)
-
- @property
- def addquestion_url(self):
- """Return the URL for the +addquestion view for the context."""
- if IQuestionTarget.providedBy(self.context):
- answers_usage = IServiceUsage(self.context).answers_usage
- if answers_usage == ServiceUsage.LAUNCHPAD:
- return canonical_url(
- self.context, rootsite='answers',
- view_name='+addquestion')
- else:
- return None
-
-
-class BugNominationsView(BugTaskSearchListingView):
- """View for accepting/declining bug nominations."""
-
- def search(self):
- """Return all the nominated tasks for this series."""
- if IDistroSeries.providedBy(self.context):
- main_context = self.context.distribution
- elif IProductSeries.providedBy(self.context):
- main_context = self.context.product
- else:
- raise AssertionError(
- 'Unknown nomination target: %r' % self.context)
- return BugTaskSearchListingView.search(
- self, context=main_context,
- extra_params=dict(nominated_for=self.context))
-
-
-class BugTargetView(LaunchpadView):
- """Used to grab bugs for a bug target; used by the latest bugs portlet"""
-
- def latestBugTasks(self, quantity=5):
- """Return <quantity> latest bugs reported against this target."""
- params = BugTaskSearchParams(orderby="-datecreated",
- omit_dupes=True,
- user=getUtility(ILaunchBag).user)
-
- tasklist = self.context.searchTasks(params)
- return tasklist[:quantity]
-
- def getMostRecentlyUpdatedBugTasks(self, limit=5):
- """Return the most recently updated bugtasks for this target."""
- params = BugTaskSearchParams(
- orderby="-date_last_updated", omit_dupes=True, user=self.user)
- return list(self.context.searchTasks(params)[:limit])
-
-
-class TextualBugTaskSearchListingView(BugTaskSearchListingView):
- """View that renders a list of bug IDs for a given set of search criteria.
- """
-
- def render(self):
- """Render the BugTarget for text display."""
- self.request.response.setHeader(
- 'Content-type', 'text/plain')
-
- # This uses the BugTaskSet internal API instead of using the
- # standard searchTasks() because the latter can retrieve a lot
- # of bugs and we don't want to load all of that data in memory.
- # Retrieving only the bug numbers is much more efficient.
- search_params = self.buildSearchParams()
-
- # XXX flacoste 2008/04/24 This should be moved to a
- # BugTaskSearchParams.setTarget().
- if (IDistroSeries.providedBy(self.context) or
- IProductSeries.providedBy(self.context)):
- search_params.setTarget(self.context)
- elif IDistribution.providedBy(self.context):
- search_params.setDistribution(self.context)
- elif IProduct.providedBy(self.context):
- search_params.setProduct(self.context)
- elif IProjectGroup.providedBy(self.context):
- search_params.setProject(self.context)
- elif (ISourcePackage.providedBy(self.context) or
- IDistributionSourcePackage.providedBy(self.context)):
- search_params.setSourcePackage(self.context)
- else:
- raise AssertionError('Unknown context type: %s' % self.context)
-
- return u"".join("%d\n" % bug_id for bug_id in
- getUtility(IBugTaskSet).searchBugIds(search_params))
-
-
def bugtask_sort_key(bugtask):
"""Return a sort key for displaying a set of tasks for a single bug.
@@ -3963,52 +2272,6 @@
)
-class BugsBugTaskSearchListingView(BugTaskSearchListingView):
- """Search all bug reports."""
-
- columns_to_show = ["id", "summary", "bugtargetdisplayname",
- "importance", "status", "heat"]
- schema = IFrontPageBugTaskSearch
- custom_widget('scope', ProjectScopeWidget)
- page_title = 'Search'
-
- def initialize(self):
- """Initialize the view for the request."""
- BugTaskSearchListingView.initialize(self)
- if not self._isRedirected():
- self._redirectToSearchContext()
-
- def _redirectToSearchContext(self):
- """Check whether a target was given and redirect to it.
-
- All the URL parameters will be passed on to the target's +bugs
- page.
-
- If the target widget contains errors, redirect to the front page
- which will handle the error.
- """
- try:
- search_target = self.widgets['scope'].getInputValue()
- except InputErrors:
- query_string = self.request['QUERY_STRING']
- bugs_url = "%s?%s" % (canonical_url(self.context), query_string)
- self.request.response.redirect(bugs_url)
- else:
- if search_target is not None:
- query_string = self.request['QUERY_STRING']
- search_url = "%s/+bugs?%s" % (
- canonical_url(search_target), query_string)
- self.request.response.redirect(search_url)
-
- def getSearchPageHeading(self):
- """Return the heading to search all Bugs."""
- return "Search all bug reports"
-
- @property
- def label(self):
- return self.getSearchPageHeading()
-
-
class BugTaskPrivacyAdapter:
"""Provides `IObjectPrivacy` for `IBugTask`."""
@@ -4130,43 +2393,6 @@
page_title = label
-class BugTaskExpirableListingView(BugTaskSearchListingView):
- """View for listing Incomplete bugs that can expire."""
-
- @property
- def can_show_expirable_bugs(self):
- """Return True or False if expirable bug listing can be shown."""
- return target_has_expirable_bugs_listing(self.context)
-
- @property
- def inactive_expiration_age(self):
- """Return the number of days an bug must be inactive to expire."""
- return config.malone.days_before_expiration
-
- @property
- def columns_to_show(self):
- """Show the columns that summarise expirable bugs."""
- if (IDistribution.providedBy(self.context)
- or IDistroSeries.providedBy(self.context)):
- return [
- 'id', 'summary', 'packagename', 'date_last_updated', 'heat']
- else:
- return ['id', 'summary', 'date_last_updated', 'heat']
-
- def search(self):
- """Return an `ITableBatchNavigator` for the expirable bugtasks."""
- bugtaskset = getUtility(IBugTaskSet)
- bugtasks = bugtaskset.findExpirableBugTasks(
- user=self.user, target=self.context, min_days_old=0)
- return BugListingBatchNavigator(
- bugtasks, self.request, columns_to_show=self.columns_to_show,
- size=config.malone.buglist_batch_size)
-
- @property
- def page_title(self):
- return "Bugs that can expire in %s" % self.context.title
-
-
class BugActivityItem:
"""A decorated BugActivity."""
delegates(IBugActivity, 'activity')
=== modified file 'lib/lp/bugs/browser/configure.zcml'
--- lib/lp/bugs/browser/configure.zcml 2014-11-29 00:03:36 +0000
+++ lib/lp/bugs/browser/configure.zcml 2014-11-29 05:04:02 +0000
@@ -16,25 +16,25 @@
facet="bugs">
<browser:page
for="lp.bugs.interfaces.bugtarget.IHasBugs"
- class="lp.bugs.browser.bugtask.BugTargetView"
+ class="lp.bugs.browser.buglisting.BugTargetView"
name="+portlet-latestbugs"
permission="zope.Public"
template="../templates/bugtarget-portlet-latestbugs.pt"/>
<browser:page
for="lp.bugs.interfaces.bugtarget.IHasBugs"
- class="lp.bugs.browser.bugtask.BugListingPortletInfoView"
+ class="lp.bugs.browser.buglisting.BugListingPortletInfoView"
permission="zope.Public"
name="+portlet-bugfilters"
template="../templates/bugtarget-portlet-bugfilters.pt"/>
<browser:page
for="lp.bugs.interfaces.bugtarget.IHasBugs"
- class="lp.bugs.browser.bugtask.BugListingPortletInfoView"
+ class="lp.bugs.browser.buglisting.BugListingPortletInfoView"
permission="zope.Public"
name="+bugtarget-portlet-bugfilters-info"
template="../templates/bugtarget-portlet-bugfilters-content.pt"/>
<browser:page
for="lp.bugs.interfaces.bugtarget.IHasBugs"
- class="lp.bugs.browser.bugtask.BugListingPortletStatsView"
+ class="lp.bugs.browser.buglisting.BugListingPortletStatsView"
permission="zope.Public"
name="+bugtarget-portlet-bugfilters-stats"
template="../templates/bugtarget-portlet-bugfilters-content.pt"/>
@@ -52,25 +52,25 @@
permission="zope.Public"/>
<browser:page
for="lp.bugs.interfaces.bugtarget.IBugTarget"
- class="lp.bugs.browser.bugtask.TextualBugTaskSearchListingView"
+ class="lp.bugs.browser.buglisting.TextualBugTaskSearchListingView"
permission="zope.Public"
name="+bugs-text"
attribute="__call__"/>
<browser:page
for="lp.registry.interfaces.projectgroup.IProjectGroup"
- class="lp.bugs.browser.bugtask.TextualBugTaskSearchListingView"
+ class="lp.bugs.browser.buglisting.TextualBugTaskSearchListingView"
permission="zope.Public"
name="+bugs-text"
attribute="__call__"/>
<browser:page
for="lp.bugs.interfaces.bugtarget.IHasBugs"
- class="lp.bugs.browser.bugtask.BugTaskSearchListingView"
+ class="lp.bugs.browser.buglisting.BugTaskSearchListingView"
permission="zope.Public"
name="+bugs"
template="../templates/buglisting-default.pt"/>
<browser:page
for="lp.bugs.interfaces.bugtarget.IHasExpirableBugs"
- class="lp.bugs.browser.bugtask.BugTaskExpirableListingView"
+ class="lp.bugs.browser.buglisting.BugTaskExpirableListingView"
permission="zope.Public"
name="+expirable-bugs"
template="../templates/bug-listing-expirable.pt"/>
@@ -250,7 +250,7 @@
module="lp.bugs.browser.malone"/>
<browser:page
for="lp.bugs.interfaces.malone.IMaloneApplication"
- class="lp.bugs.browser.bugtask.BugsBugTaskSearchListingView"
+ class="lp.bugs.browser.buglisting.BugsBugTaskSearchListingView"
permission="zope.Public"
name="+bugs"
template="../templates/buglisting-embedded-advanced-search.pt"/>
@@ -358,7 +358,7 @@
<browser:page
for="lp.registry.interfaces.distroseries.IDistroSeries"
name="+nominations"
- class="lp.bugs.browser.bugtask.BugNominationsView"
+ class="lp.bugs.browser.buglisting.BugNominationsView"
permission="zope.Public"
template="../templates/series-bug-nominations.pt"/>
<adapter
@@ -426,14 +426,15 @@
<browser:page
for="lp.registry.interfaces.productseries.IProductSeries"
name="+nominations"
- class="lp.bugs.browser.bugtask.BugNominationsView"
+ class="lp.bugs.browser.buglisting.BugNominationsView"
permission="zope.Public"
template="../templates/series-bug-nominations.pt"/>
<browser:menus
+ module="lp.bugs.browser.buglisting"
+ classes="BugTaskSearchListingMenu"/>
+ <browser:menus
module="lp.bugs.browser.bugtask"
- classes="
- BugTaskContextMenu
- BugTaskSearchListingMenu"/>
+ classes="BugTaskContextMenu"/>
<browser:page
for="*"
name="+bugtask-macros-tableview"
@@ -598,7 +599,7 @@
<browser:page
name="+listing-view"
for="lp.bugs.interfaces.bugtask.IBugTask"
- class="lp.bugs.browser.bugtask.BugTaskListingView"
+ class="lp.bugs.browser.buglisting.BugTaskListingView"
permission="launchpad.View">
</browser:page>
<browser:page
=== modified file 'lib/lp/bugs/browser/cvereport.py'
--- lib/lp/bugs/browser/cvereport.py 2012-11-29 06:35:35 +0000
+++ lib/lp/bugs/browser/cvereport.py 2014-11-29 05:04:02 +0000
@@ -12,7 +12,7 @@
from zope.component import getUtility
-from lp.bugs.browser.bugtask import BugTaskListingItem
+from lp.bugs.browser.buglisting import BugTaskListingItem
from lp.bugs.interfaces.bugtask import (
IBugTaskSet,
RESOLVED_BUGTASK_STATUSES,
=== modified file 'lib/lp/bugs/browser/person.py'
--- lib/lp/bugs/browser/person.py 2014-11-29 05:04:02 +0000
+++ lib/lp/bugs/browser/person.py 2014-11-29 05:04:02 +0000
@@ -22,7 +22,11 @@
from zope.component import getUtility
+<<<<<<< TREE
from lp.bugs.browser.bugtask import BugTaskSearchListingView
+=======
+from lp.bugs.browser.buglisting import BugTaskSearchListingView
+>>>>>>> MERGE-SOURCE
from lp.bugs.interfaces.bugtask import (
BugTaskStatus,
IBugTaskSet,
=== modified file 'lib/lp/bugs/browser/tests/bugtask-search-views.txt'
--- lib/lp/bugs/browser/tests/bugtask-search-views.txt 2014-01-03 07:39:34 +0000
+++ lib/lp/bugs/browser/tests/bugtask-search-views.txt 2014-11-29 05:04:02 +0000
@@ -399,7 +399,7 @@
get_buglisting_search_filter_url() with a bugtarget URL to make it
useful.
- >>> from lp.bugs.browser.bugtask import (
+ >>> from lp.bugs.browser.buglisting import (
... get_buglisting_search_filter_url)
Calling get_buglisting_search_filter_url() without any parameters will
=== modified file 'lib/lp/bugs/browser/tests/test_bugtarget_patches_view.py'
--- lib/lp/bugs/browser/tests/test_bugtarget_patches_view.py 2012-08-08 11:48:29 +0000
+++ lib/lp/bugs/browser/tests/test_bugtarget_patches_view.py 2014-11-29 05:04:02 +0000
@@ -4,11 +4,11 @@
__metaclass__ = type
-from lp.bugs.browser.bugtarget import BugsPatchesView
-from lp.bugs.browser.bugtask import (
+from lp.bugs.browser.buglisting import (
BugListingPortletStatsView,
DISPLAY_BUG_STATUS_FOR_PATCHES,
)
+from lp.bugs.browser.bugtarget import BugsPatchesView
from lp.services.webapp.servers import LaunchpadTestRequest
from lp.testing import (
login,
=== modified file 'lib/lp/bugs/browser/tests/test_bugtask.py'
--- lib/lp/bugs/browser/tests/test_bugtask.py 2014-11-14 22:31:05 +0000
+++ lib/lp/bugs/browser/tests/test_bugtask.py 2014-11-29 05:04:02 +0000
@@ -34,11 +34,13 @@
from lp.app.enums import InformationType
from lp.app.interfaces.launchpad import ILaunchpadCelebrities
from lp.bugs.adapters.bugchange import BugTaskStatusChange
+from lp.bugs.browser.buglisting import (
+ BugListingBatchNavigator,
+ BugTaskListingItem,
+ )
from lp.bugs.browser.bugtask import (
BugActivityItem,
- BugListingBatchNavigator,
BugTaskEditView,
- BugTaskListingItem,
BugTasksNominationsView,
BugTasksTableView,
)
=== modified file 'lib/lp/bugs/doc/bugtask-search-old-urls.txt'
--- lib/lp/bugs/doc/bugtask-search-old-urls.txt 2011-12-24 17:49:30 +0000
+++ lib/lp/bugs/doc/bugtask-search-old-urls.txt 2014-11-29 05:04:02 +0000
@@ -6,7 +6,7 @@
agent to the new location with a permanent redirect so it has an
opportunity to update bookmarks and so forth.
- >>> from lp.bugs.browser.bugtask import (
+ >>> from lp.bugs.browser.buglisting import (
... BugTaskSearchListingView, rewrite_old_bugtask_status_query_string)
>>> from lp.services.webapp.servers import LaunchpadTestRequest
=== modified file 'lib/lp/bugs/feed/bug.py'
--- lib/lp/bugs/feed/bug.py 2012-03-28 17:55:30 +0000
+++ lib/lp/bugs/feed/bug.py 2014-11-29 05:04:02 +0000
@@ -15,7 +15,7 @@
from z3c.ptcompat import ViewPageTemplateFile
from zope.component import getUtility
-from lp.bugs.browser.bugtask import (
+from lp.bugs.browser.buglisting import (
BugsBugTaskSearchListingView,
BugTargetView,
)
=== modified file 'lib/lp/registry/browser/milestone.py'
--- lib/lp/registry/browser/milestone.py 2013-04-03 05:04:11 +0000
+++ lib/lp/registry/browser/milestone.py 2014-11-29 05:04:02 +0000
@@ -46,7 +46,7 @@
safe_action,
)
from lp.app.widgets.date import DateWidget
-from lp.bugs.browser.bugtask import BugTaskListingItem
+from lp.bugs.browser.buglisting import BugTaskListingItem
from lp.bugs.browser.structuralsubscription import (
expose_structural_subscription_data_to_js,
StructuralSubscriptionMenuMixin,
=== modified file 'lib/lp/registry/browser/product.py'
--- lib/lp/registry/browser/product.py 2014-11-27 06:24:11 +0000
+++ lib/lp/registry/browser/product.py 2014-11-29 05:04:02 +0000
@@ -123,10 +123,8 @@
from lp.blueprints.browser.specificationtarget import (
HasSpecificationsMenuMixin,
)
-from lp.bugs.browser.bugtask import (
- BugTargetTraversalMixin,
- get_buglisting_search_filter_url,
- )
+from lp.bugs.browser.buglisting import get_buglisting_search_filter_url
+from lp.bugs.browser.bugtask import BugTargetTraversalMixin
from lp.bugs.browser.structuralsubscription import (
expose_structural_subscription_data_to_js,
StructuralSubscriptionMenuMixin,
References