~launchpad-pqm/launchpad/devel

10637.3.1 by Guilherme Salgado
Use the default python version instead of a hard-coded version
1
#!/usr/bin/python -S
8687.15.22 by Karl Fogel
Add the copyright header block to the remaining .py files.
2
#
3
# Copyright 2009 Canonical Ltd.  This software is licensed under the
4
# GNU Affero General Public License version 3 (see the file LICENSE).
3485.3.3 by James Henstridge
add zope spec metadata importer
5
6
# A script to import metadata about the Zope 3 specs into Launchpad
7
8
__metaclass__ = type
9
10
import itertools
11
import re
12
import sys
13
import urllib2
14
15
import _pythonpath
16
from zope.component import getUtility
17
from BeautifulSoup import BeautifulSoup
18
11824.1.4 by Tim Penhey
Fix imports for SpecificationGoalStatus.
19
from canonical.launchpad.scripts import execute_zcml_for_scripts
3485.3.3 by James Henstridge
add zope spec metadata importer
20
from canonical.lp import initZopeless
11824.1.4 by Tim Penhey
Fix imports for SpecificationGoalStatus.
21
from lp.blueprints.enums import (
22
    SpecificationStatus,
23
    SpecificationGoalStatus,
24
    SpecificationDelivery,
25
    SpecificationPriority,
26
    )
27
from lp.blueprints.interfaces.specification import ISpecificationSet
28
from lp.registry.interfaces.person import IPersonSet
29
from lp.registry.interfaces.product import IProductSet
30
3485.3.3 by James Henstridge
add zope spec metadata importer
31
3485.3.34 by James Henstridge
update import-zope-specs.py to work with current Launchpad and current Zope wiki
32
WIKI_BASE = 'http://wiki.zope.org/zope3/'
3485.3.3 by James Henstridge
add zope spec metadata importer
33
PROPOSAL_LISTS = ['Zope3Proposals', 'OldProposals', 'DraftProposals']
34
specroot = WIKI_BASE + 'Zope3Proposals'
35
36
at_replacements = ['_at_', '(at)', '@']
37
author_email_pat = re.compile('[-.A-Za-z0-9]+(?:@|%s)[-.A-Za-z0-9]+' %
38
                              '|'.join([re.escape(replacement)
39
                                        for replacement in at_replacements]))
40
41
def getTextContent(tag):
3485.3.28 by James Henstridge
local import-zope-specs.py changes from carbon.ubuntu.com
42
    if tag is None:
43
        return ''
3485.3.3 by James Henstridge
add zope spec metadata importer
44
    if isinstance(tag, basestring):
45
        return tag
46
    return ''.join([e for e in tag.recursiveChildGenerator()
47
                    if isinstance(e, basestring)])
48
49
50
class ZopeSpec:
51
52
    def __init__(self, url, title, summary):
53
        self.url = url
54
        self.name = self.url.split('/')[-1]
55
        self.title = title
56
        self.summary = summary
57
        self.authors = set()
58
        self.statuses = set()
59
60
    def parseAuthorEmails(self, text):
61
        author_email_list = author_email_pat.findall(text)
62
        for author in author_email_list:
63
            # unmangle at symbol in email:
64
            for replacement in at_replacements:
65
                author = author.replace(replacement, '@')
66
            self.authors.add(author)
67
68
    def parseStatuses(self, soup):
69
        wiki_badges = [
70
            'IsWorkInProgress',
71
72
            'IsProposal',
73
            'IsRejectedProposal',
74
            'IsSupercededProposal',
75
            'IsRetractedProposal',
76
            'IsAcceptedProposal',
77
            'IsImplementedProposal',
78
            'IsExpiredProposal',
79
            'IsDraftProposal',
80
81
            'IsPlanned',
82
            'IsResolved',
83
            'IsImplemented',
84
85
            'IsReplaced',
86
            'IsOutdated',
87
            'IsDraft',
88
            'IsEditedDraft',
89
            'IsRoughDraft',
90
            ]
91
        for badge in wiki_badges:
92
            url = WIKI_BASE + badge
93
            if soup.fetch('a', {'href': url}):
94
                self.statuses.add(badge)
95
96
    def parseSpec(self):
97
        contents = urllib2.urlopen(self.url).read()
98
        soup = BeautifulSoup(contents)
3485.3.34 by James Henstridge
update import-zope-specs.py to work with current Launchpad and current Zope wiki
99
        contentdivs = soup('div', {'class': 'content'})
3485.3.3 by James Henstridge
add zope spec metadata importer
100
        assert len(contentdivs) == 1
101
        contentdiv = contentdivs[0]
