19
17
from textwrap import dedent
22
from datetime import datetime, timedelta
22
23
from lazr.enum import enumerated_type_registry
23
24
from lazr.uri import URI
25
from z3c.ptcompat import ViewPageTemplateFile
26
from zope.error.interfaces import IErrorReportingUtility
27
from zope.interface import (
32
from zope.component import (
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
40
from z3c.ptcompat import ViewPageTemplateFile
49
42
from canonical.launchpad import _
50
43
from canonical.launchpad.interfaces.launchpad import (
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 (
69
from canonical.launchpad.webapp.menu import (
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,
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
665
642
"""The page title to be used.
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
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.
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:
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'
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):
692
title = title_object(view.context, view)
694
return module.DEFAULT_LAUNCHPAD_TITLE
694
697
# Use the reverse breadcrumbs.
695
698
return SEPARATOR.join(
696
699
breadcrumb.text for breadcrumb
1219
1220
The link text uses both the display name and Launchpad id to clearly
1220
1221
indicate which user profile is linked.
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
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
1235
text = self._context.displayname
1223
1236
return self._makeLink(view_name, 'mainsite', text)
1226
class MixedVisibilityError(Exception):
1227
"""An informational error that visibility is being mixed."""
1230
1239
class TeamFormatterAPI(PersonFormatterAPI):
1231
1240
"""Adapter for `ITeam` objects to a formatted string."""
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.
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()
1245
1253
return super(TeamFormatterAPI, self).url(view_name, rootsite)
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()
1253
1260
return super(TeamFormatterAPI, self).api_url(context)
1259
1266
when the user does not have permission to review the team.
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(
1267
1273
return super(TeamFormatterAPI, self).link(view_name, rootsite)
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>'
1275
return super(TeamFormatterAPI, self).icon(view_name)
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)
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)
1295
def _report_visibility_leak(self):
1296
if bool(getFeatureFlag('disclosure.log_private_team_leaks.enabled')):
1297
request = get_current_browser_request()
1299
raise MixedVisibilityError()
1300
except MixedVisibilityError:
1301
getUtility(IErrorReportingUtility).raising(
1302
sys.exc_info(), request)
1305
1292
class CustomizableFormatter(ObjectFormatterAPI):
1306
1293
"""A ObjectFormatterAPI that is easy to customize.
1437
1417
return super(PillarFormatterAPI, self).url(view_name, rootsite)
1439
def _getLinkHTML(self, view_name, rootsite,
1440
template, custom_icon_template):
1441
"""Generates html, mapping a link context to given templates.
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).
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()).
1454
context = self._context
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(),
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
1468
1419
def link(self, view_name, rootsite='mainsite'):
1469
1420
"""The html to show a link to a Product, ProjectGroup or distribution.
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.
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>'
1480
return self._getLinkHTML(
1481
view_name, rootsite, template, custom_icon_template)
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.
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.
1490
super(PillarFormatterAPI, self).link(view_name)
1492
u'<a href="%(url)s" class="%(css_class)s">%(displayname)s</a>'
1493
u' (<a href="%(url)s">%(name)s</a>)'
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> (<a href="%(url)s">%(name)s</a>)'
1500
return self._getLinkHTML(
1501
view_name, rootsite, template, custom_icon_template)
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)
1437
html = (u'<a href="%s" class="bg-image" '
1438
'style="background-image: url(%s)">%s</a>') % (
1439
url, custom_icon, summary)
1504
1443
class DistroSeriesFormatterAPI(CustomizableFormatter):
2175
2114
def isodate(self):
2176
2115
return self._datetime.isoformat()
2179
def _yearDelta(old, new):
2180
"""Return the difference in years between two datetimes.
2182
:param old: The old date
2183
:param new: The new date
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:
2191
def durationsince(self):
2192
"""How long since the datetime, as a string."""
2194
number = self._yearDelta(self._datetime, now)
2197
delta = now - self._datetime
2202
number = delta.seconds / 60
2204
return 'less than a minute'
2211
return '%d %s' % (number, unit)
2214
2118
class SeriesSourcePackageBranchFormatter(ObjectFormatterAPI):
2215
2119
"""Formatter for a SourcePackage, Pocket -> Branch link.
2453
2349
return clean_path_split
2352
class PageTemplateContextsAPI:
2353
"""Adapter from page tempate's CONTEXTS object to fmt:pagetitle.
2355
This is registered to be used for the dict type.
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)
2361
def __init__(self, contextdict):
2362
self.contextdict = contextdict
2364
def traverse(self, name, furtherPath):
2365
if name == 'pagetitle':
2366
return self.pagetitle()
2368
raise TraversalError(name)
2370
def pagetitle(self):
2371
"""Return the string title for the page template CONTEXTS dict.
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.
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
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 "
2391
elif isinstance(titleobj, basestring):
2394
context = self.contextdict['context']
2395
view = self.contextdict['view']
2396
title = titleobj(context, view)
2398
return canonical.launchpad.pagetitles.DEFAULT_LAUNCHPAD_TITLE
2456
2403
class PermissionRequiredQuery:
2457
2404
"""Check if the logged in user has a given permission on a given object.