~launchpad-pqm/launchpad/devel

« back to all changes in this revision

Viewing changes to lib/lp/app/browser/tales.py

  • Committer: Curtis Hovey
  • Date: 2011-08-18 20:56:37 UTC
  • mto: This revision was merged to the branch mainline in revision 13736.
  • Revision ID: curtis.hovey@canonical.com-20110818205637-ae0pf9aexdea2mlb
Cleaned up doctrings and hushed lint.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright 2009-2011 Canonical Ltd.  This software is licensed under the
 
1
# Copyright 2009-2010 Canonical Ltd.  This software is licensed under the
2
2
# GNU Affero General Public License version 3 (see the file LICENSE).
3
3
 
 
4
# pylint: disable-msg=C0103,W0613,R0911,F0401
 
5
#
4
6
"""Implementation of the lp: htmlform: fmt: namespaces in TALES."""
5
7
 
6
8
__metaclass__ = type
7
9
 
8
10
import bisect
9
11
import cgi
10
 
from datetime import (
11
 
    datetime,
12
 
    timedelta,
13
 
    )
14
12
from email.Utils import formatdate
15
13
import math
16
14
import os.path
19
17
from textwrap import dedent
20
18
import urllib
21
19
 
 
20
##import warnings
 
21
 
 
22
from datetime import datetime, timedelta
22
23
from lazr.enum import enumerated_type_registry
23
24
from lazr.uri import URI
24
 
import pytz
25
 
from z3c.ptcompat import ViewPageTemplateFile
26
 
from zope.error.interfaces import IErrorReportingUtility
27
 
from zope.interface import (
28
 
    Attribute,
29
 
    Interface,
30
 
    implements,
31
 
    )
32
 
from zope.component import (
33
 
    adapts,
34
 
    getUtility,
35
 
    queryAdapter,
36
 
    getMultiAdapter,
37
 
    )
 
25
 
 
26
from zope.interface import Interface, Attribute, implements
 
27
from zope.component import adapts, getUtility, queryAdapter, getMultiAdapter
38
28
from zope.app import zapi
39
29
from zope.publisher.browser import BrowserView
40
30
from zope.traversing.interfaces import (
46
36
from zope.security.proxy import isinstance as zope_isinstance
47
37
from zope.schema import TextLine
48
38
 
 
39
import pytz
 
40
from z3c.ptcompat import ViewPageTemplateFile
 
41
 
49
42
from canonical.launchpad import _
50
43
from canonical.launchpad.interfaces.launchpad import (
51
 
    IHasIcon,
52
 
    IHasLogo,
53
 
    IHasMugshot,
54
 
    IPrivacy
55
 
    )
 
44
    IHasIcon, IHasLogo, IHasMugshot, IPrivacy)
56
45
from canonical.launchpad.layers import LaunchpadLayer
 
46
import canonical.launchpad.pagetitles
57
47
from canonical.launchpad.webapp import canonical_url, urlappend
58
48
from canonical.launchpad.webapp.authorization import check_permission
59
49
from canonical.launchpad.webapp.badge import IHasBadges
60
50
from canonical.launchpad.webapp.interfaces import (
61
 
    IApplicationMenu,
62
 
    IContextMenu,
63
 
    IFacetMenu,
64
 
    ILaunchBag,
65
 
    INavigationMenu,
66
 
    IPrimaryContext,
67
 
    NoCanonicalUrl
68
 
    )
69
 
from canonical.launchpad.webapp.menu import (
70
 
    get_current_view,
71
 
    get_facet,
72
 
    )
 
51
    IApplicationMenu, IContextMenu, IFacetMenu, ILaunchBag, INavigationMenu,
 
52
    IPrimaryContext, NoCanonicalUrl)
 
53
from canonical.launchpad.webapp.menu import get_current_view, get_facet
73
54
from canonical.launchpad.webapp.publisher import (
74
 
    get_current_browser_request,
75
 
    LaunchpadView,
76
 
    nearest
77
 
    )
 
55
    get_current_browser_request, LaunchpadView, nearest)
78
56
from canonical.launchpad.webapp.session import get_cookie_domain
79
57
from canonical.lazr.canonicalurl import nearest_adapter
80
58
from lp.app.browser.stringformatter import escape, FormattersAPI
83
61
from lp.bugs.interfaces.bug import IBug
84
62
from lp.buildmaster.enums import BuildStatus
85
63
from lp.code.interfaces.branch import IBranch
86
 