102
103
        # Specification statuses are represented by "wiki badges",
104
        # which are just hyperlinks to particular pages.
105
        self.parseStatuses(soup)
106
107
        # There are two styles of spec.  One of them has a table with
108
        # RFC-822 style headers in it.  The other has minor level headings
109
        # with text under the heading.
110
        tables = soup('table')
111
        # Every page has one table, for the main page layout.  So, if the page
112
        # has two tables, it means that it will be using the RFC-822 style.
113
        if len(tables) >= 2:
114
            # This is a spec with RFC-822 style headers.
115
            docinfo = tables[1]
116
            for row in docinfo('tr'):
117
                if len(row('th')) < 1 or len(row('td')) < 1:
118
                    continue
119
                key = row('th')[0].renderContents()
120
                if key.endswith(':'):
121
                    key = key[:-1]
122
                value = row('td')[0].renderContents()
123
124
                if 'Author' in key:
125
                    self.parseAuthorEmails(value)
126
        else:
127
            # This is a spec with minor level headings, or perhaps with no
128
            # headings at all.
129
130
            # Look for an author heading.
131
            author_headers = soup(text=re.compile('Author.*', re.I))
132
            if author_headers:
133
                author = author_headers[0].findNext().renderContents()
134
                self.parseAuthorEmails(author)
10303.1.1 by Gary Poster
use newest version of zc.buildout
135
3485.3.3 by James Henstridge
add zope spec metadata importer
136
    @property
137
    def lpname(self):
138
        # add dashes before capitalised words
139
        name = re.sub(r'([^A-Z])([A-Z])', r'\1-\2', self.name)
140
        # lower case name
141
        name = name.lower()
142
        # remove leading dashes
143
        while name.startswith('-'):
144
            name = name[1:]
145
        # if name doesn't begin with an alphabetical character prefix it
146
        if not name[0].isalpha():
147
            name = 'x-' + name
148
        return name
149
150
    @property
151
    def lpstatus(self):
152
        # implemented and accepted specs => APPROVED
153
        for status in ['IsImplemented',
154
                       'IsImplementedProposal',
155
                       'IsAcceptedProposal']:
156
            if status in self.statuses:
157
                return SpecificationStatus.APPROVED
3485.3.28 by James Henstridge
local import-zope-specs.py changes from carbon.ubuntu.com
158
        # WIP => DISCUSSION
3485.3.3 by James Henstridge
add zope spec metadata importer
159
        if 'IsWorkInProgress' in self.statuses:
3485.3.28 by James Henstridge
local import-zope-specs.py changes from carbon.ubuntu.com
160
            return SpecificationStatus.DISCUSSION
3485.3.3 by James Henstridge
add zope spec metadata importer
161
        for status in ['IsSupercededProposal', 'IsReplaced']:
162
            if status in self.statuses:
163
                return SpecificationStatus.SUPERSEDED
164
        for status in ['IsExpiredProposal', 'IsOutdated']:
165
            if status in self.statuses:
166
                return SpecificationStatus.OBSOLETE
167
        # draft statuses:
168
        for status in ['IsDraftProposal',
169
                       'IsDraft',
170
                       'IsEditedDraft',
171
                       'IsRoughDraft']:
172
            if status in self.statuses:
173
                return SpecificationStatus.DRAFT
174
        # otherwise ...
175
        return SpecificationStatus.PENDINGREVIEW
176
177
    @property
178
    def lpgoalstatus(self):
179
        # implemented and accepted specs => ACCEPTED
180
        for status in ['IsImplemented',
181
                       'IsImplementedProposal',
182
                       'IsAcceptedProposal']:
183
            if status in self.statuses:
184
                return SpecificationGoalStatus.ACCEPTED
185
        # rejected or retracted => DECLINED
186
        for status in ['IsRetractedProposal', 'IsRejectedProposal']:
187
            if status in self.statuses:
188
                return SpecificationGoalStatus.DECLINED
189
190
        # otherwise ...
191
        return SpecificationGoalStatus.PROPOSED
192
193
    @property
194
    def lpdelivery(self):
195
        for status in ['IsImplemented',
196
                       'IsImplementedProposal']:
197
            if status in self.statuses:
198
                return SpecificationDelivery.IMPLEMENTED
199
        # otherwise ...
200
        return SpecificationDelivery.UNKNOWN
201
202
    def syncSpec(self):
3485.3.34 by James Henstridge
update import-zope-specs.py to work with current Launchpad and current Zope wiki
203
        zope = getUtility(IProductSet).getByName('zope')
204
        zope_dev = getUtility(IPersonSet).getByName('zope-dev')
3485.3.3 by James Henstridge
add zope spec metadata importer
205
        # has the spec been created?
