~launchpad-pqm/launchpad/devel

« back to all changes in this revision

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

Merged db-devel into replication.

Show diffs side-by-side

added added

removed removed

Lines of Context:
18
18
    'BugTaskBreadcrumb',
19
19
    'BugTaskContextMenu',
20
20
    'BugTaskCreateQuestionView',
 
21
    'BugTaskDeletionView',
21
22
    'BugTaskEditView',
22
23
    'BugTaskExpirableListingView',
23
24
    'BugTaskListingItem',
53
54
    log,
54
55
    )
55
56
from operator import attrgetter
 
57
import os.path
56
58
import re
57
59
import transaction
58
60
import urllib
 
61
import urlparse
59
62
 
60
63
from lazr.delegates import delegates
61
64
from lazr.enum import (
73
76
    )
74
77
from lazr.restful.utils import smartquote
75
78
from lazr.uri import URI
 
79
import pystache
76
80
from pytz import utc
77
81
from simplejson import dumps
 
82
from simplejson.encoder import JSONEncoderForHTML
78
83
from z3c.pt.pagetemplate import ViewPageTemplateFile
79
84
from zope import (
80
85
    component,
87
92
    InputErrors,
88
93
    )
89
94
from zope.app.form.utility import setUpWidget
 
95
from zope.app.security.interfaces import IUnauthenticatedPrincipal
90
96
from zope.component import (
91
97
    ComponentLookupError,
92
98
    getAdapter,
114
120
    isinstance as zope_isinstance,
115
121
    removeSecurityProxy,
116
122
    )
 
123
from zope.traversing.browser import absoluteURL
117
124
from zope.traversing.interfaces import IPathAdapter
118
125
 
119
126
from canonical.config import config
155
162
    custom_widget,
156
163
    LaunchpadEditFormView,
157
164
    LaunchpadFormView,
 
165
    ReturnToReferrerMixin,
158
166
    )
159
167
from lp.app.browser.lazrjs import (
160
168
    TextAreaEditorWidget,
163
171
    )
164
172
from lp.app.browser.stringformatter import FormattersAPI
165
173
from lp.app.browser.tales import (
 
174
    DateTimeFormatterAPI,
166
175
    ObjectImageDisplayAPI,
167
176
    PersonFormatterAPI,
168
177
    )
264
273
from lp.registry.interfaces.sourcepackage import ISourcePackage
265
274
from lp.registry.model.personroles import PersonRoles
266
275
from lp.registry.vocabularies import MilestoneVocabulary
 
276
from lp.services.features import getFeatureFlag
267
277
from lp.services.fields import PersonChoice
268
278
from lp.services.propertycache import (
269
279
    cachedproperty,
270
280
    get_property_cache,
271
281
    )
 
282
from lp.services.utils import obfuscate_structure
272
283
 
273
284
vocabulary_registry = getVocabularyRegistry()
274
285
 
671
682
            cancel_url = canonical_url(self.context)
672
683
        return cancel_url
673
684
 
 
685
    @cachedproperty
 
686
    def api_request(self):
 
687
        return IWebServiceClientRequest(self.request)
 
688
 
674
689
    def initialize(self):
675
690
        """Set up the needed widgets."""
676
691
        bug = self.context.bug
677
692
        cache = IJSONRequestCache(self.request)
678
693
        cache.objects['bug'] = bug
 
694
        subscribers_url_data = {
 
695
            'web_link': canonical_url(bug, rootsite='bugs'),
 
696
            'self_link': absoluteURL(bug, self.api_request),
 
697
            }
 
698
        cache.objects['subscribers_portlet_url_data'] = subscribers_url_data
679
699
        cache.objects['total_comments_and_activity'] = (
680
700
            self.total_comments + self.total_activity)
681
701
        cache.objects['initial_comment_batch_offset'] = (
1755
1775
        self.updateContextFromData(data)
1756
1776
 
1757
1777
 
 
1778
class BugTaskDeletionView(ReturnToReferrerMixin, LaunchpadFormView):
 
1779
    """Used to delete a bugtask."""
 
1780
 
 
1781
    schema = IBugTask
 
1782
    field_names = []
 
1783
 
 
1784
    label = 'Remove bug task'
 
1785
    page_title = label
 
1786
 
 
1787
    @property
 
1788
    def next_url(self):
 
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
 
1792
        return None
 
1793
 
 
1794
    @action('Delete', name='delete_bugtask')
 
1795
    def delete_bugtask_action(self, action, data):
 
1796
        bugtask = self.context
 
1797
        bug = bugtask.bug
 
1798
        deleted_bugtask_url = canonical_url(self.context, rootsite='bugs')
 
1799
        message = ("This bug no longer affects %s."
 
1800
                    % bugtask.bugtargetdisplayname)
 
1801
        bugtask.delete()
 
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',
 
1815
                    'application/json')
 
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')
 
