1755
1775
self.updateContextFromData(data)
1778
class BugTaskDeletionView(ReturnToReferrerMixin, LaunchpadFormView):
1779
"""Used to delete a bugtask."""
1784
label = 'Remove bug task'
1789
"""Return the next URL to call when this call completes."""
1790
if not self.request.is_ajax:
1791
return super(BugTaskDeletionView, self).next_url
1794
@action('Delete', name='delete_bugtask')
1795
def delete_bugtask_action(self, action, data):
1796
bugtask = self.context
1798
deleted_bugtask_url = canonical_url(self.context, rootsite='bugs')
1799
message = ("This bug no longer affects %s."
1800
% bugtask.bugtargetdisplayname)
1802
self.request.response.addNotification(message)
1803
if self.request.is_ajax:
1804
launchbag = getUtility(ILaunchBag)
1805
launchbag.add(bug.default_bugtask)
1806
# If we are deleting the current highlighted bugtask via ajax,
1807
# we must force a redirect to the new default bugtask to ensure
1808
# all URLs and other client cache content is correctly refreshed.
1809
# We can't do the redirect here since the XHR caller won't see it
1810
# so we return the URL to go to and let the caller do it.
1811
if self._return_url == deleted_bugtask_url:
1812
next_url = canonical_url(
1813
bug.default_bugtask, rootsite='bugs')
1814
self.request.response.setHeader('Content-type',
1816
return dumps(dict(bugtask_url=next_url))
1817
# No redirect required so return the new bugtask table HTML.
1818
view = getMultiAdapter(
1819
(bug, self.request),
1820
name='+bugtasks-and-nominations-table')
1822
return view.render()
1758
1825
class BugTaskListingView(LaunchpadView):
1759
1826
"""A view designed for displaying bug tasks in lists."""
1760
1827
# Note that this right now is only used in tests and to render
2135
2202
"""Returns the bug heat flames HTML."""
2136
2203
return bugtask_heat_html(self.bugtask, target=self.target_context)
2207
"""Provide flattened data about bugtask for simple templaters."""
2208
age = DateTimeFormatterAPI(self.bug.datecreated).durationsince()
2210
date_last_updated = self.bug.date_last_message
2211
if (date_last_updated is None or
2212
self.bug.date_last_updated > date_last_updated):
2213
date_last_updated = self.bug.date_last_updated
2214
last_updated_formatter = DateTimeFormatterAPI(date_last_updated)
2215
last_updated = last_updated_formatter.displaydate()
2216
badges = getAdapter(self.bugtask, IPathAdapter, 'image').badges()
2217
target_image = getAdapter(self.target, IPathAdapter, 'image')
2218
if self.bugtask.milestone is not None:
2219
milestone_name = self.bugtask.milestone.displayname
2221
milestone_name = None
2223
if self.assignee is not None:
2224
assignee = self.assignee.displayname
2227
'assignee': assignee,
2228
'bug_url': canonical_url(self.bugtask),
2229
'bugtarget': self.bugtargetdisplayname,
2230
'bugtarget_css': target_image.sprite_css(),
2231
'bug_heat_html': self.bug_heat_html,
2234
'importance': self.importance.title,
2235
'importance_class': 'importance' + self.importance.name,
2236
'last_updated': last_updated,
2237
'milestone_name': milestone_name,
2238
'reporter': self.bug.owner.displayname,
2239
'status': self.status.title,
2240
'status_class': 'status' + self.status.name,
2241
'tags': ' '.join(self.bug.tags),
2242
'title': self.bug.title,
2139
2246
class BugListingBatchNavigator(TableBatchNavigator):
2140
2247
"""A specialised batch navigator to load smartly extra bug information."""
2176
2297
"""Return a decorated list of visible bug tasks."""
2177
2298
return [self._getListingItem(bugtask) for bugtask in self.batch]
2301
def mustache_template(self):
2302
template_path = os.path.join(
2303
config.root, 'lib/lp/bugs/templates/buglisting.mustache')
2304
with open(template_path) as template_file:
2305
return template_file.read()
2308
def mustache_listings(self):
2309
return 'LP.mustache_listings = %s;' % dumps(
2310
self.mustache_template, cls=JSONEncoderForHTML)
2314
"""The rendered mustache template."""
2315
objects = IJSONRequestCache(self.request).objects
2316
if IUnauthenticatedPrincipal.providedBy(self.request.principal):
2317
objects = obfuscate_structure(objects)
2318
return pystache.render(self.mustache_template,
2319
objects['mustache_model'])
2323
bugtasks = [bugtask.model for bugtask in self.getBugListingItems()]
2324
for bugtask in bugtasks:
2325
bugtask.update(self.field_visibility)
2326
return {'bugtasks': bugtasks}
2180
2329
class NominatedBugReviewAction(EnumeratedType):
2181
2330
"""Enumeration for nomination review actions"""
2435
2586
expose_structural_subscription_data_to_js(
2436
2587
self.context, self.request, self.user)
2588
if getFeatureFlag('bugs.dynamic_bug_listings.enabled'):
2589
cache = IJSONRequestCache(self.request)
2590
batch_navigator = self.search()
2591
cache.objects['mustache_model'] = batch_navigator.model
2592
cache.objects['field_visibility'] = (
2593
batch_navigator.field_visibility)
2595
def _getBatchInfo(batch):
2598
return {'memo': batch.range_memo,
2599
'start': batch.startNumber() - 1}
2601
next_batch = batch_navigator.batch.nextBatch()
2602
cache.objects['next'] = _getBatchInfo(next_batch)
2603
prev_batch = batch_navigator.batch.prevBatch()
2604
cache.objects['prev'] = _getBatchInfo(prev_batch)
2605
cache.objects['total'] = batch_navigator.batch.total()
2606
cache.objects['order_by'] = ','.join(
2607
get_sortorder_from_request(self.request))
2608
cache.objects['forwards'] = batch_navigator.batch.range_forwards
2609
last_batch = batch_navigator.batch.lastBatch()
2610
cache.objects['last_start'] = last_batch.startNumber() - 1
2611
cache.objects.update(_getBatchInfo(batch_navigator.batch))
2439
2614
def columns_to_show(self):
3425
3632
def other_users_affected_count(self):
3426
"""The number of other users affected by this bug."""
3427
if self.current_user_affected_status:
3428
return self.context.users_affected_count - 1
3633
"""The number of other users affected by this bug.
3635
if getFeatureFlag('bugs.affected_count_includes_dupes.disabled'):
3636
if self.current_user_affected_status:
3637
return self.context.users_affected_count - 1
3639
return self.context.users_affected_count
3641
return self.context.other_users_affected_count_with_dupes
3644
def total_users_affected_count(self):
3645
"""The number of affected users, typically across all users.
3647
Counting across duplicates may be disabled at run time.
3649
if getFeatureFlag('bugs.affected_count_includes_dupes.disabled'):
3430
3650
return self.context.users_affected_count
3652
return self.context.users_affected_count_with_dupes
3433
3655
def affected_statement(self):
3434
3656
"""The default "this bug affects" statement to show.
3436
3658
The outputs of this method should be mirrored in
3437
3659
MeTooChoiceSource._getSourceNames() (Javascript).
3439
if self.other_users_affected_count == 1:
3440
if self.current_user_affected_status is None:
3661
me_affected = self.current_user_affected_status
3662
other_affected = self.other_users_affected_count
3663
if me_affected is None:
3664
if other_affected == 1:
3441
3665
return "This bug affects 1 person. Does this bug affect you?"
3442
elif self.current_user_affected_status:
3443
return "This bug affects you and 1 other person"
3445
return "This bug affects 1 person, but not you"
3446
elif self.other_users_affected_count > 1:
3447
if self.current_user_affected_status is None:
3666
elif other_affected > 1:
3449
3668
"This bug affects %d people. Does this bug "
3450
"affect you?" % (self.other_users_affected_count))
3451
elif self.current_user_affected_status:
3452
return "This bug affects you and %d other people" % (
3453
self.other_users_affected_count)
3669
"affect you?" % (other_affected))
3455
return "This bug affects %d people, but not you" % (
3456
self.other_users_affected_count)
3458
if self.current_user_affected_status is None:
3459
3671
return "Does this bug affect you?"
3460
elif self.current_user_affected_status:
3672
elif me_affected is True:
3673
if other_affected == 0:
3461
3674
return "This bug affects you"
3675
elif other_affected == 1:
3676
return "This bug affects you and 1 other person"
3678
return "This bug affects you and %d other people" % (
3681
if other_affected == 0:
3463
3682
return "This bug doesn't affect you"
3683
elif other_affected == 1:
3684
return "This bug affects 1 person, but not you"
3685
elif other_affected > 1:
3686
return "This bug affects %d people, but not you" % (
3466
3690
def anon_affected_statement(self):
3467
3691
"""The "this bug affects" statement to show to anonymous users.
3469
3693
The outputs of this method should be mirrored in
3470
3694
MeTooChoiceSource._getSourceNames() (Javascript).
3472
if self.context.users_affected_count == 1:
3696
affected = self.total_users_affected_count
3473
3698
return "This bug affects 1 person"
3474
elif self.context.users_affected_count > 1:
3475
return "This bug affects %d people" % (
3476
self.context.users_affected_count)
3700
return "This bug affects %d people" % affected
3705
def _allow_multipillar_private_bugs(self):
3706
""" Some teams still need to have multi pillar private bugs."""
3707
return bool(getFeatureFlag(
3708
'disclosure.allow_multipillar_private_bugs.enabled'))
3710
def canAddProjectTask(self):
3711
"""Can a new bug task on a project be added to this bug?
3713
If a bug has any bug tasks already, were it to be private, it cannot
3714
be marked as also affecting any other project, so return False.
3716
Note: this check is currently only relevant if a bug is private.
3717
Eventually, even public bugs will have this restriction too. So what
3718
happens now is that this API is used by the tales to add a class
3719
called 'disallow-private' to the Also Affects Project link. A css rule
3720
is used to hide the link when body.private is True.
3724
if self._allow_multipillar_private_bugs:
3726
return len(bug.bugtasks) == 0
3728
def canAddPackageTask(self):
3729
"""Can a new bug task on a src pkg be added to this bug?
3731
If a bug has any existing bug tasks on a project, were it to
3732
be private, then it cannot be marked as affecting a package,
3735
A task on a given package may still be illegal to add, but
3736
this will be caught when bug.addTask() is attempted.
3738
Note: this check is currently only relevant if a bug is private.
3739
Eventually, even public bugs will have this restriction too. So what
3740
happens now is that this API is used by the tales to add a class
3741
called 'disallow-private' to the Also Affects Package link. A css rule
3742
is used to hide the link when body.private is True.
3745
if self._allow_multipillar_private_bugs:
3747
for pillar in bug.affected_pillars:
3748
if IProduct.providedBy(pillar):
3481
3753
class BugTaskTableRowView(LaunchpadView, BugTaskBugWatchMixin):
3482
3754
"""Browser class for rendering a bugtask row on the bug page."""
3518
3796
row_css_class='highlight' if is_primary else None,
3519
3797
target_link=canonical_url(self.context.target),
3520
3798
target_link_title=self.target_link_title,
3521
user_can_edit_importance=self.context.userCanEditImportance(
3799
user_can_delete=self.user_can_delete_bugtask,
3800
delete_link=delete_link,
3801
user_can_edit_importance=self.user_can_edit_importance,
3523
3802
importance_css_class='importance' + self.context.importance.name,
3524
3803
importance_title=self.context.importance.title,
3525
3804
# We always look up all milestones, so there's no harm
3526
3805
# using len on the list here and avoid the COUNT query.
3527
3806
target_has_milestones=len(self._visible_milestones) > 0,
3807
user_can_edit_status=self.user_can_edit_status,
3810
if not self.many_bugtasks:
3811
cache = IJSONRequestCache(self.request)
3812
bugtask_data = cache.objects.get('bugtask_data', None)
3813
if bugtask_data is None:
3814
bugtask_data = dict()
3815
cache.objects['bugtask_data'] = bugtask_data
3816
bugtask_data[bugtask_id] = self.bugtask_config()
3530
3818
def canSeeTaskDetails(self):
3531
3819
"""Whether someone can see a task's status details.
3704
4018
not self.context.userCanSetAnyAssignee(user) and
3705
4019
(user is None or user.teams_participated_in.count() == 0))
3706
4020
cx = self.context
3708
4022
row_id=self.data['row_id'],
4023
form_row_id=self.data['form_row_id'],
3709
4024
bugtask_path='/'.join([''] + self.data['link'].split('/')[3:]),
3710
4025
prefix=get_prefix(cx),
4026
targetname=cx.bugtargetdisplayname,
4027
bug_title=cx.bug.title,
3711
4028
assignee_value=cx.assignee and cx.assignee.name,
3712
4029
assignee_is_team=cx.assignee and cx.assignee.is_team,
3713
4030
assignee_vocabulary=assignee_vocabulary,
3714
4031
assignee_vocabulary_filters=filter_details,
3715
4032
hide_assignee_team_selection=hide_assignee_team_selection,
3716
4033
user_can_unassign=cx.userCanUnassign(user),
4034
user_can_delete=self.user_can_delete_bugtask,
4035
delete_link=self.data['delete_link'],
3717
4036
target_is_product=IProduct.providedBy(cx.target),
3718
4037
status_widget_items=self.status_widget_items,
3719
4038
status_value=cx.status.title,