from lp.services.features import getFeatureFlag
87
64
from lp.soyuz.enums import ArchivePurpose
88
65
from lp.soyuz.interfaces.archive import IPPA
89
66
from lp.soyuz.interfaces.archivesubscriber import IArchiveSubscriberSet
665
642
        """The page title to be used.
666
643
 
667
644
        By default, reverse breadcrumbs are always used if they are available.
668
 
        If not available, then the view's .page_title attribut is used.
669
 
        If breadcrumbs are available, then a view can still choose to
670
 
        override them by setting the attribute .override_title_breadcrumbs
671
 
        to True.
 
645
        If not available, then the view's .page_title attribute or entry in
 
646
        pagetitles.py (deprecated) is used.  If breadcrumbs are available,
 
647
        then a view can still choose to override them by setting the attribute
 
648
        .override_title_breadcrumbs to True.
672
649
        """
673
 
        ROOT_TITLE = 'Launchpad'
674
650
        view = self._context
675
651
        request = get_current_browser_request()
 
652
        module = canonical.launchpad.pagetitles
676
653
        hierarchy_view = getMultiAdapter(
677
654
            (view.context, request), name='+hierarchy')
678
655
        override = getattr(view, 'override_title_breadcrumbs', False)
690
667
            if template is None:
691
668
                template = getattr(view, 'index', None)
692
669
                if template is None:
693
 
                    return ROOT_TITLE
 
670
                    return module.DEFAULT_LAUNCHPAD_TITLE
 
671
            # There is no .page_title attribute on the view, so fallback to
 
672
            # looking for an an entry in pagetitles.py.  This is deprecated
 
673
            # though, so issue a warning.
 
674
            filename = os.path.basename(template.filename)
 
675
            name, ext = os.path.splitext(filename)
 
676
            title_name = name.replace('-', '_')
 
677
            title_object = getattr(module, title_name, None)
 
678
            # Page titles are mandatory.
 
679
            assert title_object is not None, (
 
680
                'No .page_title or pagetitles.py found for %s'
 
681
                % template.filename)
 
682
            ## 2009-09-08 BarryWarsaw bug 426527: Enable this when we want to
 
683
            ## force conversions from pagetitles.py; however tests will fail
 
684
            ## because of this output.
 
685
            ## warnings.warn('Old style pagetitles.py entry found for %s. '
 
686
            ##               'Switch to using a .page_title attribute on the '
 
687
            ##               'view instead.' % template.filename,
 
688
            ##               DeprecationWarning)
 
689
            if isinstance(title_object, basestring):
 
690
                return title_object
 
691
            else:
 
692
                title = title_object(view.context, view)
 
693
                if title is None:
 
694
                    return module.DEFAULT_LAUNCHPAD_TITLE
 
695
                else:
 
696
                    return title
694
697
        # Use the reverse breadcrumbs.
695
698
        return SEPARATOR.join(
696
699
            breadcrumb.text for breadcrumb
717
720
        elif IProjectGroup.providedBy(context):
718
721
            return 'sprite project'
719
722
        elif IPerson.providedBy(context):
720
 
            if context.is_team:
 
723
            if context.isTeam():
721
724
                return 'sprite team'
722
725
            else:
723
726
                if context.is_valid_person:
752
755
        if IProjectGroup.providedBy(context):
753
756
            return '/@@/project-logo'
754
757
        elif IPerson.providedBy(context):
755
 
            if context.is_team:
 
758
            if context.isTeam():
756
759
                return '/@@/team-logo'
757
760
            else:
758
761
                if context.is_valid_person:
774
777
        if IProjectGroup.providedBy(context):
775
778
            return '/@@/project-mugshot'
776
779
        elif IPerson.providedBy(context):
777
 
            if context.is_team:
 
780
            if context.isTeam():
778
781
                return '/@@/team-mugshot'
779
782
            else:
780
783
                if context.is_valid_person:
789
792
            return '/@@/meeting-mugshot'
790
793
        return None
791
794
 
792
 
    def custom_icon_url(self):
 
795
    def _get_custom_icon_url(self):
793
796
        """Return the URL for this object's icon."""
794
797
        context = self._context
795
798
        if IHasIcon.providedBy(context) and context.icon is not None:
885
888
        '<span alt="%s" title="%s" class="%s">&nbsp;</span>')