206
        lpspec = getUtility(ISpecificationSet).getByURL(self.url)
207
        if not lpspec:
208
            lpspec = getUtility(ISpecificationSet).new(
209
                name=self.lpname,
210
                title=self.title,
211
                specurl=self.url,
212
                summary=self.summary,
213
                priority=SpecificationPriority.UNDEFINED,
3485.3.34 by James Henstridge
update import-zope-specs.py to work with current Launchpad and current Zope wiki
214
                status=SpecificationStatus.NEW,
3485.3.3 by James Henstridge
add zope spec metadata importer
215
                owner=zope_dev,
216
                product=zope)
217
218
        # synchronise
219
        lpspec.title = self.title
220
        lpspec.summary = self.summary
221
        lpspec.status = self.lpstatus
3485.3.28 by James Henstridge
local import-zope-specs.py changes from carbon.ubuntu.com
222
        newgoalstatus = self.lpgoalstatus
223
        if newgoalstatus != lpspec.goalstatus:
224
            if newgoalstatus == SpecificationGoalStatus.PROPOSED:
3485.3.34 by James Henstridge
update import-zope-specs.py to work with current Launchpad and current Zope wiki
225
                lpspec.proposeGoal(None, zope_dev)
3485.3.28 by James Henstridge
local import-zope-specs.py changes from carbon.ubuntu.com
226
            elif newgoalstatus == SpecificationGoalStatus.ACCEPTED:
3485.3.34 by James Henstridge
update import-zope-specs.py to work with current Launchpad and current Zope wiki
227
                lpspec.acceptBy(zope_dev)
3485.3.28 by James Henstridge
local import-zope-specs.py changes from carbon.ubuntu.com
228
            elif newgoalstatus == SpecificationGoalStatus.DECLINED:
3485.3.34 by James Henstridge
update import-zope-specs.py to work with current Launchpad and current Zope wiki
229
                lpspec.declineBy(zope_dev)
3485.3.3 by James Henstridge
add zope spec metadata importer
230
        lpspec.delivery = self.lpdelivery
3485.3.34 by James Henstridge
update import-zope-specs.py to work with current Launchpad and current Zope wiki
231
        lpspec.updateLifecycleStatus(zope_dev)
10303.1.1 by Gary Poster
use newest version of zc.buildout
232
3485.3.3 by James Henstridge
add zope spec metadata importer
233
        # set the assignee to the first author email with an LP account
234
        for author in sorted(self.authors):
235
            person = getUtility(IPersonSet).getByEmail(author)
236
            if person is not None:
237
                lpspec.assignee = person
238
                break
239
240
241
def iter_spec_urls(url=specroot):
242
    contents = urllib2.urlopen(url)
243
    soup = BeautifulSoup(contents)
3485.3.34 by James Henstridge
update import-zope-specs.py to work with current Launchpad and current Zope wiki
244
    contentdivs = soup('div', {'class': 'content'})
3485.3.3 by James Henstridge
add zope spec metadata importer
245
    assert len(contentdivs) == 1
246
    contentdiv = contentdivs[0]
247
    listofspecs = contentdiv('ul')[0]
248
249
    for listitem in listofspecs('li', recursive=False):
250
        anchors = listitem('a')
251
        if not anchors:
252
            continue
253
        specanchor = anchors[0]
254
        href = specanchor['href']
255
        # broken wiki link => ignore
3485.3.34 by James Henstridge
update import-zope-specs.py to work with current Launchpad and current Zope wiki
256
        if 'createform?page=' in href:
3485.3.3 by James Henstridge
add zope spec metadata importer
257
            continue
258
        title = getTextContent(specanchor)
259
        summary = ''.join([getTextContent(tag)
260
                               for tag in specanchor.nextSiblingGenerator()])
261
        yield ZopeSpec(href, title, summary.strip())
262
10303.1.1 by Gary Poster
use newest version of zc.buildout
263
3485.3.3 by James Henstridge
add zope spec metadata importer
264
def main(argv):
265
    execute_zcml_for_scripts()
266
    ztm = initZopeless()
267
268
    for spec in itertools.chain(*[iter_spec_urls(WIKI_BASE + page)
269
                                  for page in PROPOSAL_LISTS]):
270
        # parse extra information from the spec body
271
        spec.parseSpec()
272
        # add its metadata to LP
273
        print 'Synchronising', spec.name
274
        ztm.begin()
275
        try:
276
            spec.syncSpec()
277
            ztm.commit()
278
        except:
279
            ztm.abort()
280
            raise
281
282
if __name__ == '__main__':
283
    sys.exit(main(sys.argv))