1821
            view.initialize()
 
1822
            return view.render()
 
1823
 
 
1824
 
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)
2137
2204
 
 
2205
    @property
 
2206
    def model(self):
 
2207
        """Provide flattened data about bugtask for simple templaters."""
 
2208
        age = DateTimeFormatterAPI(self.bug.datecreated).durationsince()
 
2209
        age += ' old'
 
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
 
2220
        else:
 
2221
            milestone_name = None
 
2222
        assignee = None
 
2223
        if self.assignee is not None:
 
2224
            assignee = self.assignee.displayname
 
2225
        return {
 
2226
            'age': age,
 
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,
 
2232
            'badges': badges,
 
2233
            'id': self.bug.id,
 
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,
 
2243
            }
 
2244
 
2138
2245
 
2139
2246
class BugListingBatchNavigator(TableBatchNavigator):
2140
2247
    """A specialised batch navigator to load smartly extra bug information."""
2145
2252
        # rules to a mixin so that MilestoneView and others can use it.
2146
2253
        self.request = request
2147
2254
        self.target_context = target_context
 
2255
        self.field_visibility = {
 
2256
            'show_age': False,
 
2257
            'show_assignee': False,
 
2258
            'show_bugtarget': True,
 
2259
            'show_bug_heat': True,
 
2260
            'show_id': True,
 
2261
            'show_importance': True,
 
2262
            'show_last_updated': False,
 
2263
            'show_milestone_name': False,
 
2264
            'show_reporter': False,
 
2265
            'show_status': True,
 
2266
            'show_tags': False,
 
2267
            'show_title': True,
 
2268
        }
2148
2269
        TableBatchNavigator.__init__(
2149
2270
            self, tasks, request, columns_to_show=columns_to_show, size=size)
2150
2271
 
2176
2297
        """Return a decorated list of visible bug tasks."""
2177
2298
        return [self._getListingItem(bugtask) for bugtask in self.batch]
2178
2299
 
 
2300
    @cachedproperty
 
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()
 
2306
 
 
2307
    @property
 
2308
    def mustache_listings(self):
 
2309
        return 'LP.mustache_listings = %s;' % dumps(
 
2310
            self.mustache_template, cls=JSONEncoderForHTML)
 
2311
 
 
2312
    @property
 
2313
    def mustache(self):
 
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'])
 
2320
 
 
2321
    @property
 
2322
    def model(self):
 
2323
        bugtasks = [bugtask.model for bugtask in self.getBugListingItems()]
 
2324
        for bugtask in bugtasks:
 
2325
            bugtask.update(self.field_visibility)
 
2326
        return {'bugtasks': bugtasks}
 
2327
 
2179
2328
 
2180
2329
class NominatedBugReviewAction(EnumeratedType):
2181
2330
    """Enumeration for nomination review actions"""
2300
2449
 
2301
2450
    implements(IBugTaskSearchListingMenu)
2302
2451
 
 
2452
    beta_features = ['bugs.dynamic_bug_listings.enabled']
 
2453
 
2303
2454
    # Only include <link> tags for bug feeds when using this view.
2304
2455
    feed_types = (
2305
2456
        BugTargetLatestBugsFeedLink,
2434
2585
 
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)
 
2594
 
 
2595
            def _getBatchInfo(batch):
 
2596
                if batch is None:
 
2597
                    return None
 
2598
                return {'memo': batch.range_memo,
 
2599
                        'start': batch.startNumber() - 1}
 
2600
 
 
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))
2437
2612
 
2438
2613
    @property
2439
2614
    def columns_to_show(self):
2462
2637
                "Unrecognized context; don't know which report "
2463
2638
                "columns to show.")
2464
2639
 
 
2640
    bugtask_table_template = ViewPageTemplateFile(
 
2641
        '../templates/bugs-table-include.pt')
 
2642
 
 
2643
    @property
 
2644
    def template(self):
 
2645
        query_string = self.request.get('QUERY_STRING') or ''
 
2646
        query_params = urlparse.parse_qs(query_string)
 
2647
        if 'batch_request' in query_params:
 
2648
            return self.bugtask_table_template
 
2649
        else:
 
2650
            return super(BugTaskSearchListingView, self).template
 
2651
 
2465
2652
    def validate_search_params(self):
2466
2653
        """Validate the params passed for the search.
2467
2654
 
3028
3215
        else:
3029
3216
            return None
3030
3217
 
 
3218
    @cachedproperty
 
3219
    def dynamic_bug_listing_enabled(self):
 
3220
        """Feature flag: Can the bug listing be customized?"""
 
3221
        return bool(getFeatureFlag('bugs.dynamic_bug_listings.enabled'))
 
3222
 
 
3223
    @property
 
3224
    def search_macro_title(self):
 
3225
        """The search macro's title text."""
 