886
889
 
887
890
    linked_icon_template = (
888
 
        '<a href="%s" alt="%s" title="%s" class="%s">&nbsp;</a>')
 
891
        '<a href="%s" alt="%s" title="%s" class="%s"></a>')
889
892
 
890
893
    def traverse(self, name, furtherPath):
891
894
        """Special-case traversal for icons with an optional rootsite."""
1080
1083
            BuildStatus.BUILDING: {'src': "/@@/processing"},
1081
1084
            BuildStatus.UPLOADING: {'src': "/@@/processing"},
1082
1085
            BuildStatus.FAILEDTOUPLOAD: {'src': "/@@/build-failedtoupload"},
1083
 
            BuildStatus.CANCELLING: {'src': "/@@/processing"},
1084
 
            BuildStatus.CANCELLED: {'src': "/@@/build-failed"},
1085
1086
            }
1086
1087
 
1087
1088
        alt = '[%s]' % self._context.status.name
1174
1175
    def _makeLink(self, view_name, rootsite, text):
1175
1176
        person = self._context
1176
1177
        url = self.url(view_name, rootsite)
1177
 
        custom_icon = ObjectImageDisplayAPI(person).custom_icon_url()
 
1178
        custom_icon = ObjectImageDisplayAPI(person)._get_custom_icon_url()
1178
1179
        if custom_icon is None:
1179
1180
            css_class = ObjectImageDisplayAPI(person).sprite_css()
1180
1181
            return (u'<a href="%s" class="%s">%s</a>') % (
1206
1207
    def icon(self, view_name):
1207
1208
        """Return the URL for the person's icon."""
1208
1209
        custom_icon = ObjectImageDisplayAPI(
1209
 
            self._context).custom_icon_url()
 
1210
            self._context)._get_custom_icon_url()
1210
1211
        if custom_icon is None:
1211
1212
            css_class = ObjectImageDisplayAPI(self._context).sprite_css()
1212
1213
            return '<span class="' + css_class + '"></span>'
1219
1220
        The link text uses both the display name and Launchpad id to clearly
1220
1221
        indicate which user profile is linked.
1221
1222
        """
1222
 
        text = self.unique_displayname(None)
 
1223
        from lp.services.features import getFeatureFlag
 
1224
        if bool(getFeatureFlag('disclosure.picker_enhancements.enabled')):
 
1225
            text = self.unique_displayname(None)
 
1226
            # XXX sinzui 2011-05-31: Remove this next line when the feature
 
1227
            # flag is removed.
 
1228
            view_name = None
 
1229
        elif view_name == 'id-only':
 
1230
            # XXX sinzui 2011-05-31: remove this block and /id-only from
 
1231
            # launchpad-loginstatus.pt whwn the feature flag is removed.
 
1232
            text = self._context.name
 
1233
            view_name = None
 
1234
        else:
 
1235
            text = self._context.displayname
1223
1236
        return self._makeLink(view_name, 'mainsite', text)
1224
1237
 
1225
1238
 
1226
 
class MixedVisibilityError(Exception):
1227
 
    """An informational error that visibility is being mixed."""
1228
 
 
1229
 
 
1230
1239
class TeamFormatterAPI(PersonFormatterAPI):
1231
1240
    """Adapter for `ITeam` objects to a formatted string."""
1232
1241
 
1238
1247
        The default URL for a team is to the mainsite. None is returned
1239
1248
        when the user does not have permission to review the team.
1240
1249
        """
1241
 
        if not check_permission('launchpad.LimitedView', self._context):
 
1250
        if not check_permission('launchpad.View', self._context):
1242
1251
            # This person has no permission to view the team details.
1243
 
            self._report_visibility_leak()
1244
1252
            return None
1245
1253
        return super(TeamFormatterAPI, self).url(view_name, rootsite)
1246
1254
 
1247
1255
    def api_url(self, context):
1248
1256
        """See `ObjectFormatterAPI`."""
1249
 
        if not check_permission('launchpad.LimitedView', self._context):
 
1257
        if not check_permission('launchpad.View', self._context):
1250
1258
            # This person has no permission to view the team details.
1251
 
            self._report_visibility_leak()
1252
1259
            return None
1253
1260
        return super(TeamFormatterAPI, self).api_url(context)
1254
1261
 
1259
1266
        when the user does not have permission to review the team.
