~launchpad-pqm/launchpad/devel

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
#!/usr/bin/python -S
#
# Copyright 2011 Canonical Ltd.  This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).

"""list-pages -- list the pages that are in Launchpad

Prints out a CSV of all of the pages that exist in Launchpad. A "page" is a
whole, actual, web page. Something you might actually want to see filling a
browser window. Page fragments, portlets etc. are excluded.

The output contains the page ID, content interface, layer, template and
example URL.  Here 'layer' is a clue as to the subdomain.

The example URLs will look like::

  https://launchpad.dev/~<ITeam.name>/+addmember

Which means all segments are present, and you can get a real URL by
substituting data (in this case, a team name),

or::

  https://launchpad.dev/[[ITranslator]]/+admin

Which means we couldn't actually figure out the proper URL. [[ITranslator]]
might actually be made of multiple URL segments,

or::

  <IPOFile>/+export

Which means we have no clue how to figure out the URL.  This is generally
because our cheat objects don't match the app-encoded business logic.

"""

# The implementation approach here is fairly simple:
# - load the ZCML
# - go through all of the adapters, find the ones that look like they might
#   be for pages.
# - bang the adapter nut against the hardest rock we can find until it cracks
# - print the nourishing nutty goodness to stdout

import _pythonpath
_pythonpath

from inspect import getmro
import os

from zope.app.pagetemplate.simpleviewclass import simple
from zope.app.testing.functional import FunctionalTestSetup
from zope.component import adapts, getGlobalSiteManager
from zope.interface import directlyProvides, implements
from zope.publisher.interfaces.browser import IDefaultBrowserLayer

from canonical.config import config
from lp.services.scripts import execute_zcml_for_scripts
from lp.services.webapp.interfaces import ICanonicalUrlData
from lp.services.webapp.publisher import canonical_url


ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))


def load_zcml(zopeless=False):
    """Load ZCML the way we do in the web site."""
    # We can't load ZCML without generating configuration.
    config.generate_overrides()
    if zopeless:
        execute_zcml_for_scripts()
    else:
        FunctionalTestSetup(os.path.join(ROOT, 'site.zcml')).setUp()


def is_page_adapter(adapter):
    """Is 'adapter' a page adapter?

    We figure this out by checking to see whether it is adapting from
    IDefaultBrowserLayer or one of its subclasses.
    """
    for interface in adapter.required:
        if issubclass(interface, IDefaultBrowserLayer):
            return True
    return False


def get_view(adapter):
    """Get the view factory associated with 'adapter'."""
    return adapter.factory


def get_template_filename(view):
    """Return the template filename associated with 'view'."""
    try:
        filename = view.index.filename
    except AttributeError:
        return None
    return os.path.abspath(filename)


def has_page_title(adapter):
    """Does 'adapter' have a page title?

    We use this to tell if 'adapter' is indeed an adapter for rendering an
    entire page. The theory goes that if there's a page title associated with
    something, then it's a page.
    """
    view = get_view(adapter)
    marker = object()
    template = get_template_filename(view)
    if template is None:
        return False
    name = os.path.splitext(os.path.basename(template))[0].replace('-', '_')
    if (getattr(view, 'page_title', marker) is marker):
        return False
    return has_html_element(template)


def has_html_element(template):
    return '</html>' in open(template).read()


def iter_page_adapters():
    """Iterate over adapters for browser:page directives."""
    gsm = getGlobalSiteManager()
    return (a for a in gsm.registeredAdapters()
            if is_page_adapter(a) and has_page_title(a))


def iter_url_adapters():
    gsm = getGlobalSiteManager()
    return (a for a in gsm.registeredAdapters()
            if issubclass(a.provided, ICanonicalUrlData))


def format_page_adapter(a):
    """Format the adapter 'a' for end-user display.

    :return: A string of the form, "pageid,content,layer,template".
    """
    factory = a.factory
    try:
        bases = getmro(factory)
    except AttributeError:
        return a.name
    # Zope, bless it, does all sorts of type shenanigans to obscure the name
    # of the factory as it appears in our code. (See
    # z.browserpage.metaconfigure.page and
    # z.app.pagetemplate.simpleviewclass). Ideally, we'd just construct the
    # view, but that's hard, since in general they won't construct without an
    # object that implements the interface they need.
    bases = [b for b in bases
             if b is not simple
             and 'SimpleViewClass' not in b.__name__]
    template = get_template_filename(get_view(a))
    if template:
        template = os.path.relpath(template, ROOT)
    url = '%s/%s' % (get_example_canonical_url(a.required[0]), a.name)
    return '%s:%s,%s,%s,%s,%s' % (
        bases[0].__name__,
        a.name,
        a.required[0].__name__,
        a.required[1].__name__,
        template,
        url,
        )


_BLACKLIST = [
    '__conform__',
    ]
class Whatever(object):

    def __init__(self, name, interface=None):
        self._name = name
        self._interface = interface
        if interface is not None:
            directlyProvides(self, interface)

    def __str__(self):
        return '<%s>' % (self._name,)

    def __repr__(self):
        return '<%s>' % str(self)

    def __int__(self):
        return 1

    def __call__(self, *args, **kwargs):
        args = map(repr, args)
        args.extend('%s=%r' % (k, v) for k, v in kwargs.iteritems())
        # If we're being called with no args, assume this is part of crazy
        # TALES stuff:
        #   lib/canonical/launchpad/webapp/metazcml.py(365)path()
        #   -> return self._compiled_path_expression(self._expression_context)
        if args:
            name = '%s(%s)' % (self._name, ', '.join(args))
            return Whatever(name)
        else:
            return str(self)

    def __getattr__(self, name):
        if name in _BLACKLIST:
            raise AttributeError
        if self._interface:
            interface = getattr(self._interface[name], 'schema', None)
        else:
            interface = None
        child = Whatever('%s.%s' % (self._name, name), interface)
        if 'name' in name:
            # Then it's probably displayname, or something.
            return str(child)
        else:
            return child


class DefaultCanonicalUrlData(object):
    implements(ICanonicalUrlData)
    adapts(object)

    def __init__(self, name):
        self.path = '[[%s]]' % (name,)
        self.rootsite = None
        self.inside = None


def make_fake_object(interface):
    return Whatever(interface.__name__, interface)


def get_example_canonical_url(interface):
    fake_obj = make_fake_object(interface)
    try:
        return canonical_url(fake_obj)
    except (KeyError, TypeError, AssertionError, AttributeError):
        # If we get any of the errors that we know about, just return the
        # object. In general, these errors come from app-specific logic that
        # we can't cheat our way around.
        return str(fake_obj)


def main():
    load_zcml()
    gsm = getGlobalSiteManager()
    gsm.registerAdapter(DefaultCanonicalUrlData)
    for adapter in iter_page_adapters():
        print format_page_adapter(adapter)


if __name__ == '__main__':
    main()