3226
        return u"Search bugs %s" % self.context_description
 
3227
 
 
3228
    @property
 
3229
    def context_description(self):
 
3230
        """A phrase describing the context of the bug.
 
3231
 
 
3232
        The phrase is intended to be used for headings like
 
3233
        "Bugs in $context", "Search bugs in $context". This
 
3234
        property should be overridden for person related views.
 
3235
        """
 
3236
        return "in %s" % self.context.displayname
 
3237
 
3031
3238
 
3032
3239
class BugNominationsView(BugTaskSearchListingView):
3033
3240
    """View for accepting/declining bug nominations."""
3421
3628
        else:
3422
3629
            return 'false'
3423
3630
 
3424
 
    @property
 
3631
    @cachedproperty
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.
 
3634
        """
 
3635
        if getFeatureFlag('bugs.affected_count_includes_dupes.disabled'):
 
3636
            if self.current_user_affected_status:
 
3637
                return self.context.users_affected_count - 1
 
3638
            else:
 
3639
                return self.context.users_affected_count
3429
3640
        else:
 
3641
            return self.context.other_users_affected_count_with_dupes
 
3642
 
 
3643
    @cachedproperty
 
3644
    def total_users_affected_count(self):
 
3645
        """The number of affected users, typically across all users.
 
3646
 
 
3647
        Counting across duplicates may be disabled at run time.
 
3648
        """
 
3649
        if getFeatureFlag('bugs.affected_count_includes_dupes.disabled'):
3430
3650
            return self.context.users_affected_count
 
3651
        else:
 
3652
            return self.context.users_affected_count_with_dupes
3431
3653
 
3432
 
    @property
 
3654
    @cachedproperty
3433
3655
    def affected_statement(self):
3434
3656
        """The default "this bug affects" statement to show.
3435
3657
 
3436
3658
        The outputs of this method should be mirrored in
3437
3659
        MeTooChoiceSource._getSourceNames() (Javascript).
3438
3660
        """
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"
3444
 
            else:
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:
3448
3667
                return (
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))
3454
3670
            else:
3455
 
                return "This bug affects %d people, but not you" % (
3456
 
                    self.other_users_affected_count)
3457
 
        else:
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"
3462
3677
            else:
 
3678
                return "This bug affects you and %d other people" % (
 
3679
                    other_affected)
 
3680
        else:
 
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" % (
 
3687
                    other_affected)
3464
3688
 
3465
 
    @property
 
3689
    @cachedproperty
3466
3690
    def anon_affected_statement(self):
3467
3691
        """The "this bug affects" statement to show to anonymous users.
3468
3692
 
3469
3693
        The outputs of this method should be mirrored in
3470
3694
        MeTooChoiceSource._getSourceNames() (Javascript).
3471
3695
        """
3472
 
        if self.context.users_affected_count == 1:
 
3696
        affected = self.total_users_affected_count
 
3697
        if affected == 1:
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)
 
3699
        elif affected > 1:
 
3700
            return "This bug affects %d people" % affected
3477
3701
        else:
3478
3702
            return None
3479
3703
 
 
3704
    @property
 
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'))
 
3709
 
 
3710
    def canAddProjectTask(self):
 
3711
        """Can a new bug task on a project be added to this bug?
 
3712
 
 
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.
 
3715
 
 
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.
 
3721
 
 
3722
        """
 
3723
        bug = self.context
 
3724
        if self._allow_multipillar_private_bugs:
 
3725
            return True
 
3726
        return len(bug.bugtasks) == 0
 
3727
 
 
3728
    def canAddPackageTask(self):
 
3729
        """Can a new bug task on a src pkg be added to this bug?
 
3730
 
 
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,
 
3733
        so return False.
 
3734
 
 
3735
        A task on a given package may still be illegal to add, but
 
3736
        this will be caught when bug.addTask() is attempted.
 
3737
 
 
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.
 