1260
1267
        """
1261
1268
        person = self._context
1262
 
        if not check_permission('launchpad.LimitedView', person):
 
1269
        if not check_permission('launchpad.View', person):
1263
1270
            # This person has no permission to view the team details.
1264
 
            self._report_visibility_leak()
1265
1271
            return '<span class="sprite team">%s</span>' % cgi.escape(
1266
1272
                self.hidden)
1267
1273
        return super(TeamFormatterAPI, self).link(view_name, rootsite)
1268
1274
 
1269
 
    def icon(self, view_name):
1270
 
        team = self._context
1271
 
        if not check_permission('launchpad.LimitedView', team):
1272
 
            css_class = ObjectImageDisplayAPI(team).sprite_css()
1273
 
            return '<span class="' + css_class + '"></span>'
1274
 
        else:
1275
 
            return super(TeamFormatterAPI, self).icon(view_name)
1276
 
 
1277
1275
    def displayname(self, view_name, rootsite=None):
1278
1276
        """See `PersonFormatterAPI`."""
1279
1277
        person = self._context
1280
 
        if not check_permission('launchpad.LimitedView', person):
 
1278
        if not check_permission('launchpad.View', person):
1281
1279
            # This person has no permission to view the team details.
1282
 
            self._report_visibility_leak()
1283
1280
            return self.hidden
1284
1281
        return super(TeamFormatterAPI, self).displayname(view_name, rootsite)
1285
1282
 
1286
1283
    def unique_displayname(self, view_name):
1287
1284
        """See `PersonFormatterAPI`."""
1288
1285
        person = self._context
1289
 
        if not check_permission('launchpad.LimitedView', person):
 
1286
        if not check_permission('launchpad.View', person):
1290
1287
            # This person has no permission to view the team details.
1291
 
            self._report_visibility_leak()
1292
1288
            return self.hidden
1293
1289
        return super(TeamFormatterAPI, self).unique_displayname(view_name)
1294
1290
 
1295
 
    def _report_visibility_leak(self):
1296
 
        if bool(getFeatureFlag('disclosure.log_private_team_leaks.enabled')):
1297
 
            request = get_current_browser_request()
1298
 
            try:
1299
 
                raise MixedVisibilityError()
1300
 
            except MixedVisibilityError:
1301
 
                getUtility(IErrorReportingUtility).raising(
1302
 
                    sys.exc_info(), request)
1303
 
 
1304
1291
 
1305
1292
class CustomizableFormatter(ObjectFormatterAPI):
1306
1293
    """A ObjectFormatterAPI that is easy to customize.
1418
1405
    _link_summary_template = '%(displayname)s'
1419
1406
    _link_permission = 'zope.Public'
1420
1407
 
1421
 
    traversable_names = {
1422
 
        'api_url': 'api_url',
1423
 
        'link': 'link',
1424
 
        'url': 'url',
1425
 
        'link_with_displayname': 'link_with_displayname'
1426
 
        }
1427
 
 
1428
1408
    def _link_summary_values(self):
1429
1409
        displayname = self._context.displayname
1430
1410
        return {'displayname': displayname}
1436
1416
        """
1437
1417
        return super(PillarFormatterAPI, self).url(view_name, rootsite)
1438
1418
 
1439
 
    def _getLinkHTML(self, view_name, rootsite,
1440
 
        template, custom_icon_template):
1441
 
        """Generates html, mapping a link context to given templates.
1442
 
 
1443
 
        The html is generated using given `template` or `custom_icon_template`
1444
 
        based on the presence of a custom icon for Products/ProjectGroups.
1445
 
        Named string substitution is used to render the final html
1446
 
        (see below for a list of allowed keys).
1447
 
 
1448
 
        The link context is a dict containing info about current
1449
 
        Products or ProjectGroups.
1450
 
        Keys are `url`, `name`, `displayname`, `custom_icon` (if present),
1451
 
        `css_class` (if a custom icon does not exist),
1452
 
        'summary' (see CustomizableFormatter._make_link_summary()).
