Merge lp:~bryce/launchpad/lp-617698-forwarding into lp:launchpad/db-devel
- lp-617698-forwarding
- Merge into db-devel
Status: | Rejected |
---|---|
Rejected by: | Curtis Hovey |
Proposed branch: | lp:~bryce/launchpad/lp-617698-forwarding |
Merge into: | lp:launchpad/db-devel |
Prerequisite: | lp:~bryce/launchpad/lp-617695-linkui |
Diff against target: |
1109 lines (+705/-45) 17 files modified
lib/canonical/launchpad/scripts/bzremotecomponentfinder.py (+6/-2) lib/lp/bugs/browser/bugalsoaffects.py (+23/-7) lib/lp/bugs/browser/bugtracker.py (+106/-4) lib/lp/bugs/browser/configure.zcml (+9/-0) lib/lp/bugs/browser/tests/bugtask-adding-views.txt (+1/-1) lib/lp/bugs/browser/tests/test_bugtracker_component.py (+189/-0) lib/lp/bugs/browser/widgets/bugtask.py (+20/-0) lib/lp/bugs/doc/bugtracker.txt (+13/-20) lib/lp/bugs/interfaces/bugtracker.py (+8/-2) lib/lp/bugs/model/bugtracker.py (+29/-9) lib/lp/bugs/templates/bugtracker-edit-component.pt (+16/-0) lib/lp/bugs/templates/bugtracker-index.pt (+2/-0) lib/lp/bugs/templates/bugtracker-portlet-components.pt (+36/-0) lib/lp/registry/configure.zcml (+1/-0) lib/lp/registry/interfaces/sourcepackage.py (+2/-0) lib/lp/registry/model/sourcepackage.py (+11/-0) lib/lp/services/features/scopes.py (+233/-0) |
To merge this branch: | bzr merge lp:~bryce/launchpad/lp-617698-forwarding |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Jeroen T. Vermeulen (community) | code | Approve | |
Bryce Harrington (community) | Needs Resubmitting | ||
Review via email: mp+41003@code.launchpad.net |
Commit message
Implement remote_component support when forwarding bugs.
Description of the change
Implement remote_component support when forwarding bugs.
This branch is dependent on the lp:~bryce/launchpad/lp-617695-linkui so that branch should be landed first.
This causes the 'component' field to be properly filled in for the bugzilla submission page when forwarding a bug report to a remote project.
lint has been checked. ec2 test has been run and passed successfully.
== Testing ==
For manual testing of this feature locally, I first ran cronscripts/
[ Next I turned the feature flag on via the psql statement 'insert into featureflag (scope, priority, flag, value) values ('default', 1, 'bugtracker_
Next, I configured the alsa-utils project as using the gnome bugzilla, at https:/
Navigating to https:/
I logged out, and verified as anonymous that the edit links don't show up on https:/
Finally, after re-logging in, I filed a new bug report against alsa-utils, clicked "Also affects project", clicked the "bug filing form" link, and verified it filled in the component field in the bugzilla form properly.
- 9926. By Bryce Harrington
-
Review comments by Jeroen.
- 9927. By Bryce Harrington
-
Review by Jerone: Code can be simplified using the ubuntu celebrity
- 9928. By Bryce Harrington
-
Almost all of the method is now an "else" clause after an early
return; we can drop the else and make the code stand on its own. - 9929. By Bryce Harrington
-
Review by Jerone: Make if statement easier to read
- 9930. By Bryce Harrington
-
Review by Jeroen: Split out lookup functionality to separate routine
- 9931. By Bryce Harrington
-
Review by Jeroen: Find way to maintain visual separation between the two
if conditions and the body - 9932. By Bryce Harrington
-
Review by Jeroen: Note interface change in docstring
- 9933. By Bryce Harrington
-
Review by Jeroen: Paragraph is too detailed for a doctest and mostly is
just stating obvious stuff about a function's parameters that doesn't
belong here. - 9934. By Bryce Harrington
-
Review by Jeroen: Simplify setup by using helper functions.
- 9935. By Bryce Harrington
-
Review by Jeroen: Split up linking test into two tests
- 9936. By Bryce Harrington
-
Review by Jeroen: Use dict to eliminate some if clauses
- 9937. By Bryce Harrington
-
Review by Jeroen: Put a space in "% ("
- 9938. By Bryce Harrington
-
Review by Jeroen: Construct full name in a variable to make easier to follow
- 9939. By Bryce Harrington
-
Review by Jeroen: Use more verbose but conventional way of calling find.
- 9940. By Bryce Harrington
-
Re-merge db-devel trunk
- 9941. By Bryce Harrington
-
Review by Jeroen: State how the view should behave in this situation.
- 9942. By Bryce Harrington
-
Unnecessary to re-test output from the first component since this was
tested previously and is just setup here. - 9943. By Bryce Harrington
-
Review by Jeroen: It doesn't matter how the first component got linked
so just treat it as setup and link the package directly, so that this
test focuses just on testing one thing. - 9944. By Bryce Harrington
-
Review by Jeroen: You could use
self.assertTextMatchesExpressi onIgnoreWhitesp ace for the
verification. - 9945. By Bryce Harrington
-
Avoid potential extra database call when there are no components
- 9946. By Bryce Harrington
-
Review by Jeroen: Use URL formatter for links
- 9947. By Bryce Harrington
-
Review by Jeroen: Only display editing links if there is a logged in user
- 9948. By Bryce Harrington
-
Omit the nonbreaking space for anonymous users
- 9949. By Bryce Harrington
-
Preliminary implementation of test for checking anonymous edits
- 9950. By Bryce Harrington
-
Re-merge db-devel trunk
- 9951. By Bryce Harrington
-
Fix typo in find conditional
- 9952. By Bryce Harrington
-
ILaunchpadCeleb
rities needs declared - 9953. By Bryce Harrington
-
Fix up several test errors
Jeroen T. Vermeulen (jtv) wrote : | # |
Hi Bryce,
I see that you've done a lot of work on this branch since my initial review. Looks a lot better (though watch that double blank line below test_no_
Please let me know when it's ready for the next round and hopefully we can get it landed!
- 9954. By Bryce Harrington
-
Use a store call to delete components.
- 9955. By Bryce Harrington
-
Enable testing generated links
Featureflags and testing view/user are interfering, so disable them
temporarily. - 9956. By Bryce Harrington
-
Adjust create_
initialized_ view() call to set up proper logged in user - 9957. By Bryce Harrington
-
Further experimentation with feature flags.
Just can't sort out how to make it work in a unittest. :-/
- 9958. By Bryce Harrington
-
Re-merge trunk
- 9959. By Bryce Harrington
-
Giving up on feature flags stuff and ripping it out.
The flags work fine in the browser when setting the flag manually, but
I just can't sort out how to get the unittests to recognize it. This
issue has been blocking progress on this branch for months now, and I've
failed to find anyone who can explain how to make it work properly.
Martin Pool suggested just dropping it and moving on, which sounds good
to me. - 9960. By Bryce Harrington
-
Re-merge db-devel trunk
- 9961. By Bryce Harrington
-
Adjust to new widgets location
Bryce Harrington (bryce) wrote : | # |
Hi Jeroen, yeah the extensiveness of the review made for a rather Herculean labor...
I'm afraid since returning from launchpad X.org has monopolized my attention, however I think this branch is more or less done and ready to go, but I haven't looked at it in a while; I'll check it over again.
- 9962. By Bryce Harrington
-
Compare string against the '.name' field of DSP rather than object itself.
- 9963. By Bryce Harrington
-
Revert Jeroen's change for component form so editing components works.
In review, Jeroen noticed I had an ugly if clause to look up the source
package name field for the component edit form, and suggested replacing
it with a dict to look up the fields.Unfortunately, it's not so simple. The SourcePackageName object is not
a string property, so has to be handled differently than other kinds of
objects (we have to explicitly reference it's .name field to get the
string out of it). So the None check and special handling of this field
is required. Otherwise, an Oops occurs when it tries to fill in the
package name field with the object rather than the string. - 9964. By Bryce Harrington
-
Re-merge db-devel trunk
Bryce Harrington (bryce) wrote : | # |
Alright, I exercised the code for a while, found a couple issues that were introduced due to changes from the review and got them fixed up.
I also re-merged db-devel and fixed a minor conflict; unfortunately it seems launchpad has moved from postgres 8.3 to 8.4, and the upgrade scripts seem to have borked up my system, so I can no longer make schema or run tests locally. Hopefully it's correct though.
I don't know when I'll next get some spare moments to get back to launchpad work, so if there are any remaining minor niggles like whitespace or formatting or UI tweaks or whatnot, if you don't mind please feel free to correct those. Also, if someone else could handle testing and landing this baby I would *really* appreciate getting it off my plate.
Jeroen T. Vermeulen (jtv) wrote : | # |
Bryce,
Thanks for sticking with it! I'm re-reviewing it. Meanwhile, to get your Launchpad instance working again:
* Make sure your postgres config has 8.4 running on port 5432. If you install it side by side with an existing 8.3, it'll try to evade to port 5433 instead. Get rid of the 8.3.
* There's a script called launchpad-
Jeroen
Jeroen T. Vermeulen (jtv) wrote : | # |
= Approving =
This looks like Pretty Good Stuff now. The commit logs show lots of changes (and actually explain them, which is a nice touch).
I see just 2 things that need fixing, but I'm sure that won't be helped by any further nannying from me. See "Important" below. Some other notes that you may want to consider or improve at your option: nice to have but not worth further delay.
== Design ==
I see a few documented assumptions (very thoughtful, by the way!) about Ubuntu being involved. Does that fit with the ongoing work for supporting derived distributions? Probably depends on the requirements for derived distros.
If not, it may present some new development tasks for derived distros. I wouldn't hold up your branch for it unless it poses some kind of serious problem.
== Important ==
=== Jack-in-the-box file ===
According to the MP diff, you're _adding_ lib/lp/
=== Model/view boundaries ===
In lib/lp/
188 + def updateContextFr
[...]
208 + # Has this source package already been assigned to a component?
209 + pkgs = Store.of(
210 + BugTrackerCompo
211 + BugTrackerCompo
212 + BugTrackerCompo
213 + pkg.sourcepacka
214 + if pkgs.count() > 0:
215 + self.request.
216 + "The %s source package is already linked to %s in %s" % (
217 + sourcepackagename, component_
218 + return
Querying the database from browser code is frowned upon: it's a job for the model, not for the view.
This particular query does a job that can be concisely defined, so that's good. (In fact some say that a block of code like this with a defining comment on top should almost certainly be a method of its own). I didn't find any method that does exactly this, but there's no reason why there shouldn't be. I think SourcePackage would be a good place.
(Also note that a count() on the query can be relatively expensive and deliver more information than you need. If all you need to know is whether there are any at all, use is_empty() instead.)
== Style ==
In lib/lp/
62 + # Look up the remote component if we have the necessary info
63 + #if data.get(
64 + comp_group = target.
65 + target.
There's still a commented-out line in there.
In lib/lp/
Also in lib/lp/
353 + expected_html = """
354 + <h2>Components</h2>
355 + <ul>
356 + <li>
357 + <strong>
358 + <ul>
359 + <li>
360 + ...
Bryce Harrington (bryce) wrote : | # |
"Finally, a question: in lib/lp/
==> product-
<product-cve-report
==> remotebug-index.pt <==
<bug-tracker-
==> malone-index.pt <==
<malone-index
==> distribution-
<distribution-
==> distribution-
<distribution-
==> distroseries-
<distroseries-
Seems there are a number of other instances where this is done... I was cribbing off the cve templates since they seemed close to what I was trying to do. If this style is wrong it looks like it'll need fixed in a number of places.
- 9965. By Bryce Harrington
-
Review from jeroen: "Also note that a count() on the query can be
relatively expensive and deliver more information than you need. If all
you need to know is whether there are any at all, use is_empty() instead" - 9966. By Bryce Harrington
-
Review from jeroen: "Commented-out code is a bad thing."
- 9967. By Bryce Harrington
-
Review from jeroen: Typo for "searched"?
- 9968. By Bryce Harrington
-
Review from jeroen: Move component/
sourcepackage check to a new
member function of SourcePackage.
Bryce Harrington (bryce) wrote : | # |
"According to the MP diff, you're _adding_ lib/lp/
According to my local tree, this only came in as part of a re-merge of db-devel and is not created locally. I certainly never added it myself. I have no idea why it's showing up in the merge proposal diff, nor any clue on how to correct that.
Jeroen T. Vermeulen (jtv) wrote : | # |
To be honest, I get these things as well sometimes and don't know how to explain them. Often I'll just produce a diff, clean it up, and patch it into a new branch.
Robert Collins (lifeless) wrote : | # |
File a bug about the odd diff?
Bryce Harrington (bryce) wrote : | # |
I think there may have been some overlap in the review comments for this MP and the MP for the branch this depends on, https:/
Curtis Hovey (sinzui) wrote : | # |
I marked this old review rejected because I think I see code that has already landed in devel. If I am wrong, please submit a new merge proposal with devel.
Unmerged revisions
- 9968. By Bryce Harrington
-
Review from jeroen: Move component/
sourcepackage check to a new
member function of SourcePackage. - 9967. By Bryce Harrington
-
Review from jeroen: Typo for "searched"?
- 9966. By Bryce Harrington
-
Review from jeroen: "Commented-out code is a bad thing."
- 9965. By Bryce Harrington
-
Review from jeroen: "Also note that a count() on the query can be
relatively expensive and deliver more information than you need. If all
you need to know is whether there are any at all, use is_empty() instead" - 9964. By Bryce Harrington
-
Re-merge db-devel trunk
- 9963. By Bryce Harrington
-
Revert Jeroen's change for component form so editing components works.
In review, Jeroen noticed I had an ugly if clause to look up the source
package name field for the component edit form, and suggested replacing
it with a dict to look up the fields.Unfortunately, it's not so simple. The SourcePackageName object is not
a string property, so has to be handled differently than other kinds of
objects (we have to explicitly reference it's .name field to get the
string out of it). So the None check and special handling of this field
is required. Otherwise, an Oops occurs when it tries to fill in the
package name field with the object rather than the string. - 9962. By Bryce Harrington
-
Compare string against the '.name' field of DSP rather than object itself.
- 9961. By Bryce Harrington
-
Adjust to new widgets location
- 9960. By Bryce Harrington
-
Re-merge db-devel trunk
- 9959. By Bryce Harrington
-
Giving up on feature flags stuff and ripping it out.
The flags work fine in the browser when setting the flag manually, but
I just can't sort out how to get the unittests to recognize it. This
issue has been blocking progress on this branch for months now, and I've
failed to find anyone who can explain how to make it work properly.
Martin Pool suggested just dropping it and moving on, which sounds good
to me.
Preview Diff
1 | === modified file 'lib/canonical/launchpad/scripts/bzremotecomponentfinder.py' |
2 | --- lib/canonical/launchpad/scripts/bzremotecomponentfinder.py 2010-12-20 03:21:03 +0000 |
3 | +++ lib/canonical/launchpad/scripts/bzremotecomponentfinder.py 2011-05-19 01:14:21 +0000 |
4 | @@ -117,7 +117,7 @@ |
5 | self.static_bugzilla_text = static_bugzilla_text |
6 | |
7 | def getRemoteProductsAndComponents(self, bugtracker_name=None): |
8 | - """""" |
9 | + """Retrieves, parses, and stores component data for each bugtracker""" |
10 | lp_bugtrackers = getUtility(IBugTrackerSet) |
11 | if bugtracker_name is not None: |
12 | lp_bugtrackers = [ |
13 | @@ -182,7 +182,11 @@ |
14 | else: |
15 | # Component is now missing from Bugzilla, |
16 | # so drop it here too |
17 | - component.remove() |
18 | + store = IStore(BugTrackerComponent) |
19 | + store.find( |
20 | + BugTrackerComponent, |
21 | + BugTrackerComponent.id == component.id, |
22 | + ).remove() |
23 | |
24 | # The remaining components in the collection will need to be |
25 | # added to launchpad. Record them for now. |
26 | |
27 | === modified file 'lib/lp/bugs/browser/bugalsoaffects.py' |
28 | --- lib/lp/bugs/browser/bugalsoaffects.py 2011-03-23 16:28:51 +0000 |
29 | +++ lib/lp/bugs/browser/bugalsoaffects.py 2011-05-19 01:14:21 +0000 |
30 | @@ -646,6 +646,14 @@ |
31 | packaging=PackagingType.PRIME, owner=self.user) |
32 | return super(ProductBugTaskCreationStep, self).main_action(data) |
33 | |
34 | + def _lookup_remote_component_name(self, comp_group, package_name): |
35 | + if comp_group is None: |
36 | + return "" |
37 | + for component in comp_group.components: |
38 | + package = component.distro_source_package |
39 | + if (package is not None and package.name == package_name.name): |
40 | + return component.name |
41 | + |
42 | @property |
43 | def upstream_bugtracker_links(self): |
44 | """Return the upstream bugtracker links for the current target. |
45 | @@ -658,13 +666,21 @@ |
46 | |
47 | if not target.bugtracker: |
48 | return None |
49 | - else: |
50 | - bug = self.context.bug |
51 | - title = bug.title |
52 | - description = u"Originally reported at:\n %s\n\n%s" % ( |
53 | - canonical_url(bug), bug.description) |
54 | - return target.bugtracker.getBugFilingAndSearchLinks( |
55 | - target.remote_product, title, description) |
56 | + |
57 | + bug = self.context.bug |
58 | + title = bug.title |
59 | + description = u"Originally reported at:\n %s\n\n%s" % ( |
60 | + canonical_url(bug), bug.description) |
61 | + |
62 | + # Look up the remote component if we have the necessary info |
63 | + comp_group = target.bugtracker.getRemoteComponentGroup( |
64 | + target.remote_product) |
65 | + package_name = self.context.target.sourcepackagename |
66 | + remote_component = self._lookup_remote_component_name( |
67 | + comp_group, package_name) |
68 | + |
69 | + return target.bugtracker.getBugFilingAndSearchLinks( |
70 | + target.remote_product, remote_component, title, description) |
71 | |
72 | |
73 | class BugTrackerCreationStep(AlsoAffectsStep): |
74 | |
75 | === modified file 'lib/lp/bugs/browser/bugtracker.py' |
76 | --- lib/lp/bugs/browser/bugtracker.py 2011-02-02 15:43:31 +0000 |
77 | +++ lib/lp/bugs/browser/bugtracker.py 2011-05-19 01:14:21 +0000 |
78 | @@ -10,6 +10,7 @@ |
79 | 'BugTrackerBreadcrumb', |
80 | 'BugTrackerComponentGroupNavigation', |
81 | 'BugTrackerEditView', |
82 | + 'BugTrackerEditComponentView', |
83 | 'BugTrackerNavigation', |
84 | 'BugTrackerNavigationMenu', |
85 | 'BugTrackerSetBreadcrumb', |
86 | @@ -21,6 +22,7 @@ |
87 | ] |
88 | |
89 | from itertools import chain |
90 | +from storm.locals import Store |
91 | |
92 | from zope.app.form.browser import TextAreaWidget |
93 | from zope.component import getUtility |
94 | @@ -59,6 +61,7 @@ |
95 | from canonical.launchpad.webapp.breadcrumb import Breadcrumb |
96 | from canonical.launchpad.webapp.menu import NavigationMenu |
97 | from canonical.lazr.utils import smartquote |
98 | +from lp.bugs.browser.widgets.bugtask import UbuntuSourcePackageNameWidget |
99 | from lp.app.browser.launchpadform import ( |
100 | action, |
101 | custom_widget, |
102 | @@ -73,7 +76,11 @@ |
103 | IBugTrackerComponentGroup, |
104 | IBugTrackerSet, |
105 | IRemoteBug, |
106 | + IBugTrackerComponent, |
107 | + IBugTrackerComponentGroup, |
108 | ) |
109 | +from lp.bugs.model.bugtracker import BugTrackerComponent |
110 | +from lp.registry.interfaces.distribution import IDistributionSet |
111 | from lp.services.propertycache import cachedproperty |
112 | |
113 | # A set of bug tracker types for which there can only ever be one bug |
114 | @@ -229,6 +236,14 @@ |
115 | return shortlist(chain(self.context.projects, |
116 | self.context.products), 100) |
117 | |
118 | + @property |
119 | + def related_component_groups(self): |
120 | + """Return all component groups and components. |
121 | + |
122 | + This property was created for the Related components portlet in |
123 | + the bug tracker's page. |
124 | + """ |
125 | + return self.context.getAllRemoteComponentGroups() |
126 | |
127 | BUG_TRACKER_ACTIVE_VOCABULARY = SimpleVocabulary.fromItems( |
128 | [('On', True), ('Off', False)]) |
129 | @@ -447,16 +462,103 @@ |
130 | return RemoteBug(self.context, remotebug, bugs) |
131 | |
132 | @stepthrough("+components") |
133 | - def component_groups(self, name): |
134 | - return self.context.getRemoteComponentGroup(name) |
135 | + def component_groups(self, name_or_id): |
136 | + """Navigate by id (component group name should work too)""" |
137 | + return self.context.getRemoteComponentGroup(name_or_id) |
138 | + |
139 | + |
140 | +class BugTrackerEditComponentView(LaunchpadEditFormView): |
141 | + """Provides editing form for setting source packages for components. |
142 | + |
143 | + This class assumes that bug tracker components are always |
144 | + linked to source packages in the Ubuntu distribution. |
145 | + """ |
146 | + |
147 | + schema = IBugTrackerComponent |
148 | + custom_widget('sourcepackagename', UbuntuSourcePackageNameWidget) |
149 | + |
150 | + @property |
151 | + def page_title(self): |
152 | + return smartquote( |
153 | + u'Link a distribution source package to the %s component' |
154 | + % self.context.name) |
155 | + |
156 | + @property |
157 | + def field_names(self): |
158 | + field_names = ['sourcepackagename'] |
159 | + return field_names |
160 | + |
161 | + def setUpWidgets(self, context=None): |
162 | + for field in self.form_fields: |
163 | + if (field.custom_widget is None): |
164 | + if (field.__name__ in self.custom_widgets): |
165 | + field.custom_widget = self.custom_widgets[field.__name__] |
166 | + self.widgets = form.setUpWidgets( |
167 | + self.form_fields, self.prefix, self.context, self.request, |
168 | + data=self.initial_values, adapters=self.adapters, |
169 | + ignore_request=False) |
170 | + |
171 | + @property |
172 | + def initial_values(self): |
173 | + """See `LaunchpadFormView.`""" |
174 | + field_values = {} |
175 | + for name in self.field_names: |
176 | + if name == 'sourcepackagename': |
177 | + pkg = self.context.distro_source_package |
178 | + if pkg is not None: |
179 | + field_values['sourcepackagename'] = pkg.name |
180 | + else: |
181 | + field_values['sourcepackagename'] = "" |
182 | + else: |
183 | + field_values[name] = getattr(self.context, name) |
184 | + |
185 | + return field_values |
186 | + |
187 | + def updateContextFromData(self, data, context=None): |
188 | + """Link component to specified distro source package. |
189 | + |
190 | + Get the user-provided source package name from the form widget, |
191 | + look it up in Ubuntu to retrieve the distro_source_package |
192 | + object, and link it to this component. |
193 | + """ |
194 | + if context is None: |
195 | + context = self.context |
196 | + component = context |
197 | + |
198 | + sourcepackagename = self.request.form.get( |
199 | + self.widgets['sourcepackagename'].name) |
200 | + |
201 | + component_full_name = "%s:%s" % ( |
202 | + component.component_group.name, component.name) |
203 | + distribution = self.widgets['sourcepackagename'].getDistribution() |
204 | + distro_name = distribution.name |
205 | + pkg = distribution.getSourcePackage(sourcepackagename) |
206 | + if pkg.has_component: |
207 | + self.request.response.addNotification( |
208 | + "The %s source package is already linked to a component in %s" % ( |
209 | + sourcepackagename, distro_name)) |
210 | + return |
211 | + |
212 | + component.distro_source_package = pkg |
213 | + self.request.response.addNotification( |
214 | + "%s is now linked to the %s source package in %s" % ( |
215 | + component_full_name, sourcepackagename, distro_name)) |
216 | + |
217 | + @action('Save Changes', name='save') |
218 | + def save_action(self, action, data): |
219 | + """Update the component with the form data.""" |
220 | + self.updateContextFromData(data) |
221 | + |
222 | + self.next_url = canonical_url( |
223 | + self.context.component_group.bug_tracker) |
224 | |
225 | |
226 | class BugTrackerComponentGroupNavigation(Navigation): |
227 | |
228 | usedfor = IBugTrackerComponentGroup |
229 | |
230 | - def traverse(self, name): |
231 | - return self.context.getComponent(name) |
232 | + def traverse(self, id): |
233 | + return self.context.getComponent(id) |
234 | |
235 | |
236 | class BugTrackerSetBreadcrumb(Breadcrumb): |
237 | |
238 | === modified file 'lib/lp/bugs/browser/configure.zcml' |
239 | --- lib/lp/bugs/browser/configure.zcml 2011-04-28 12:51:56 +0000 |
240 | +++ lib/lp/bugs/browser/configure.zcml 2011-05-19 01:14:21 +0000 |
241 | @@ -813,6 +813,12 @@ |
242 | path_expression="name" |
243 | attribute_to_parent="component_group" |
244 | rootsite="bugs"/> |
245 | + <browser:page |
246 | + name="+edit" |
247 | + for="lp.bugs.interfaces.bugtracker.IBugTrackerComponent" |
248 | + class="lp.bugs.browser.bugtracker.BugTrackerEditComponentView" |
249 | + permission="launchpad.AnyPerson" |
250 | + template="../templates/bugtracker-edit-component.pt"/> |
251 | <browser:pages |
252 | for="lp.bugs.interfaces.bugtracker.IBugTracker" |
253 | class="lp.bugs.browser.bugtracker.BugTrackerView" |
254 | @@ -824,6 +830,9 @@ |
255 | name="+portlet-details" |
256 | template="../templates/bugtracker-portlet-details.pt"/> |
257 | <browser:page |
258 | + name="+portlet-components" |
259 | + template="../templates/bugtracker-portlet-components.pt"/> |
260 | + <browser:page |
261 | name="+portlet-projects" |
262 | template="../templates/bugtracker-portlet-projects.pt"/> |
263 | <browser:page |
264 | |
265 | === modified file 'lib/lp/bugs/browser/tests/bugtask-adding-views.txt' |
266 | --- lib/lp/bugs/browser/tests/bugtask-adding-views.txt 2011-03-28 20:54:50 +0000 |
267 | +++ lib/lp/bugs/browser/tests/bugtask-adding-views.txt 2011-05-19 01:14:21 +0000 |
268 | @@ -618,7 +618,7 @@ |
269 | |
270 | >>> print_links(add_task_view.upstream_bugtracker_links) |
271 | bug_filing_url: |
272 | - ...?product=foo&short_desc=Reflow%20...&long_desc=Originally%20... |
273 | + ...?product=foo...&short_desc=Reflow%20...&long_desc=Originally%20... |
274 | bug_search_url: ...query.cgi?product=foo&short_desc=Reflow%20problems... |
275 | |
276 | If the product's `bugtracker` isn't specified its |
277 | |
278 | === added file 'lib/lp/bugs/browser/tests/test_bugtracker_component.py' |
279 | --- lib/lp/bugs/browser/tests/test_bugtracker_component.py 1970-01-01 00:00:00 +0000 |
280 | +++ lib/lp/bugs/browser/tests/test_bugtracker_component.py 2011-05-19 01:14:21 +0000 |
281 | @@ -0,0 +1,189 @@ |
282 | +# Copyright 2010 Canonical Ltd. This software is licensed under the |
283 | +# GNU Affero General Public License version (see the file LICENSE). |
284 | + |
285 | +"""Unit tests for linking bug tracker components to source packages.""" |
286 | + |
287 | +__metaclass__ = type |
288 | + |
289 | +import unittest |
290 | +from zope.component import getUtility |
291 | + |
292 | +from canonical.launchpad.testing.pages import find_tag_by_id |
293 | +from canonical.testing.layers import DatabaseFunctionalLayer |
294 | +from lp.registry.interfaces.distribution import IDistributionSet |
295 | +from lp.testing import ( |
296 | + ANONYMOUS, |
297 | + login, |
298 | + login_person, |
299 | + person_logged_in, |
300 | + TestCaseWithFactory, |
301 | + ) |
302 | +from lp.testing.views import create_initialized_view |
303 | +from storm.store import Store |
304 | + |
305 | + |
306 | +class TestBugTrackerEditComponentView(TestCaseWithFactory): |
307 | + |
308 | + layer = DatabaseFunctionalLayer |
309 | + |
310 | + def setUp(self): |
311 | + super(TestBugTrackerEditComponentView, self).setUp() |
312 | + self.regular_user = self.factory.makePerson() |
313 | + login_person(self.regular_user) |
314 | + |
315 | + self.bug_tracker = self.factory.makeBugTracker( |
316 | + title=u'Example Bug Tracker', |
317 | + name=u'example-bug-tracker' |
318 | + ) |
319 | + self.comp_group = self.factory.makeBugTrackerComponentGroup( |
320 | + u'alpha', self.bug_tracker) |
321 | + |
322 | + def _makeForm(self, sourcepackage): |
323 | + if sourcepackage is None: |
324 | + name = '' |
325 | + else: |
326 | + name = sourcepackage.name |
327 | + return { |
328 | + 'field.sourcepackagename': name, |
329 | + 'field.actions.save': 'Save', |
330 | + } |
331 | + |
332 | + def _makeComponent(self, name): |
333 | + return self.factory.makeBugTrackerComponent(name, self.comp_group) |
334 | + |
335 | + def _makeUbuntuSourcePackage(self, package_name): |
336 | + distro = getUtility(IDistributionSet).getByName('ubuntu') |
337 | + return self.factory.makeDistributionSourcePackage( |
338 | + sourcepackagename=package_name, distribution=distro) |
339 | + |
340 | + def test_no_anonymous_edits(self): |
341 | + '''Only logged in users should see the edit buttons''' |
342 | + login(ANONYMOUS) |
343 | + component = self._makeComponent(u'Example') |
344 | + |
345 | + expected_html = """ |
346 | + <h2>Components</h2> |
347 | + <ul> |
348 | + <li> |
349 | + <strong>alpha</strong> |
350 | + <ul> |
351 | + <li> |
352 | + Example |
353 | + </li> |
354 | + </ul> |
355 | + </li> |
356 | +""" |
357 | + |
358 | + view = create_initialized_view( |
359 | + self.bug_tracker, name='+index', rootsite='bugs', |
360 | + current_request=True) |
361 | + actual_html = view.render() |
362 | + self.assertIsNot(None, find_tag_by_id(actual_html, 'portlet-components')) |
363 | + self.assertTextMatchesExpressionIgnoreWhitespace( |
364 | + expected_html, actual_html) |
365 | + |
366 | + def test_generated_links(self): |
367 | + """Validates hyperlinks generated in the TAL""" |
368 | + component = self._makeComponent(u'Example') |
369 | + package = self._makeUbuntuSourcePackage('example') |
370 | + |
371 | + projectgroup = self.factory.makeProject(name=u'sample-project') |
372 | + product = self.factory.makeProduct() |
373 | + with person_logged_in(product.owner): |
374 | + product.project = projectgroup |
375 | + product.bugtracker = self.bug_tracker |
376 | + product.remote_product = u'sample-product' |
377 | + |
378 | + expected_html = """<strong>alpha</strong> |
379 | + .*Example.* |
380 | + .*href=".*/bugs/bugtrackers/example-bug-tracker/.components/alpha/Example/.edit"></a>""" |
381 | + |
382 | + login_person(self.regular_user) |
383 | + view = create_initialized_view( |
384 | + self.bug_tracker, name='+index', principal=self.regular_user) |
385 | + actual_html = view.render() |
386 | + |
387 | + self.assertTextMatchesExpressionIgnoreWhitespace( |
388 | + expected_html, actual_html) |
389 | + |
390 | + def test_view_attributes(self): |
391 | + component = self._makeComponent(u'Example') |
392 | + package = self._makeUbuntuSourcePackage('example-package') |
393 | + form = self._makeForm(package) |
394 | + view = create_initialized_view( |
395 | + component, name='+edit', form=form) |
396 | + label = 'Link a distribution source package to the Example component' |
397 | + self.assertEqual(label, view.page_title) |
398 | + fields = ['sourcepackagename'] |
399 | + self.assertEqual(fields, view.field_names) |
400 | + |
401 | + def test_linking(self): |
402 | + component = self._makeComponent(u'Example') |
403 | + package = self._makeUbuntuSourcePackage('example-package') |
404 | + form = self._makeForm(package) |
405 | + |
406 | + self.assertIs(None, component.distro_source_package) |
407 | + view = create_initialized_view( |
408 | + component, name='+edit', form=form) |
409 | + notifications = view.request.response.notifications |
410 | + self.assertEqual(package, component.distro_source_package) |
411 | + |
412 | + def test_linking_html(self): |
413 | + package = self._makeUbuntuSourcePackage('example-package') |
414 | + component = self._makeComponent(u'Example') |
415 | + component.distro_source_package = package |
416 | + |
417 | + expected_html = """<strong>alpha</strong> |
418 | + .*Example.* |
419 | + href=".*example-bug-tracker/.components/alpha/Example/.edit"> |
420 | + example-package</a>""" |
421 | + login_person(self.regular_user) |
422 | + view = create_initialized_view( |
423 | + self.bug_tracker, name='+index', principal=self.regular_user) |
424 | + actual_html = view.render() |
425 | + |
426 | + self.assertIsNot(None, find_tag_by_id(actual_html, 'portlet-components')) |
427 | + self.assertTextMatchesExpressionIgnoreWhitespace( |
428 | + expected_html, actual_html) |
429 | + |
430 | + def test_linking_notifications(self): |
431 | + component = self._makeComponent(u'Example') |
432 | + package = self._makeUbuntuSourcePackage('example-package') |
433 | + form = self._makeForm(package) |
434 | + |
435 | + view = create_initialized_view( |
436 | + component, name='+edit', form=form) |
437 | + self.assertEqual([], view.errors) |
438 | + notifications = view.request.response.notifications |
439 | + expected = """ |
440 | + alpha:Example is now linked to the example-package |
441 | + source package in ubuntu""" |
442 | + self.assertTextMatchesExpressionIgnoreWhitespace( |
443 | + expected, notifications.pop().message) |
444 | + |
445 | + def test_cannot_doublelink_sourcepackages(self): |
446 | + ''' Two components try linking to same same package |
447 | + |
448 | + We must maintain a one-to-one relationship between components |
449 | + and source packages. However, users are bound to attempt to try |
450 | + to make multiple components linked to the same source package, |
451 | + so the view needs to be sure to not allow this to be done and |
452 | + pop up a friendly error message instead. |
453 | + ''' |
454 | + component_a = self._makeComponent(u'a') |
455 | + component_b = self._makeComponent(u'b') |
456 | + package = self._makeUbuntuSourcePackage('example-package') |
457 | + component_a.distro_source_package = package |
458 | + |
459 | + form = self._makeForm(package) |
460 | + view = create_initialized_view( |
461 | + component_b, name='+edit', form=form) |
462 | + self.assertIs(None, component_b.distro_source_package) |
463 | + self.assertEqual([], view.errors) |
464 | + notifications = view.request.response.notifications |
465 | + self.assertEqual(1, len(notifications)) |
466 | + expected = """ |
467 | + The example-package source package is already linked to |
468 | + alpha:b in ubuntu""" |
469 | + self.assertTextMatchesExpressionIgnoreWhitespace( |
470 | + expected, notifications.pop().message) |
471 | |
472 | === modified file 'lib/lp/bugs/browser/widgets/bugtask.py' |
473 | --- lib/lp/bugs/browser/widgets/bugtask.py 2011-02-02 15:43:31 +0000 |
474 | +++ lib/lp/bugs/browser/widgets/bugtask.py 2011-05-19 01:14:21 +0000 |
475 | @@ -14,6 +14,7 @@ |
476 | "DBItemDisplayWidget", |
477 | "NewLineToSpacesWidget", |
478 | "NominationReviewActionWidget", |
479 | + "UbuntuSourcePackageNameWidget" |
480 | ] |
481 | |
482 | from xml.sax.saxutils import escape |
483 | @@ -47,6 +48,7 @@ |
484 | ) |
485 | |
486 | from canonical.launchpad import _ |
487 | +from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities |
488 | from canonical.launchpad.webapp import canonical_url |
489 | from canonical.launchpad.webapp.interfaces import ILaunchBag |
490 | from lp.app.browser.tales import TeamFormatterAPI |
491 | @@ -527,6 +529,24 @@ |
492 | return distribution |
493 | |
494 | |
495 | +class UbuntuSourcePackageNameWidget(BugTaskSourcePackageNameWidget): |
496 | + """Package widget where the distribution can be assumed as Ubuntu. |
497 | + |
498 | + This widgets works the same as `BugTaskSourcePackageNameWidget`, |
499 | + except that it assumes the distribution is 'ubuntu'. |
500 | + """ |
501 | + distribution_name = "ubuntu" |
502 | + |
503 | + def getDistribution(self): |
504 | + """See `BugTaskSourcePackageNameWidget` |
505 | + |
506 | + _toFieldValue() in BugTaskSourcePackageNameWidget needs this |
507 | + routine to determine the distribution. We just hardcode it |
508 | + to Ubuntu. |
509 | + """ |
510 | + return getUtility(ILaunchpadCelebrities).ubuntu |
511 | + |
512 | + |
513 | class AssigneeDisplayWidget(BrowserWidget): |
514 | """A widget for displaying an assignee.""" |
515 | |
516 | |
517 | === modified file 'lib/lp/bugs/doc/bugtracker.txt' |
518 | --- lib/lp/bugs/doc/bugtracker.txt 2011-03-23 16:28:51 +0000 |
519 | +++ lib/lp/bugs/doc/bugtracker.txt 2011-05-19 01:14:21 +0000 |
520 | @@ -324,26 +324,20 @@ |
521 | Filing a bug on the remote tracker |
522 | ---------------------------------- |
523 | |
524 | -The IBugTracker interface defines a method, getBugFilingAndSearchLinks(), |
525 | -which returns the URLs of the bug filing form and the bug search on |
526 | -the remote bug tracker as a dict. It accepts three parameters: |
527 | -remote_product, which is the name of the product on the remote |
528 | -tracker; summary, which is the bug summary to be passed to the remote |
529 | -bug tracker for searching or filing and description, which is the full |
530 | -description of the bug. This is only passed to the bug filing form as |
531 | -it is too specific for the search form. |
532 | +The IBugTracker interface defines a method to convert product, |
533 | +component, summary, and description strings into URLs for filing and/or |
534 | +searching bugs. |
535 | |
536 | >>> def print_links(links_dict): |
537 | ... for key in sorted(links_dict): |
538 | ... print "%s: %s" % (key, links_dict[key]) |
539 | |
540 | >>> links = mozilla_bugzilla.getBugFilingAndSearchLinks( |
541 | - ... remote_product='testproduct', summary="Foo", description="Bar") |
542 | + ... remote_product='testproduct', remote_component='testcomponent', |
543 | + ... summary="Foo", description="Bar") |
544 | >>> print_links(links) |
545 | - bug_filing_url: |
546 | - https://.../enter_bug.cgi?product=testproduct&short_desc=Foo&long_desc=Bar |
547 | - bug_search_url: |
548 | - https://.../query.cgi?product=testproduct&short_desc=Foo |
549 | + bug_filing_url: https://.../enter_bug.cgi?product=testproduct&component=testcomponent&short_desc=Foo&long_desc=Bar |
550 | + bug_search_url: https://.../query.cgi?product=testproduct&short_desc=Foo |
551 | |
552 | For the RT tracker we specify a Queue in which to file a ticket. |
553 | |
554 | @@ -430,7 +424,8 @@ |
555 | >>> example_roundup = factory.makeBugTracker( |
556 | ... 'http://roundup.example.com', BugTrackerType.ROUNDUP) |
557 | >>> links = example_roundup.getBugFilingAndSearchLinks( |
558 | - ... remote_product='testproduct', summary="Foo", description="Bar") |
559 | + ... remote_product='testproduct', remote_component='testcomponent', |
560 | + ... summary="Foo", description="Bar") |
561 | >>> print_links(links) |
562 | bug_filing_url: http://.../issue?@template=item&title=Foo&@note=Bar |
563 | bug_search_url: http://.../issue?@template=search&@search_text=Foo |
564 | @@ -473,7 +468,7 @@ |
565 | >>> links = mozilla_bugzilla.getBugFilingAndSearchLinks( |
566 | ... 'test', None, None) |
567 | >>> print_links(links) |
568 | - bug_filing_url: ...?product=test&short_desc=&long_desc= |
569 | + bug_filing_url: ...?product=test&component=&short_desc=&long_desc= |
570 | bug_search_url: ...?product=test&short_desc= |
571 | |
572 | The remote_product, summary and description values are URL-encoded to ensure |
573 | @@ -482,7 +477,7 @@ |
574 | >>> links = mozilla_bugzilla.getBugFilingAndSearchLinks( |
575 | ... remote_product='@test&', summary="%&", description="()") |
576 | >>> print_links(links) |
577 | - bug_filing_url: ...?product=%40test%26&short_desc=%25%26&long_desc=%28%29 |
578 | + bug_filing_url: ...?product=%40test%26&component=&short_desc=%25%26&long_desc=%28%29 |
579 | bug_search_url: ...?product=%40test%26&short_desc=%25%26 |
580 | |
581 | getBugFilingAndSearchLinks() will also handle unicode values in the |
582 | @@ -551,8 +546,6 @@ |
583 | ... remote_product='testproduct', summary="Foo", description="Bar") |
584 | |
585 | >>> print_links(links) |
586 | - bug_filing_url: |
587 | - http://.../enter_bug.cgi?product=testproduct&short_desc=Foo&comment=Bar |
588 | - bug_search_url: |
589 | - http://.../query.cgi?product=testproduct&short_desc=Foo |
590 | + bug_filing_url: http://.../enter_bug.cgi?product=testproduct&component=&short_desc=Foo&comment=Bar |
591 | + bug_search_url: http://.../query.cgi?product=testproduct&short_desc=Foo |
592 | |
593 | |
594 | === modified file 'lib/lp/bugs/interfaces/bugtracker.py' |
595 | --- lib/lp/bugs/interfaces/bugtracker.py 2011-02-23 20:26:53 +0000 |
596 | +++ lib/lp/bugs/interfaces/bugtracker.py 2011-05-19 01:14:21 +0000 |
597 | @@ -306,12 +306,14 @@ |
598 | watches_needing_update = Attribute( |
599 | "The set of bug watches that need updating.") |
600 | |
601 | - def getBugFilingAndSearchLinks(remote_product, summary=None, |
602 | - description=None): |
603 | + def getBugFilingAndSearchLinks(remote_product, remote_component=None, |
604 | + summary=None, description=None): |
605 | """Return the bug filing and search links for the tracker. |
606 | |
607 | :param remote_product: The name of the product on which the bug |
608 | is to be filed or search for. |
609 | + :param remote_component: The name of the component on which the bug |
610 | + is to be filed or searched for. |
611 | :param summary: The string with which to pre-filly the summary |
612 | field of the upstream bug tracker's search and bug filing forms. |
613 | :param description: The string with which to pre-filly the description |
614 | @@ -538,6 +540,10 @@ |
615 | title=_('Name'), |
616 | description=_("The name of a software component " |
617 | "as shown in Launchpad."))) |
618 | + sourcepackagename = Choice( |
619 | + title=_("Package"), required=False, vocabulary='SourcePackageName') |
620 | + distribution = Choice( |
621 | + title=_("Distribution"), required=False, vocabulary='Distribution') |
622 | |
623 | distro_source_package = exported( |
624 | Reference( |
625 | |
626 | === modified file 'lib/lp/bugs/model/bugtracker.py' |
627 | --- lib/lp/bugs/model/bugtracker.py 2011-03-23 16:28:51 +0000 |
628 | +++ lib/lp/bugs/model/bugtracker.py 2011-05-19 01:14:21 +0000 |
629 | @@ -251,7 +251,6 @@ |
630 | |
631 | def addComponent(self, component_name): |
632 | """Adds a component that is synced from a remote bug tracker""" |
633 | - |
634 | component = BugTrackerComponent() |
635 | component.name = component_name |
636 | component.component_group = self |
637 | @@ -263,23 +262,28 @@ |
638 | return component |
639 | |
640 | def getComponent(self, component_name): |
641 | - """Retrieves a component by the given name. |
642 | + """Retrieves a component by the given name or id number. |
643 | |
644 | None is returned if there is no component by that name in the |
645 | group. |
646 | """ |
647 | - |
648 | if component_name is None: |
649 | return None |
650 | + elif component_name.isdigit(): |
651 | + component_id = int(component_name) |
652 | + return Store.of(self).find( |
653 | + BugTrackerComponent, |
654 | + BugTrackerComponent.id == component_id, |
655 | + BugTrackerComponent.component_group == self.id).one() |
656 | else: |
657 | return Store.of(self).find( |
658 | BugTrackerComponent, |
659 | - (BugTrackerComponent.name == component_name)).one() |
660 | + BugTrackerComponent.name == component_name, |
661 | + BugTrackerComponent.component_group == self.id).one() |
662 | |
663 | def addCustomComponent(self, component_name): |
664 | """Adds a component locally that isn't synced from a remote tracker |
665 | """ |
666 | - |
667 | component = BugTrackerComponent() |
668 | component.name = component_name |
669 | component.component_group = self |
670 | @@ -329,6 +333,7 @@ |
671 | _filing_url_patterns = { |
672 | BugTrackerType.BUGZILLA: ( |
673 | "%(base_url)s/enter_bug.cgi?product=%(remote_product)s" |
674 | + "&component=%(remote_component)s" |
675 | "&short_desc=%(summary)s&long_desc=%(description)s"), |
676 | BugTrackerType.GOOGLE_CODE: ( |
677 | "%(base_url)s/entry?summary=%(summary)s&" |
678 | @@ -387,6 +392,7 @@ |
679 | return { |
680 | gnome_bugzilla: ( |
681 | "%(base_url)s/enter_bug.cgi?product=%(remote_product)s" |
682 | + "&component=%(remote_component)s" |
683 | "&short_desc=%(summary)s&comment=%(description)s"), |
684 | } |
685 | |
686 | @@ -403,7 +409,8 @@ |
687 | else: |
688 | return False |
689 | |
690 | - def getBugFilingAndSearchLinks(self, remote_product, summary=None, |
691 | + def getBugFilingAndSearchLinks(self, remote_product, |
692 | + remote_component=None, summary=None, |
693 | description=None): |
694 | """See `IBugTracker`.""" |
695 | bugtracker_urls = {'bug_filing_url': None, 'bug_search_url': None} |
696 | @@ -418,6 +425,10 @@ |
697 | # quote() doesn't blow up later on. |
698 | remote_product = '' |
699 | |
700 | + if remote_component is None: |
701 | + # Ditto for remote component. |
702 | + remote_component = '' |
703 | + |
704 | if self in self._custom_filing_url_patterns: |
705 | # Some bugtrackers are customised to accept different |
706 | # querystring parameters from the default. We special-case |
707 | @@ -475,6 +486,7 @@ |
708 | url_components = { |
709 | 'base_url': base_url, |
710 | 'remote_product': quote(remote_product), |
711 | + 'remote_component': quote(remote_component), |
712 | 'summary': quote(summary), |
713 | 'description': quote(description), |
714 | } |
715 | @@ -675,9 +687,17 @@ |
716 | """See `IBugTracker`.""" |
717 | component_group = None |
718 | store = IStore(BugTrackerComponentGroup) |
719 | - component_group = store.find( |
720 | - BugTrackerComponentGroup, |
721 | - name = component_group_name).one() |
722 | + if component_group_name is None: |
723 | + return None |
724 | + elif component_group_name.isdigit(): |
725 | + component_group_id = int(component_group_name) |
726 | + component_group = store.find( |
727 | + BugTrackerComponentGroup, |
728 | + BugTrackerComponentGroup.id==component_group_id).one() |
729 | + else: |
730 | + component_group = store.find( |
731 | + BugTrackerComponentGroup, |
732 | + BugTrackerComponentGroup.name==component_group_name).one() |
733 | return component_group |
734 | |
735 | |
736 | |
737 | === added file 'lib/lp/bugs/templates/bugtracker-edit-component.pt' |
738 | --- lib/lp/bugs/templates/bugtracker-edit-component.pt 1970-01-01 00:00:00 +0000 |
739 | +++ lib/lp/bugs/templates/bugtracker-edit-component.pt 2011-05-19 01:14:21 +0000 |
740 | @@ -0,0 +1,16 @@ |
741 | +<bug-tracker-edit-component |
742 | + xmlns="http://www.w3.org/1999/xhtml" |
743 | + xmlns:tal="http://xml.zope.org/namespaces/tal" |
744 | + xmlns:metal="http://xml.zope.org/namespaces/metal" |
745 | + xmlns:i18n="http://xml.zope.org/namespaces/i18n" |
746 | + metal:use-macro="view/macro:page/main_only" |
747 | + i18n:domain="malone"> |
748 | + |
749 | + <div metal:fill-slot="main"> |
750 | + <h1>Link <span tal:replace="context/component_group/name"/> component '<span tal:replace="context/name"/>'</h1> |
751 | + <div metal:use-macro="context/@@launchpad_form/form"> |
752 | + <p>Configure component</p> |
753 | + </div> |
754 | + </div> |
755 | + |
756 | +</bug-tracker-edit-component> |
757 | |
758 | === modified file 'lib/lp/bugs/templates/bugtracker-index.pt' |
759 | --- lib/lp/bugs/templates/bugtracker-index.pt 2010-10-10 21:54:16 +0000 |
760 | +++ lib/lp/bugs/templates/bugtracker-index.pt 2011-05-19 01:14:21 +0000 |
761 | @@ -4,6 +4,7 @@ |
762 | xmlns:metal="http://xml.zope.org/namespaces/metal" |
763 | xmlns:i18n="http://xml.zope.org/namespaces/i18n" |
764 | metal:use-macro="view/macro:page/main_side" |
765 | + tal:define="features request/features" |
766 | i18n:domain="malone"> |
767 | |
768 | <metal:heading fill-slot="heading"> |
769 | @@ -34,6 +35,7 @@ |
770 | <div class="yui-g"> |
771 | <div class="first yui-u"> |
772 | <div tal:replace="structure context/@@+portlet-details" /> |
773 | + <div tal:replace="structure context/@@+portlet-components" /> |
774 | </div> |
775 | <div class="yui-u"> |
776 | <div tal:replace="structure context/@@+portlet-projects" /> |
777 | |
778 | === added file 'lib/lp/bugs/templates/bugtracker-portlet-components.pt' |
779 | --- lib/lp/bugs/templates/bugtracker-portlet-components.pt 1970-01-01 00:00:00 +0000 |
780 | +++ lib/lp/bugs/templates/bugtracker-portlet-components.pt 2011-05-19 01:14:21 +0000 |
781 | @@ -0,0 +1,36 @@ |
782 | +<div |
783 | + xmlns:tal="http://xml.zope.org/namespaces/tal" |
784 | + xmlns:metal="http://xml.zope.org/namespaces/metal" |
785 | + xmlns:i18n="http://xml.zope.org/namespaces/i18n" |
786 | + class="portlet" id="portlet-components"> |
787 | + <h2>Components</h2> |
788 | + <p tal:condition="view/user"> |
789 | + You can link components from this bug tracker to their corresponding |
790 | + distribution source packages in the project's “Change |
791 | + components” page. |
792 | + </p> |
793 | + <ul tal:define="related_component_groups view/related_component_groups"> |
794 | + <li tal:repeat="component_group related_component_groups"> |
795 | + <strong><span tal:replace="component_group/name" /></strong> |
796 | + <ul tal:define="components component_group/components"> |
797 | + <li tal:repeat="component components"> |
798 | + <span tal:replace="component/name" /> |
799 | + <tal:logged-in condition="view/user"> |
800 | + |
801 | + <span tal:condition="component/distro_source_package"> |
802 | + <a class="sprite edit" |
803 | + tal:attributes="href component/fmt:url/+edit"> |
804 | + <span tal:replace="structure component/distro_source_package/name"/></a> |
805 | + </span> |
806 | + <a class="sprite add" |
807 | + tal:condition="not: component/distro_source_package" |
808 | + tal:attributes="href component/fmt:url/+edit"></a> |
809 | + </tal:logged-in> |
810 | + </li> |
811 | + </ul> |
812 | + </li> |
813 | + <li tal:condition="not: related_component_groups"> |
814 | + <em>This bug tracker has no components</em> |
815 | + </li> |
816 | + </ul> |
817 | +</div> |
818 | |
819 | === modified file 'lib/lp/registry/configure.zcml' |
820 | --- lib/lp/registry/configure.zcml 2011-05-13 03:46:29 +0000 |
821 | +++ lib/lp/registry/configure.zcml 2011-05-19 01:14:21 +0000 |
822 | @@ -482,6 +482,7 @@ |
823 | getVersion |
824 | get_distroseries_packages |
825 | has_bugtasks |
826 | + has_component |
827 | high_bugtasks |
828 | inprogress_bugtasks |
829 | latest_overall_publication |
830 | |
831 | === modified file 'lib/lp/registry/interfaces/sourcepackage.py' |
832 | --- lib/lp/registry/interfaces/sourcepackage.py 2011-05-12 14:55:54 +0000 |
833 | +++ lib/lp/registry/interfaces/sourcepackage.py 2011-05-19 01:14:21 +0000 |
834 | @@ -328,6 +328,8 @@ |
835 | :return: A {`Pocket`-name : `IBranch`} dict. |
836 | """ |
837 | |
838 | + def has_component(): |
839 | + """Indicates if the source package has a bug tracker component.""" |
840 | |
841 | class ISourcePackageFactory(Interface): |
842 | """A creator of source packages.""" |
843 | |
844 | === modified file 'lib/lp/registry/model/sourcepackage.py' |
845 | --- lib/lp/registry/model/sourcepackage.py 2011-05-12 14:55:54 +0000 |
846 | +++ lib/lp/registry/model/sourcepackage.py 2011-05-19 01:14:21 +0000 |
847 | @@ -45,6 +45,7 @@ |
848 | HasBugHeatMixin, |
849 | ) |
850 | from lp.bugs.model.bugtask import BugTask |
851 | +from lp.bugs.model.bugtracker import BugTrackerComponent |
852 | from lp.buildmaster.enums import BuildStatus |
853 | from lp.code.interfaces.seriessourcepackagebranch import ( |
854 | IMakeOfficialBranchLinks, |
855 | @@ -501,6 +502,16 @@ |
856 | BugTask.sourcepackagename == self.sourcepackagename), |
857 | user, wanted_tags=wanted_tags) |
858 | |
859 | + def has_component(self): |
860 | + """Indicates if the source package has a bug tracker component.""" |
861 | + store = Store.of(self.sourcepackagename) |
862 | + pkgs = store.find( |
863 | + BugTrackerComponent, |
864 | + BugTrackerComponent.distribution == self.distribution.id, |
865 | + BugTrackerComponent.source_package_name == |
866 | + self.sourcepackagename.id) |
867 | + return not pkgs.is_empty() |
868 | + |
869 | @property |
870 | def max_bug_heat(self): |
871 | """See `IHasBugs`.""" |
872 | |
873 | === added file 'lib/lp/services/features/scopes.py' |
874 | --- lib/lp/services/features/scopes.py 1970-01-01 00:00:00 +0000 |
875 | +++ lib/lp/services/features/scopes.py 2011-05-07 15:31:21 +0000 |
876 | @@ -0,0 +1,233 @@ |
877 | +# Copyright 2011 Canonical Ltd. This software is licensed under the |
878 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
879 | + |
880 | +"""Connect feature flags into scopes where they can be used. |
881 | + |
882 | +The most common is flags scoped by some attribute of a web request, such as |
883 | +the page ID or the server name. But other types of scope can also match code |
884 | +run from cron scripts and potentially also other places. |
885 | +""" |
886 | + |
887 | +__all__ = [ |
888 | + 'HANDLERS', |
889 | + 'ScopesForScript', |
890 | + 'ScopesFromRequest', |
891 | + 'undocumented_scopes', |
892 | + ] |
893 | + |
894 | +__metaclass__ = type |
895 | + |
896 | +import re |
897 | + |
898 | +from zope.component import getUtility |
899 | + |
900 | +from canonical.launchpad.webapp.interfaces import ILaunchBag |
901 | +from lp.services.propertycache import cachedproperty |
902 | +import canonical.config |
903 | + |
904 | + |
905 | +undocumented_scopes = set() |
906 | + |
907 | + |
908 | +class BaseScope(): |
909 | + """A base class for scope handlers. |
910 | + |
911 | + The docstring of subclasses is used on the +feature-info page as |
912 | + documentation, so write them accordingly. |
913 | + """ |
914 | + |
915 | + # The regex pattern used to decide if a handler can evaluate a particular |
916 | + # scope. Also used on +feature-info. |
917 | + pattern = None |
918 | + |
919 | + @cachedproperty |
920 | + def compiled_pattern(self): |
921 | + """The compiled scope matching regex. A small optimization.""" |
922 | + return re.compile(self.pattern) |
923 | + |
924 | + def lookup(self, scope_name): |
925 | + """Returns true if the given scope name is "active".""" |
926 | + raise NotImplementedError('Subclasses of BaseScope must implement ' |
927 | + 'lookup.') |
928 | + |
929 | + |
930 | +class DefaultScope(BaseScope): |
931 | + """The default scope. Always active.""" |
932 | + |
933 | + pattern = r'default$' |
934 | + |
935 | + def lookup(self, scope_name): |
936 | + return True |
937 | + |
938 | + |
939 | +class BaseWebRequestScope(BaseScope): |
940 | + """Base class for scopes that key off web request attributes.""" |
941 | + |
942 | + def __init__(self, request): |
943 | + self.request = request |
944 | + |
945 | + |
946 | +class PageScope(BaseWebRequestScope): |
947 | + """The current page ID. |
948 | + |
949 | + Pageid scopes are written as 'pageid:' + the pageid to match. Pageids |
950 | + are treated as a namespace with : and # delimiters. |
951 | + |
952 | + For example, the scope 'pageid:Foo' will be active on pages with pageids: |
953 | + Foo |
954 | + Foo:Bar |
955 | + Foo#quux |
956 | + """ |
957 | + |
958 | + pattern = r'pageid:' |
959 | + |
960 | + def lookup(self, scope_name): |
961 | + """Is the given scope match the current pageid?""" |
962 | + pageid_scope = scope_name[len('pageid:'):] |
963 | + scope_segments = self._pageid_to_namespace(pageid_scope) |
964 | + request_segments = self._request_pageid_namespace |
965 | + # In 2.6, this can be replaced with izip_longest |
966 | + for pos, name in enumerate(scope_segments): |
967 | + if pos == len(request_segments): |
968 | + return False |
969 | + if request_segments[pos] != name: |
970 | + return False |
971 | + return True |
972 | + |
973 | + @staticmethod |
974 | + def _pageid_to_namespace(pageid): |
975 | + """Return a list of namespace elements for pageid.""" |
976 | + # Normalise delimiters. |
977 | + pageid = pageid.replace('#', ':') |
978 | + # Create a list to walk, empty namespaces are elided. |
979 | + return [name for name in pageid.split(':') if name] |
980 | + |
981 | + @cachedproperty |
982 | + def _request_pageid_namespace(self): |
983 | + return tuple(self._pageid_to_namespace( |
984 | + self.request._orig_env.get('launchpad.pageid', ''))) |
985 | + |
986 | + |
987 | +class TeamScope(BaseScope): |
988 | + """The current user's team memberships. |
989 | + |
990 | + Team ID scopes are written as 'team:' + the team name to match. |
991 | + |
992 | + The scope 'team:launchpad-beta-users' will match members of the team |
993 | + 'launchpad-beta-users'. |
994 | + """ |
995 | + |
996 | + pattern = r'team:' |
997 | + |
998 | + def lookup(self, scope_name): |
999 | + """Is the given scope a team membership? |
1000 | + |
1001 | + This will do a two queries, so we probably want to keep the number of |
1002 | + team based scopes in use to a small number. (Person.inTeam could be |
1003 | + fixed to reduce this to one query). |
1004 | + """ |
1005 | + team_name = scope_name[len('team:'):] |
1006 | + person = getUtility(ILaunchBag).user |
1007 | + if person is None: |
1008 | + return False |
1009 | + return person.inTeam(team_name) |
1010 | + |
1011 | + |
1012 | +class ServerScope(BaseScope): |
1013 | + """Matches the current server. |
1014 | + |
1015 | + For example, the scope server.lpnet is active when is_lpnet is set to True |
1016 | + in the Launchpad configuration. |
1017 | + """ |
1018 | + |
1019 | + pattern = r'server\.' |
1020 | + |
1021 | + def lookup(self, scope_name): |
1022 | + """Match the current server as a scope.""" |
1023 | + server_name = scope_name.split('.', 1)[1] |
1024 | + try: |
1025 | + return canonical.config.config['launchpad']['is_' + server_name] |
1026 | + except KeyError: |
1027 | + pass |
1028 | + return False |
1029 | + |
1030 | + |
1031 | +class ScriptScope(BaseScope): |
1032 | + """Matches the name of the currently running script. |
1033 | + |
1034 | + For example, the scope script:embroider is active in a script called |
1035 | + "embroider." |
1036 | + """ |
1037 | + |
1038 | + pattern = r'script:' |
1039 | + |
1040 | + def __init__(self, script_name): |
1041 | + self.script_scope = self.pattern + script_name |
1042 | + |
1043 | + def lookup(self, scope_name): |
1044 | + """Match the running script as a scope.""" |
1045 | + return scope_name == self.script_scope |
1046 | + |
1047 | + |
1048 | +# These are the handlers for all of the allowable scopes, listed here so that |
1049 | +# we can for example show all of them in an admin page. Any new scope will |
1050 | +# need a scope handler and that scope handler has to be added to this list. |
1051 | +# See BaseScope for hints as to what a scope handler should look like. |
1052 | +HANDLERS = set([DefaultScope, PageScope, TeamScope, ServerScope, ScriptScope]) |
1053 | + |
1054 | + |
1055 | +class MultiScopeHandler(): |
1056 | + """A scope handler that combines multiple `BaseScope`s. |
1057 | + |
1058 | + The ordering in which they're added is arbitrary, because precedence is |
1059 | + determined by the ordering of rules. |
1060 | + """ |
1061 | + |
1062 | + def __init__(self, scopes): |
1063 | + self.handlers = scopes |
1064 | + |
1065 | + def _findMatchingHandlers(self, scope_name): |
1066 | + """Find any handlers that match `scope_name`.""" |
1067 | + return [ |
1068 | + handler |
1069 | + for handler in self.handlers |
1070 | + if handler.compiled_pattern.match(scope_name)] |
1071 | + |
1072 | + def lookup(self, scope_name): |
1073 | + """Determine if scope_name applies. |
1074 | + |
1075 | + This method iterates over the configured scope handlers until it |
1076 | + either finds one that claims the requested scope name matches, |
1077 | + or the handlers are exhausted, in which case the |
1078 | + scope name is not a match. |
1079 | + """ |
1080 | + matching_handlers = self._findMatchingHandlers(scope_name) |
1081 | + for handler in matching_handlers: |
1082 | + if handler.lookup(scope_name): |
1083 | + return True |
1084 | + |
1085 | + # If we didn't find at least one matching handler, then the |
1086 | + # requested scope is unknown and we want to record the scope for |
1087 | + # the +flag-info page to display. |
1088 | + if len(matching_handlers) == 0: |
1089 | + undocumented_scopes.add(scope_name) |
1090 | + |
1091 | + |
1092 | +class ScopesFromRequest(MultiScopeHandler): |
1093 | + """Identify feature scopes based on request state.""" |
1094 | + |
1095 | + def __init__(self, request): |
1096 | + super(ScopesFromRequest, self).__init__([ |
1097 | + DefaultScope(), |
1098 | + PageScope(request), |
1099 | + TeamScope(), |
1100 | + ServerScope()]) |
1101 | + |
1102 | + |
1103 | +class ScopesForScript(MultiScopeHandler): |
1104 | + """Identify feature scopes for a given script.""" |
1105 | + |
1106 | + def __init__(self, script_name): |
1107 | + super(ScopesForScript, self).__init__([ |
1108 | + DefaultScope(), |
1109 | + ScriptScope(script_name)]) |
Hi Bryce,
There's lots of stuff that I'm unfamiliar with here, so please forgive me for holding you up with a "Needs Information" vote. I have some big questions about the UI and test coverage.
Otherwise it would be Approved, with some smaller notes and superficial issues.
> === modified file 'lib/canonical/ launchpad/ scripts/ bzremotecompone ntfinder. py' launchpad/ scripts/ bzremotecompone ntfinder. py 2010-10-20 00:24:50 +0000 launchpad/ scripts/ bzremotecompone ntfinder. py 2010-11-16 19:54:52 +0000 bugzilla_ text = static_ bugzilla_ text tsAndComponents (self, bugtracker_ name=None) :
> --- lib/canonical/
> +++ lib/canonical/
> @@ -117,7 +117,6 @@
> self.static_
>
> def getRemoteProduc
> - """"""
There's supposed to be a docstring. It looks as if this one was put in
as a placeholder: "I'll write one later, and if I forget, the reviewer
will spot it." Can you think of what they mant to write?
The fact that this got into the codebase may be a clue that we're
being too sloppy in our reviews.
> === modified file 'lib/canonical/ widgets/ bugtask. py' widgets/ bugtask. py 2010-11-08 12:52:43 +0000 widgets/ bugtask. py 2010-11-16 19:54:52 +0000 kageNameWidget( ckageNameWidget ):
> --- lib/canonical/
> +++ lib/canonical/
> @@ -495,6 +495,25 @@
> return distribution
>
>
> +class UbuntuSourcePac
> + BugTaskSourcePa
Why is this line broken? There's plenty of space. Did you rename this
from a longer original name?
> + """Package widget where the distribution can be assumed as Ubuntu
There should be a full stop at the end of the sentence.
> + distribution_name = "ubuntu" (self): ackageNameWidge t`""" IDistributionSe t).getByName( on_name) on_name)
> +
> + def getDistribution
> + """See `BugTaskSourceP
> + distribution = getUtility(
> + self.distributi
> + if distribution is None:
> + raise UnexpectedFormData(
> + "No such distribution: %s" % self.distributi
> + return distribution
Would it be worth having a comment here to say why you have to override
this method?
Also, doesn't this method amount to a difficult spelling for
return getUtility( ILaunchpadCeleb rities) .ubuntu
?
> === modified file 'lib/lp/ bugs/browser/ bugalsoaffects. py' bugs/browser/ bugalsoaffects. py 2010-09-03 03:12:39 +0000 bugs/browser/ bugalsoaffects. py 2010-11-16 19:54:52 +0000 bugtracker. getRemoteCompon entGroup( remote_ product) 'add_packaging' , False): target. sourcepackagena me components: distro_ source_ package is not None and distro_ source_ package. name == package_name):
> --- lib/lp/
> +++ lib/lp/
> @@ -664,8 +664,20 @@
> title = bug.title
> description = u"Originally reported at:\n %s\n\n%s" % (
> canonical_url(bug), bug.description)
> + remote_component = ""
> + comp_group = target.
> + target.
> +
> + # Look up the remote component if we have the necessary info
> + if comp_group is not None and data.get(
> + package_name = self.context.
> + for component in comp_group.
> + if (component.
> + component.
> + ...