3743
        """
 
3744
        bug = self.context
 
3745
        if self._allow_multipillar_private_bugs:
 
3746
            return True
 
3747
        for pillar in bug.affected_pillars:
 
3748
            if IProduct.providedBy(pillar):
 
3749
                return False
 
3750
        return True
 
3751
 
3480
3752
 
3481
3753
class BugTaskTableRowView(LaunchpadView, BugTaskBugWatchMixin):
3482
3754
    """Browser class for rendering a bugtask row on the bug page."""
3493
3765
        super(BugTaskTableRowView, self).__init__(context, request)
3494
3766
        self.milestone_source = MilestoneVocabulary
3495
3767
 
 
3768
    @cachedproperty
 
3769
    def api_request(self):
 
3770
        return IWebServiceClientRequest(self.request)
 
3771
 
3496
3772
    def initialize(self):
3497
3773
        super(BugTaskTableRowView, self).initialize()
3498
3774
        link = canonical_url(self.context)
3499
 
        task_link = edit_link = link + '/+editstatus'
 
3775
        task_link = edit_link = canonical_url(
 
3776
                                    self.context, view_name='+editstatus')
 
3777
        delete_link = canonical_url(self.context, view_name='+delete')
3500
3778
        can_edit = check_permission('launchpad.Edit', self.context)
3501
3779
        bugtask_id = self.context.id
3502
3780
        launchbag = getUtility(ILaunchBag)
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(
3522
 
                self.user),
 
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,
3528
3808
            )
3529
3809
 
 
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()
 
3817
 
3530
3818
    def canSeeTaskDetails(self):
3531
3819
        """Whether someone can see a task's status details.
3532
3820
 
3629
3917
            items = vocabulary_to_choice_edit_items(
3630
3918
                self._visible_milestones,
3631
3919
                value_fn=lambda item: canonical_url(
3632
 
                    item, request=IWebServiceClientRequest(self.request)))
 
3920
                    item, request=self.api_request))
3633
3921
            items.append({
3634
3922
                "name": "Remove milestone",
3635
3923
                "disabled": False,
3643
3931
        """Return the canonical url for the bugtask."""
3644
3932
        return canonical_url(self.context)
3645
3933
 
3646
 
    @property
 
3934
    @cachedproperty
3647
3935
    def user_can_edit_importance(self):
3648
3936
        """Can the user edit the Importance field?
3649
3937
 
3650
3938
        If yes, return True, otherwise return False.
3651
3939
        """
3652
 
        return self.context.userCanEditImportance(self.user)
 
3940
        bugtask = self.context
 
3941
        return (self.user_can_edit_status
 
3942
                and bugtask.userCanEditImportance(self.user))
 
3943
 
 
3944
    @cachedproperty
 
3945
    def user_can_edit_status(self):
 
3946
        """Can the user edit the Status field?
 
3947
 
 
3948
        If yes, return True, otherwise return False.
 
3949
        """
 
3950
        bugtask = self.context
 
3951
        edit_allowed = bugtask.target_uses_malone or bugtask.bugwatch
 
3952
        if bugtask.bugwatch:
 
3953
            bugtracker = bugtask.bugwatch.bugtracker
 
3954
            edit_allowed = (
 
3955
                bugtracker.bugtrackertype == BugTrackerType.EMAILADDRESS)
 
3956
        return edit_allowed
3653
3957
 
3654
3958
    @property
3655
3959
    def user_can_edit_assignee(self):
3667
3971
        """
3668
3972
        return self.context.userCanEditMilestone(self.user)
3669
3973
 
 
3974
    @cachedproperty
 
3975
    def user_can_delete_bugtask(self):
 
3976
        """Can the user delete the bug task?
 
3977
 
 
3978
        If yes, return True, otherwise return False.
 
3979
        """
 
3980
        bugtask = self.context
 
3981
        return (check_permission('launchpad.Delete', bugtask)
 
3982
                and bugtask.canBeDeleted())
 
3983
 
3670
3984
    @property
3671
3985
    def style_for_add_milestone(self):
3672
3986
        if self.context.milestone is None:
3681
3995
        else:
3682
3996
            return ''
3683
3997
 
3684
 
    def js_config(self):
3685
 
        """Configuration for the JS widgets on the row, JSON-serialized."""
 
3998
    def bugtask_config(self):
 
3999
        """Configuration for the bugtask JS widgets on the row."""
3686
4000
        assignee_vocabulary, assignee_vocabulary_filters = (
3687
4001
            get_assignee_vocabulary_info(self.context))
3688
4002
        # If we have no filters or just the ALL filter, then no filtering
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
3707
 
        return dumps(dict(
 
4021
        return dict(
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,
3723
4042
            milestone_value=(
3724
4043
                canonical_url(
3725
4044
                    cx.milestone,
3726
 
                    request=IWebServiceClientRequest(self.request))
 
4045
                    request=self.api_request)
3727
4046
                if cx.milestone else None),
3728
4047
            user_can_edit_assignee=self.user_can_edit_assignee,
3729
4048
            user_can_edit_milestone=self.user_can_edit_milestone,
3730
 
            user_can_edit_status=not cx.bugwatch,
3731
 
            user_can_edit_importance=(
3732
 
                self.user_can_edit_importance and not cx.bugwatch)
3733
 
            ))
 
4049
            user_can_edit_status=self.user_can_edit_status,
 
4050
            user_can_edit_importance=self.user_can_edit_importance,
 
4051
            )
3734
4052
 
3735
4053
 
3736
4054
class BugsBugTaskSearchListingView(BugTaskSearchListingView):