1453
 
        """
1454
 
        context = self._context
1455
 
        mapping = {
1456
 
            'url': self.url(view_name, rootsite),
1457
 
            'name': cgi.escape(context.name),
1458
 
            'displayname': cgi.escape(context.displayname),
1459
 
            'summary': self._make_link_summary(),
1460
 
            }
1461
 
        custom_icon = ObjectImageDisplayAPI(context).custom_icon_url()
1462
 
        if custom_icon is None:
1463
 
            mapping['css_class'] = ObjectImageDisplayAPI(context).sprite_css()
1464
 
            return template % mapping
1465
 
        mapping['custom_icon'] = custom_icon
1466
 
        return custom_icon_template % mapping
1467
 
 
1468
1419
    def link(self, view_name, rootsite='mainsite'):
1469
1420
        """The html to show a link to a Product, ProjectGroup or distribution.
1470
1421
 
1471
1422
        In the case of Products or ProjectGroups we display the custom
1472
1423
        icon, if one exists. The default URL for a pillar is to the mainsite.
1473
1424
        """
1474
 
        super(PillarFormatterAPI, self).link(view_name)
1475
 
        template = u'<a href="%(url)s" class="%(css_class)s">%(summary)s</a>'
1476
 
        custom_icon_template = (
1477
 
            u'<a href="%(url)s" class="bg-image" '
1478
 
            u'style="background-image: url(%(custom_icon)s)">%(summary)s</a>'
1479
 
            )
1480
 
        return self._getLinkHTML(
1481
 
            view_name, rootsite, template, custom_icon_template)
1482
 
 
1483
 
    def link_with_displayname(self, view_name, rootsite='mainsite'):
1484
 
        """The html to show a link to a Product, ProjectGroup or
1485
 
        distribution, including displayname and name.
1486
 
 
1487
 
        In the case of Products or ProjectGroups we display the custom
1488
 
        icon, if one exists. The default URL for a pillar is to the mainsite.
1489
 
        """
1490
 
        super(PillarFormatterAPI, self).link(view_name)
1491
 
        template = (
1492
 
            u'<a href="%(url)s" class="%(css_class)s">%(displayname)s</a>'
1493
 
            u'&nbsp;(<a href="%(url)s">%(name)s</a>)'
1494
 
            )
1495
 
        custom_icon_template = (
1496
 
            u'<a href="%(url)s" class="bg-image" '
1497
 
            u'style="background-image: url(%(custom_icon)s)">'
1498
 
            u'%(displayname)s</a>&nbsp;(<a href="%(url)s">%(name)s</a>)'
1499
 
            )
1500
 
        return self._getLinkHTML(
1501
 
            view_name, rootsite, template, custom_icon_template)
 
1425
 
 
1426
        html = super(PillarFormatterAPI, self).link(view_name)
 
1427
        context = self._context
 
1428
        custom_icon = ObjectImageDisplayAPI(
 
1429
            context)._get_custom_icon_url()
 
1430
        url = self.url(view_name, rootsite)
 
1431
        summary = self._make_link_summary()
 
1432
        if custom_icon is None:
 
1433
            css_class = ObjectImageDisplayAPI(context).sprite_css()
 
1434
            html = (u'<a href="%s" class="%s">%s</a>') % (
 
1435
                url, css_class, summary)
 
1436
        else:
 
1437
            html = (u'<a href="%s" class="bg-image" '
 
1438
                     'style="background-image: url(%s)">%s</a>') % (
 
1439
                url, custom_icon, summary)
 
1440
        return html
1502
1441
 
1503
1442
 
1504
1443
class DistroSeriesFormatterAPI(CustomizableFormatter):
1724
1663
    def link(self, view_name, rootsite=None):
1725
1664
        build = self._context
1726
1665
        if not check_permission('launchpad.View', build):
1727
 
            return 'private job'
 
1666
            return 'private source'
1728
1667
 
1729
1668
        url = self.url(view_name=view_name, rootsite=rootsite)
1730
1669
        title = cgi.escape(build.title)
2175
2114
    def isodate(self):
2176
2115
        return self._datetime.isoformat()
2177
2116
 
2178
 
    @staticmethod
2179
 
    def _yearDelta(old, new):
2180
 
        """Return the difference in years between two datetimes.
2181
 
 
2182
 
        :param old: The old date
2183
 
        :param new: The new date
2184
 
        """
2185
 
        year_delta = new.year - old.year
2186
 
        year_timedelta = datetime(new.year, 1, 1) - datetime(old.year, 1, 1)
2187
 
        if new - old < year_timedelta:
2188
 
            year_delta -= 1
2189
 
        return year_delta
2190
 
 
2191
 
    def durationsince(self):
2192
 
        """How long since the datetime, as a string."""
2193
 
        now = self._now()
2194
 
        number = self._yearDelta(self._datetime, now)
2195
 
        unit = 'year'
2196
 
        if number < 1:
2197
 
            delta = now - self._datetime
2198
 
            if delta.days > 0:
2199
 
                number = delta.days
2200
 
                unit = 'day'
2201
 
            else:
2202
 
                number = delta.seconds / 60
2203
 
                if number == 0:
2204
 
                    return 'less than a minute'
2205
 
                unit = 'minute'
2206
 
                if number >= 60:
2207
 
                    number /= 60
2208
 
                    unit = 'hour'
2209
 
        if number != 1:
2210
 
            unit += 's'
2211
 
        return '%d %s' % (number, unit)
2212
 
 
2213
2117
 
2214
2118
class SeriesSourcePackageBranchFormatter(ObjectFormatterAPI):
2215
2119
    """Formatter for a SourcePackage, Pocket -> Branch link.
2241
2145
            return self.exactduration()
2242
2146
        elif name == 'approximateduration':
2243
2147
            return self.approximateduration()
2244
 
        elif name == 'millisecondduration':
2245
 
            return self.millisecondduration()
2246
2148
        else:
2247
2149
            raise TraversalError(name)
2248
2150
 
2388
2290
        weeks = int(round(seconds / (7 * 24 * 3600.0)))
2389
2291
        return "%d weeks" % weeks
2390
2292
 
2391
 
    def millisecondduration(self):
2392
 
        return str(
2393
 
            (self._duration.days * 24 * 3600
2394
 
             + self._duration.seconds * 1000
2395
 
             + self._duration.microseconds // 1000)) + 'ms'
2396
 
 
2397
2293
 
2398
2294
class LinkFormatterAPI(ObjectFormatterAPI):
2399
2295
    """Adapter from Link objects to a formatted anchor."""
2453
2349
    return clean_path_split
2454
2350
 
2455
2351
 
 
2352
class PageTemplateContextsAPI:
 
2353
    """Adapter from page tempate's CONTEXTS object to fmt:pagetitle.
 
2354
 
 
2355
    This is registered to be used for the dict type.
 
2356
    """
 
2357
    # 2009-09-08 BarryWarsaw bug 426532.  Remove this class, all references
 
2358
    # to it, and all instances of CONTEXTS/fmt:pagetitle
 
2359
    implements(ITraversable)
 
2360
 
 
2361
    def __init__(self, contextdict):
 
2362
        self.contextdict = contextdict
 
2363
 
 
2364
    def traverse(self, name, furtherPath):
 
2365
        if name == 'pagetitle':
 
2366
            return self.pagetitle()
 
2367
        else:
 
2368
            raise TraversalError(name)
 
2369
 
 
2370
    def pagetitle(self):
 
2371
        """Return the string title for the page template CONTEXTS dict.
 
2372
 
 
2373
        Take the simple filename without extension from
 
2374
        self.contextdict['template'].filename, replace any hyphens with
 
2375
        underscores, and use this to look up a string, unicode or
 
2376
        function in the module canonical.launchpad.pagetitles.
 
2377
 
 
2378
        If no suitable object is found in canonical.launchpad.pagetitles, emit
 
2379
        a warning that this page has no title, and return the default page
 
2380
        title.
 
2381
        """
 
2382
        template = self.contextdict['template']
 
2383
        filename = os.path.basename(template.filename)
 
2384
        name, ext = os.path.splitext(filename)
 
2385
        name = name.replace('-', '_')
 
2386
        titleobj = getattr(canonical.launchpad.pagetitles, name, None)
 
2387
        if titleobj is None:
 
2388
            raise AssertionError(
 
2389
                 "No page title in canonical.launchpad.pagetitles "
 
2390
                 "for %s" % name)
 
2391
        elif isinstance(titleobj, basestring):
 
2392
            return titleobj
 
2393
        else:
 
2394
            context = self.contextdict['context']
 
2395
            view = self.contextdict['view']
 
2396
            title = titleobj(context, view)
 
2397
            if title is None:
 
2398
                return canonical.launchpad.pagetitles.DEFAULT_LAUNCHPAD_TITLE
 
2399
            else:
 
2400
                return title
 
2401
 
 
2402
 
2456
2403
class PermissionRequiredQuery:
2457
2404
    """Check if the logged in user has a given permission on a given object.
2458
2405