Merge lp:~allenap/launchpad/bug-subscription-filter-models-bug-639749 into lp:launchpad/db-devel
- bug-subscription-filter-models-bug-639749
- Merge into db-devel
| Status: | Merged |
|---|---|
| Approved by: | Gavin Panella on 2010-09-16 |
| Approved revision: | no longer in the source branch. |
| Merged at revision: | 9825 |
| Proposed branch: | lp:~allenap/launchpad/bug-subscription-filter-models-bug-639749 |
| Merge into: | lp:launchpad/db-devel |
| Diff against target: |
2507 lines (+988/-246) 52 files modified
database/schema/security.cfg (+46/-0) lib/canonical/launchpad/emailtemplates/build-request.txt (+1/-0) lib/lp/bugs/browser/bug.py (+32/-26) lib/lp/bugs/browser/bugtask.py (+14/-11) lib/lp/bugs/browser/configure.zcml (+5/-0) lib/lp/bugs/browser/tests/test_bugtask.py (+39/-32) lib/lp/bugs/configure.zcml (+3/-0) lib/lp/bugs/doc/bugsubscription.txt (+2/-2) lib/lp/bugs/doc/initial-bug-contacts.txt (+1/-1) lib/lp/bugs/interfaces/bug.py (+9/-1) lib/lp/bugs/interfaces/bugactivity.py (+44/-9) lib/lp/bugs/interfaces/bugtarget.py (+3/-0) lib/lp/bugs/interfaces/bugtask.py (+1/-0) lib/lp/bugs/model/bug.py (+106/-28) lib/lp/bugs/model/bugsubscriptionfilter.py (+36/-0) lib/lp/bugs/model/bugsubscriptionfilterimportance.py (+29/-0) lib/lp/bugs/model/bugsubscriptionfilterstatus.py (+29/-0) lib/lp/bugs/model/bugsubscriptionfiltertag.py (+29/-0) lib/lp/bugs/model/bugtarget.py (+16/-15) lib/lp/bugs/model/bugtask.py (+13/-5) lib/lp/bugs/model/tests/test_bugsubscriptionfilter.py (+66/-0) lib/lp/bugs/model/tests/test_bugsubscriptionfilterimportance.py (+54/-0) lib/lp/bugs/model/tests/test_bugsubscriptionfilterstatus.py (+52/-0) lib/lp/bugs/model/tests/test_bugsubscriptionfiltertag.py (+51/-0) lib/lp/bugs/stories/webservice/xx-bug.txt (+31/-0) lib/lp/bugs/templates/bug-portlet-actions.pt (+2/-2) lib/lp/bugs/templates/bugtask-index.pt (+1/-1) lib/lp/bugs/tests/has-bug-supervisor.txt (+1/-1) lib/lp/buildmaster/tests/mock_slaves.py (+0/-2) lib/lp/code/browser/sourcepackagerecipe.py (+6/-0) lib/lp/code/browser/tests/test_sourcepackagerecipe.py (+19/-0) lib/lp/code/mail/sourcepackagerecipebuild.py (+3/-0) lib/lp/code/mail/tests/test_sourcepackagerecipebuild.py (+18/-6) lib/lp/code/model/tests/test_recipebuilder.py (+65/-41) lib/lp/code/model/tests/test_sourcepackagerecipe.py (+8/-5) lib/lp/code/tests/helpers.py (+12/-0) lib/lp/codehosting/branchdistro.py (+23/-1) lib/lp/codehosting/tests/test_branchdistro.py (+40/-0) lib/lp/registry/browser/product.py (+1/-0) lib/lp/registry/configure.zcml (+1/-0) lib/lp/registry/interfaces/product.py (+1/-0) lib/lp/registry/interfaces/projectgroup.py (+2/-0) lib/lp/registry/model/distributionsourcepackage.py (+3/-0) lib/lp/registry/model/distroseries.py (+3/-0) lib/lp/registry/model/milestone.py (+8/-5) lib/lp/registry/model/person.py (+2/-0) lib/lp/registry/model/productseries.py (+3/-0) lib/lp/registry/model/projectgroup.py (+20/-1) lib/lp/registry/model/sourcepackage.py (+3/-0) lib/lp/registry/model/structuralsubscription.py (+20/-31) lib/lp/registry/vocabularies.py (+9/-18) lib/lp/soyuz/tests/test_binarypackagebuildbehavior.py (+2/-2) |
| To merge this branch: | bzr merge lp:~allenap/launchpad/bug-subscription-filter-models-bug-639749 |
| Related bugs: |
| Reviewer | Review Type | Date Requested | Status |
|---|---|---|---|
| Edwin Grubbs (community) | code | 2010-09-15 | Approve on 2010-09-15 |
|
Review via email:
|
|||
Commit Message
Add Storm model classes for BugSubscription
Description of the Change
- Add Storm model classes for BugSubscription
already exist),
- Add database permissions for the aforementioned tables,
- Clean up some of the code in StructuralSubsc
| Gavin Panella (allenap) wrote : | # |
> Unless something has changed, each model class with its own table
> needs to have its own file.
There are at least a couple of other places in bugs/model where more
than one model class is in the same module. Like these new classes,
they're all supporting classes for the first model class in the
module. These BugSubscription
be exposed to browser code (and if they are then I'll move them to
their own modules anyway).
I'll gamble and land this branch as-is, but if you object I'll knock
up another branch to split them up.
Thanks for the review!
| Edwin Grubbs (edwin-grubbs) wrote : | # |
Having separate files for separate database classes is still listed in the CodeReviewCheck
Here is the reason that I have vivid memories of this rule.
https:/
| Gavin Panella (allenap) wrote : | # |
Okay, I'll split them out anyway. My branch didn't land because of some test failures anyway, so I can do it before landing.
BTW, I can't get see that thread. I suspect I unsubscribed from launchpad-reviews; even though I just changed my password globally on that Mailman I can't authorization is denied for that archive.
Preview Diff
| 1 | === modified file 'database/schema/security.cfg' |
| 2 | --- database/schema/security.cfg 2010-09-22 07:12:37 +0000 |
| 3 | +++ database/schema/security.cfg 2010-09-23 11:12:49 +0000 |
| 4 | @@ -539,6 +539,10 @@ |
| 5 | public.bugnotification = SELECT, INSERT |
| 6 | public.bugnotificationrecipient = SELECT, INSERT |
| 7 | public.bugsubscription = SELECT |
| 8 | +public.bugsubscriptionfilter = SELECT |
| 9 | +public.bugsubscriptionfilterstatus = SELECT |
| 10 | +public.bugsubscriptionfilterimportance = SELECT |
| 11 | +public.bugsubscriptionfiltertag = SELECT |
| 12 | public.bugtask = SELECT, INSERT, UPDATE |
| 13 | public.bugtracker = SELECT, INSERT |
| 14 | public.bugtrackeralias = SELECT |
| 15 | @@ -618,6 +622,10 @@ |
| 16 | public.bugactivity = SELECT, INSERT |
| 17 | public.bugaffectsperson = SELECT, INSERT, UPDATE, DELETE |
| 18 | public.bugsubscription = SELECT |
| 19 | +public.bugsubscriptionfilter = SELECT |
| 20 | +public.bugsubscriptionfilterstatus = SELECT |
| 21 | +public.bugsubscriptionfilterimportance = SELECT |
| 22 | +public.bugsubscriptionfiltertag = SELECT |
| 23 | public.bugnotification = SELECT, INSERT |
| 24 | public.bugnotificationrecipient = SELECT, INSERT |
| 25 | public.structuralsubscription = SELECT |
| 26 | @@ -630,6 +638,7 @@ |
| 27 | [branch-distro] |
| 28 | type=user |
| 29 | public.branch = SELECT, INSERT, UPDATE |
| 30 | +public.branchrevision = SELECT, INSERT |
| 31 | public.branchsubscription = SELECT, INSERT |
| 32 | public.distribution = SELECT |
| 33 | public.distroseries = SELECT |
| 34 | @@ -637,6 +646,7 @@ |
| 35 | public.karmaaction = SELECT |
| 36 | public.person = SELECT |
| 37 | public.product = SELECT |
| 38 | +public.revision = SELECT |
| 39 | public.seriessourcepackagebranch = SELECT, INSERT, DELETE |
| 40 | public.sourcepackagename = SELECT |
| 41 | public.teamparticipation = SELECT |
| 42 | @@ -804,6 +814,10 @@ |
| 43 | public.bugactivity = SELECT, INSERT |
| 44 | public.bugaffectsperson = SELECT, INSERT, UPDATE, DELETE |
| 45 | public.bugsubscription = SELECT |
| 46 | +public.bugsubscriptionfilter = SELECT |
| 47 | +public.bugsubscriptionfilterstatus = SELECT |
| 48 | +public.bugsubscriptionfilterimportance = SELECT |
| 49 | +public.bugsubscriptionfiltertag = SELECT |
| 50 | public.bugnotification = SELECT, INSERT |
| 51 | public.bugnotificationrecipient = SELECT, INSERT |
| 52 | public.bugnomination = SELECT |
| 53 | @@ -928,6 +942,10 @@ |
| 54 | public.bugpackageinfestation = SELECT, INSERT, UPDATE |
| 55 | public.bugproductinfestation = SELECT, INSERT, UPDATE |
| 56 | public.bugsubscription = SELECT, INSERT, UPDATE, DELETE |
| 57 | +public.bugsubscriptionfilter = SELECT, INSERT, UPDATE, DELETE |
| 58 | +public.bugsubscriptionfilterstatus = SELECT, INSERT, UPDATE, DELETE |
| 59 | +public.bugsubscriptionfilterimportance = SELECT, INSERT, UPDATE, DELETE |
| 60 | +public.bugsubscriptionfiltertag = SELECT, INSERT, UPDATE, DELETE |
| 61 | public.bugtask = SELECT, INSERT, UPDATE, DELETE |
| 62 | public.bugtracker = SELECT, INSERT, UPDATE, DELETE |
| 63 | public.bugtrackeralias = SELECT, INSERT, UPDATE, DELETE |
| 64 | @@ -1156,6 +1174,10 @@ |
| 65 | public.bugaffectsperson = SELECT, INSERT, UPDATE, DELETE |
| 66 | public.bugjob = SELECT, INSERT |
| 67 | public.bugsubscription = SELECT |
| 68 | +public.bugsubscriptionfilter = SELECT |
| 69 | +public.bugsubscriptionfilterstatus = SELECT |
| 70 | +public.bugsubscriptionfilterimportance = SELECT |
| 71 | +public.bugsubscriptionfiltertag = SELECT |
| 72 | public.bugnotification = SELECT, INSERT |
| 73 | public.bugnotificationrecipient = SELECT, INSERT |
| 74 | public.bugnomination = SELECT |
| 75 | @@ -1258,6 +1280,10 @@ |
| 76 | public.bugaffectsperson = SELECT, INSERT, UPDATE, DELETE |
| 77 | public.bugjob = SELECT, INSERT |
| 78 | public.bugsubscription = SELECT |
| 79 | +public.bugsubscriptionfilter = SELECT |
| 80 | +public.bugsubscriptionfilterstatus = SELECT |
| 81 | +public.bugsubscriptionfilterimportance = SELECT |
| 82 | +public.bugsubscriptionfiltertag = SELECT |
| 83 | public.bugnotification = SELECT, INSERT |
| 84 | public.bugnotificationrecipient = SELECT, INSERT |
| 85 | public.bugnomination = SELECT |
| 86 | @@ -1325,6 +1351,10 @@ |
| 87 | public.bugnotification = SELECT, INSERT, UPDATE |
| 88 | public.bugnotificationrecipient = SELECT, INSERT, UPDATE |
| 89 | public.bugsubscription = SELECT, INSERT |
| 90 | +public.bugsubscriptionfilter = SELECT, INSERT |
| 91 | +public.bugsubscriptionfilterstatus = SELECT, INSERT |
| 92 | +public.bugsubscriptionfilterimportance = SELECT, INSERT |
| 93 | +public.bugsubscriptionfiltertag = SELECT, INSERT |
| 94 | public.bugnomination = SELECT |
| 95 | public.bug = SELECT, INSERT, UPDATE |
| 96 | public.bugactivity = SELECT, INSERT |
| 97 | @@ -1545,6 +1575,10 @@ |
| 98 | public.bugaffectsperson = SELECT, INSERT, UPDATE, DELETE |
| 99 | public.bugjob = SELECT, INSERT |
| 100 | public.bugsubscription = SELECT, INSERT |
| 101 | +public.bugsubscriptionfilter = SELECT, INSERT |
| 102 | +public.bugsubscriptionfilterstatus = SELECT, INSERT |
| 103 | +public.bugsubscriptionfilterimportance = SELECT, INSERT |
| 104 | +public.bugsubscriptionfiltertag = SELECT, INSERT |
| 105 | public.bugnotification = SELECT, INSERT |
| 106 | public.bugnotificationattachment = SELECT |
| 107 | public.bugnotificationrecipient = SELECT, INSERT |
| 108 | @@ -1553,6 +1587,10 @@ |
| 109 | public.bugtask = SELECT, INSERT, UPDATE |
| 110 | public.bugmessage = SELECT, INSERT |
| 111 | public.bugsubscription = SELECT, INSERT, UPDATE, DELETE |
| 112 | +public.bugsubscriptionfilter = SELECT, INSERT, UPDATE, DELETE |
| 113 | +public.bugsubscriptionfilterstatus = SELECT, INSERT, UPDATE, DELETE |
| 114 | +public.bugsubscriptionfilterimportance = SELECT, INSERT, UPDATE, DELETE |
| 115 | +public.bugsubscriptionfiltertag = SELECT, INSERT, UPDATE, DELETE |
| 116 | public.bugtracker = SELECT, INSERT |
| 117 | public.bugtrackeralias = SELECT, INSERT |
| 118 | public.bugwatch = SELECT, INSERT |
| 119 | @@ -1812,6 +1850,10 @@ |
| 120 | public.message = SELECT, INSERT |
| 121 | public.messagechunk = SELECT, INSERT |
| 122 | public.bugsubscription = SELECT, INSERT |
| 123 | +public.bugsubscriptionfilter = SELECT, INSERT |
| 124 | +public.bugsubscriptionfilterstatus = SELECT, INSERT |
| 125 | +public.bugsubscriptionfilterimportance = SELECT, INSERT |
| 126 | +public.bugsubscriptionfiltertag = SELECT, INSERT |
| 127 | public.bugmessage = SELECT, INSERT |
| 128 | public.sourcepackagename = SELECT |
| 129 | public.job = SELECT, INSERT, UPDATE |
| 130 | @@ -1839,6 +1881,10 @@ |
| 131 | public.bug = SELECT, UPDATE |
| 132 | public.bugattachment = SELECT, DELETE |
| 133 | public.bugsubscription = SELECT |
| 134 | +public.bugsubscriptionfilter = SELECT |
| 135 | +public.bugsubscriptionfilterstatus = SELECT |
| 136 | +public.bugsubscriptionfilterimportance = SELECT |
| 137 | +public.bugsubscriptionfiltertag = SELECT |
| 138 | public.bugaffectsperson = SELECT |
| 139 | public.bugnotification = SELECT, DELETE |
| 140 | public.bugnotificationrecipientarchive = SELECT |
| 141 | |
| 142 | === modified file 'lib/canonical/launchpad/emailtemplates/build-request.txt' |
| 143 | --- lib/canonical/launchpad/emailtemplates/build-request.txt 2010-07-30 16:24:59 +0000 |
| 144 | +++ lib/canonical/launchpad/emailtemplates/build-request.txt 2010-09-23 11:12:49 +0000 |
| 145 | @@ -4,4 +4,5 @@ |
| 146 | * Distroseries: %(distroseries)s |
| 147 | * Duration: %(duration)s |
| 148 | * Build Log: %(log_url)s |
| 149 | + * Upload Log: %(upload_log_url)s |
| 150 | * Builder: %(builder_url)s |
| 151 | |
| 152 | === modified file 'lib/lp/bugs/browser/bug.py' |
| 153 | --- lib/lp/bugs/browser/bug.py 2010-08-31 11:11:09 +0000 |
| 154 | +++ lib/lp/bugs/browser/bug.py 2010-09-23 11:12:49 +0000 |
| 155 | @@ -299,7 +299,7 @@ |
| 156 | |
| 157 | def unlinkcve(self): |
| 158 | """Return 'Remove CVE link' Link.""" |
| 159 | - enabled = bool(self.context.bug.cves) |
| 160 | + enabled = self.context.bug.has_cves |
| 161 | text = 'Remove CVE link' |
| 162 | return Link('+unlinkcve', text, icon='remove', enabled=enabled) |
| 163 | |
| 164 | @@ -307,31 +307,30 @@ |
| 165 | """Return the 'Offer mentorship' Link.""" |
| 166 | text = 'Offer mentorship' |
| 167 | user = getUtility(ILaunchBag).user |
| 168 | - enabled = self.context.bug.canMentor(user) |
| 169 | + enabled = False |
| 170 | return Link('+mentor', text, icon='add', enabled=enabled) |
| 171 | |
| 172 | def retractmentoring(self): |
| 173 | """Return the 'Retract mentorship' Link.""" |
| 174 | text = 'Retract mentorship' |
| 175 | user = getUtility(ILaunchBag).user |
| 176 | - # We should really only allow people to retract mentoring if the |
| 177 | - # bug's open and the user's already a mentor. |
| 178 | - if user and not self.context.bug.is_complete: |
| 179 | - enabled = self.context.bug.isMentor(user) |
| 180 | - else: |
| 181 | - enabled = False |
| 182 | + enabled = False |
| 183 | return Link('+retractmentoring', text, icon='remove', enabled=enabled) |
| 184 | |
| 185 | + @property |
| 186 | + def _bug_question(self): |
| 187 | + return self.context.bug.getQuestionCreatedFromBug() |
| 188 | + |
| 189 | def createquestion(self): |
| 190 | """Create a question from this bug.""" |
| 191 | text = 'Convert to a question' |
| 192 | - enabled = self.context.bug.getQuestionCreatedFromBug() is None |
| 193 | + enabled = self._bug_question is None |
| 194 | return Link('+create-question', text, enabled=enabled, icon='add') |
| 195 | |
| 196 | def removequestion(self): |
| 197 | """Remove the created question from this bug.""" |
| 198 | text = 'Convert back to a bug' |
| 199 | - enabled = self.context.bug.getQuestionCreatedFromBug() is not None |
| 200 | + enabled = self._bug_question is not None |
| 201 | return Link('+remove-question', text, enabled=enabled, icon='remove') |
| 202 | |
| 203 | def activitylog(self): |
| 204 | @@ -510,29 +509,36 @@ |
| 205 | else: |
| 206 | return 'subscribed-false %s' % dup_class |
| 207 | |
| 208 | + @cachedproperty |
| 209 | + def _bug_attachments(self): |
| 210 | + """Get a dict of attachment type -> attachments list.""" |
| 211 | + # Note that this is duplicated with get_comments_for_bugtask |
| 212 | + # if you are looking to consolidate things. |
| 213 | + result = {BugAttachmentType.PATCH: [], |
| 214 | + 'other': [] |
| 215 | + } |
| 216 | + for attachment in self.context.attachments_unpopulated: |
| 217 | + info = { |
| 218 | + 'attachment': attachment, |
| 219 | + 'file': ProxiedLibraryFileAlias( |
| 220 | + attachment.libraryfile, attachment), |
| 221 | + } |
| 222 | + if attachment.type == BugAttachmentType.PATCH: |
| 223 | + key = attachment.type |
| 224 | + else: |
| 225 | + key = 'other' |
| 226 | + result[key].append(info) |
| 227 | + return result |
| 228 | + |
| 229 | @property |
| 230 | def regular_attachments(self): |
| 231 | """The list of bug attachments that are not patches.""" |
| 232 | - return [ |
| 233 | - { |
| 234 | - 'attachment': attachment, |
| 235 | - 'file': ProxiedLibraryFileAlias( |
| 236 | - attachment.libraryfile, attachment), |
| 237 | - } |
| 238 | - for attachment in self.context.attachments_unpopulated |
| 239 | - if attachment.type != BugAttachmentType.PATCH] |
| 240 | + return self._bug_attachments['other'] |
| 241 | |
| 242 | @property |
| 243 | def patches(self): |
| 244 | """The list of bug attachments that are patches.""" |
| 245 | - return [ |
| 246 | - { |
| 247 | - 'attachment': attachment, |
| 248 | - 'file': ProxiedLibraryFileAlias( |
| 249 | - attachment.libraryfile, attachment), |
| 250 | - } |
| 251 | - for attachment in self.context.attachments_unpopulated |
| 252 | - if attachment.type == BugAttachmentType.PATCH] |
| 253 | + return self._bug_attachments[BugAttachmentType.PATCH] |
| 254 | |
| 255 | |
| 256 | class BugView(LaunchpadView, BugViewMixin): |
| 257 | |
| 258 | === modified file 'lib/lp/bugs/browser/bugtask.py' |
| 259 | --- lib/lp/bugs/browser/bugtask.py 2010-09-09 17:02:33 +0000 |
| 260 | +++ lib/lp/bugs/browser/bugtask.py 2010-09-23 11:12:49 +0000 |
| 261 | @@ -921,6 +921,9 @@ |
| 262 | This is particularly useful for views that may render a |
| 263 | NullBugTask. |
| 264 | """ |
| 265 | + if self.context.id is not None: |
| 266 | + # Fast path for real bugtasks: they have a DB id. |
| 267 | + return True |
| 268 | params = BugTaskSearchParams(user=self.user, bug=self.context.bug) |
| 269 | matching_bugtasks = self.context.target.searchTasks(params) |
| 270 | if self.context.productseries is not None: |
| 271 | @@ -1194,14 +1197,14 @@ |
| 272 | @property |
| 273 | def official_tags(self): |
| 274 | """The list of official tags for this bug.""" |
| 275 | - target_official_tags = self.context.target.official_bug_tags |
| 276 | + target_official_tags = set(self.context.bug.official_tags) |
| 277 | return [tag for tag in self.context.bug.tags |
| 278 | if tag in target_official_tags] |
| 279 | |
| 280 | @property |
| 281 | def unofficial_tags(self): |
| 282 | """The list of unofficial tags for this bug.""" |
| 283 | - target_official_tags = self.context.target.official_bug_tags |
| 284 | + target_official_tags = set(self.context.bug.official_tags) |
| 285 | return [tag for tag in self.context.bug.tags |
| 286 | if tag not in target_official_tags] |
| 287 | |
| 288 | @@ -1213,11 +1216,10 @@ |
| 289 | bug has a task. It is returned as Javascript snippet, to be embedded |
| 290 | in the bug page. |
| 291 | """ |
| 292 | - available_tags = set() |
| 293 | - for task in self.context.bug.bugtasks: |
| 294 | - available_tags.update(task.target.official_bug_tags) |
| 295 | - return 'var available_official_tags = %s;' % dumps(list(sorted( |
| 296 | - available_tags))) |
| 297 | + # Unwrap the security proxy. - official_tags is a security proxy |
| 298 | + # wrapped list. |
| 299 | + available_tags = list(self.context.bug.official_tags) |
| 300 | + return 'var available_official_tags = %s;' % dumps(available_tags) |
| 301 | |
| 302 | @property |
| 303 | def user_is_admin(self): |
| 304 | @@ -3247,6 +3249,7 @@ |
| 305 | """Cache the list of bugtasks and set up the release mapping.""" |
| 306 | # Cache some values, so that we don't have to recalculate them |
| 307 | # for each bug task. |
| 308 | + # This query is redundant: the publisher also queries all the bugtasks. |
| 309 | self.bugtasks = list(self.context.bugtasks) |
| 310 | self.many_bugtasks = len(self.bugtasks) >= 10 |
| 311 | self.cached_milestone_source = CachedMilestoneSourceFactory() |
| 312 | @@ -3338,7 +3341,6 @@ |
| 313 | |
| 314 | upstream_tasks.sort(key=_by_targetname) |
| 315 | distro_tasks.sort(key=_by_targetname) |
| 316 | - |
| 317 | all_bugtasks = upstream_tasks + distro_tasks |
| 318 | |
| 319 | # Cache whether the bug was converted to a question, since |
| 320 | @@ -3402,7 +3404,7 @@ |
| 321 | # Hide the links when the bug is viewed in a CVE context |
| 322 | return self.request.getNearest(ICveSet) == (None, None) |
| 323 | |
| 324 | - @property |
| 325 | + @cachedproperty |
| 326 | def current_user_affected_status(self): |
| 327 | """Is the current user marked as affected by this bug?""" |
| 328 | return self.context.isUserAffected(self.user) |
| 329 | @@ -3623,10 +3625,11 @@ |
| 330 | |
| 331 | return items |
| 332 | |
| 333 | - @property |
| 334 | + @cachedproperty |
| 335 | def target_has_milestones(self): |
| 336 | """Are there any milestones we can target?""" |
| 337 | - return list(MilestoneVocabulary(self.context)) != [] |
| 338 | + return MilestoneVocabulary.getMilestoneTarget( |
| 339 | + self.context).has_milestones |
| 340 | |
| 341 | def bugtask_canonical_url(self): |
| 342 | """Return the canonical url for the bugtask.""" |
| 343 | |
| 344 | === modified file 'lib/lp/bugs/browser/configure.zcml' |
| 345 | --- lib/lp/bugs/browser/configure.zcml 2010-09-06 15:14:17 +0000 |
| 346 | +++ lib/lp/bugs/browser/configure.zcml 2010-09-23 11:12:49 +0000 |
| 347 | @@ -531,6 +531,11 @@ |
| 348 | permission="launchpad.View" |
| 349 | name="+activity" |
| 350 | template="../templates/bug-activity.pt"/> |
| 351 | + <browser:url |
| 352 | + for="lp.bugs.interfaces.bugactivity.IBugActivity" |
| 353 | + path_expression="string:activity" |
| 354 | + attribute_to_parent="bug" |
| 355 | + rootsite="bugs"/> |
| 356 | <browser:page |
| 357 | for="lp.bugs.interfaces.bugtask.IBugTask" |
| 358 | class="lp.bugs.browser.bugtask.BugTaskTableRowView" |
| 359 | |
| 360 | === modified file 'lib/lp/bugs/browser/tests/test_bugtask.py' |
| 361 | --- lib/lp/bugs/browser/tests/test_bugtask.py 2010-09-01 01:57:37 +0000 |
| 362 | +++ lib/lp/bugs/browser/tests/test_bugtask.py 2010-09-23 11:12:49 +0000 |
| 363 | @@ -28,6 +28,7 @@ |
| 364 | setUp, |
| 365 | tearDown, |
| 366 | ) |
| 367 | +from canonical.launchpad.webapp import canonical_url |
| 368 | from canonical.launchpad.webapp.servers import LaunchpadTestRequest |
| 369 | from canonical.testing import LaunchpadFunctionalLayer |
| 370 | from lp.bugs.browser import bugtask |
| 371 | @@ -37,11 +38,8 @@ |
| 372 | BugTasksAndNominationsView, |
| 373 | ) |
| 374 | from lp.bugs.interfaces.bugtask import BugTaskStatus |
| 375 | -from lp.testing import ( |
| 376 | - person_logged_in, |
| 377 | - StormStatementRecorder, |
| 378 | - TestCaseWithFactory, |
| 379 | - ) |
| 380 | +from lp.testing import TestCaseWithFactory |
| 381 | +from lp.testing._webservice import QueryCollector |
| 382 | from lp.testing.matchers import HasQueryCount |
| 383 | from lp.testing.sampledata import ( |
| 384 | ADMIN_EMAIL, |
| 385 | @@ -54,16 +52,6 @@ |
| 386 | |
| 387 | layer = LaunchpadFunctionalLayer |
| 388 | |
| 389 | - def record_view_initialization(self, bugtask, person): |
| 390 | - self.invalidate_caches(bugtask) |
| 391 | - # Login first because logging in triggers queries. |
| 392 | - with nested(person_logged_in(person), StormStatementRecorder()) as ( |
| 393 | - _, |
| 394 | - recorder): |
| 395 | - view = BugTaskView(bugtask, LaunchpadTestRequest()) |
| 396 | - view.initialize() |
| 397 | - return recorder |
| 398 | - |
| 399 | def invalidate_caches(self, obj): |
| 400 | store = Store.of(obj) |
| 401 | # Make sure everything is in the database. |
| 402 | @@ -72,24 +60,31 @@ |
| 403 | # the domain objects) |
| 404 | store.invalidate() |
| 405 | |
| 406 | - def test_query_counts_constant_with_team_memberships(self): |
| 407 | + def test_rendered_query_counts_constant_with_team_memberships(self): |
| 408 | login(ADMIN_EMAIL) |
| 409 | bugtask = self.factory.makeBugTask() |
| 410 | - person_no_teams = self.factory.makePerson() |
| 411 | - person_with_teams = self.factory.makePerson() |
| 412 | + person_no_teams = self.factory.makePerson(password='test') |
| 413 | + person_with_teams = self.factory.makePerson(password='test') |
| 414 | for _ in range(10): |
| 415 | self.factory.makeTeam(members=[person_with_teams]) |
| 416 | # count with no teams |
| 417 | - recorder = self.record_view_initialization(bugtask, person_no_teams) |
| 418 | - self.assertThat(recorder, HasQueryCount(LessThan(14))) |
| 419 | + url = canonical_url(bugtask) |
| 420 | + recorder = QueryCollector() |
| 421 | + recorder.register() |
| 422 | + self.addCleanup(recorder.unregister) |
| 423 | + self.invalidate_caches(bugtask) |
| 424 | + self.getUserBrowser(url, person_no_teams) |
| 425 | + # This may seem large: it is; there is easily another 30% fat in there. |
| 426 | + self.assertThat(recorder, HasQueryCount(LessThan(64))) |
| 427 | count_with_no_teams = recorder.count |
| 428 | # count with many teams |
| 429 | - recorder2 = self.record_view_initialization(bugtask, person_with_teams) |
| 430 | + self.invalidate_caches(bugtask) |
| 431 | + self.getUserBrowser(url, person_with_teams) |
| 432 | # Allow an increase of one because storm bug 619017 causes additional |
| 433 | # queries, revalidating things unnecessarily. An increase which is |
| 434 | # less than the number of new teams shows it is definitely not |
| 435 | # growing per-team. |
| 436 | - self.assertThat(recorder2, HasQueryCount( |
| 437 | + self.assertThat(recorder, HasQueryCount( |
| 438 | LessThan(count_with_no_teams + 3), |
| 439 | )) |
| 440 | |
| 441 | @@ -105,23 +100,32 @@ |
| 442 | self.view = BugTasksAndNominationsView( |
| 443 | self.bug, LaunchpadTestRequest()) |
| 444 | |
| 445 | + def refresh(self): |
| 446 | + # The view caches, to see different scenarios, a refresh is needed. |
| 447 | + self.view = BugTasksAndNominationsView( |
| 448 | + self.bug, LaunchpadTestRequest()) |
| 449 | + |
| 450 | def test_current_user_affected_status(self): |
| 451 | self.failUnlessEqual( |
| 452 | None, self.view.current_user_affected_status) |
| 453 | - self.view.context.markUserAffected(self.view.user, True) |
| 454 | + self.bug.markUserAffected(self.view.user, True) |
| 455 | + self.refresh() |
| 456 | self.failUnlessEqual( |
| 457 | True, self.view.current_user_affected_status) |
| 458 | - self.view.context.markUserAffected(self.view.user, False) |
| 459 | + self.bug.markUserAffected(self.view.user, False) |
| 460 | + self.refresh() |
| 461 | self.failUnlessEqual( |
| 462 | False, self.view.current_user_affected_status) |
| 463 | |
| 464 | def test_current_user_affected_js_status(self): |
| 465 | self.failUnlessEqual( |
| 466 | 'null', self.view.current_user_affected_js_status) |
| 467 | - self.view.context.markUserAffected(self.view.user, True) |
| 468 | + self.bug.markUserAffected(self.view.user, True) |
| 469 | + self.refresh() |
| 470 | self.failUnlessEqual( |
| 471 | 'true', self.view.current_user_affected_js_status) |
| 472 | - self.view.context.markUserAffected(self.view.user, False) |
| 473 | + self.bug.markUserAffected(self.view.user, False) |
| 474 | + self.refresh() |
| 475 | self.failUnlessEqual( |
| 476 | 'false', self.view.current_user_affected_js_status) |
| 477 | |
| 478 | @@ -148,10 +152,12 @@ |
| 479 | # logged-in user marked him or herself as affected or not. |
| 480 | self.failUnlessEqual( |
| 481 | 1, self.view.other_users_affected_count) |
| 482 | - self.view.context.markUserAffected(self.view.user, True) |
| 483 | + self.bug.markUserAffected(self.view.user, True) |
| 484 | + self.refresh() |
| 485 | self.failUnlessEqual( |
| 486 | 1, self.view.other_users_affected_count) |
| 487 | - self.view.context.markUserAffected(self.view.user, False) |
| 488 | + self.bug.markUserAffected(self.view.user, False) |
| 489 | + self.refresh() |
| 490 | self.failUnlessEqual( |
| 491 | 1, self.view.other_users_affected_count) |
| 492 | |
| 493 | @@ -161,17 +167,18 @@ |
| 494 | self.failUnlessEqual( |
| 495 | 1, self.view.other_users_affected_count) |
| 496 | other_user_1 = self.factory.makePerson() |
| 497 | - self.view.context.markUserAffected(other_user_1, True) |
| 498 | + self.bug.markUserAffected(other_user_1, True) |
| 499 | self.failUnlessEqual( |
| 500 | 2, self.view.other_users_affected_count) |
| 501 | other_user_2 = self.factory.makePerson() |
| 502 | - self.view.context.markUserAffected(other_user_2, True) |
| 503 | + self.bug.markUserAffected(other_user_2, True) |
| 504 | self.failUnlessEqual( |
| 505 | 3, self.view.other_users_affected_count) |
| 506 | - self.view.context.markUserAffected(other_user_1, False) |
| 507 | + self.bug.markUserAffected(other_user_1, False) |
| 508 | self.failUnlessEqual( |
| 509 | 2, self.view.other_users_affected_count) |
| 510 | - self.view.context.markUserAffected(self.view.user, True) |
| 511 | + self.bug.markUserAffected(self.view.user, True) |
| 512 | + self.refresh() |
| 513 | self.failUnlessEqual( |
| 514 | 2, self.view.other_users_affected_count) |
| 515 | |
| 516 | |
| 517 | === modified file 'lib/lp/bugs/configure.zcml' |
| 518 | --- lib/lp/bugs/configure.zcml 2010-09-10 16:21:21 +0000 |
| 519 | +++ lib/lp/bugs/configure.zcml 2010-09-23 11:12:49 +0000 |
| 520 | @@ -191,6 +191,7 @@ |
| 521 | date_fix_released |
| 522 | date_left_closed |
| 523 | date_closed |
| 524 | + productseriesID |
| 525 | task_age |
| 526 | bug_subscribers |
| 527 | is_complete |
| 528 | @@ -592,6 +593,7 @@ |
| 529 | isMentor |
| 530 | getNullBugTask |
| 531 | is_complete |
| 532 | + official_tags |
| 533 | who_made_private |
| 534 | date_made_private |
| 535 | userCanView |
| 536 | @@ -624,6 +626,7 @@ |
| 537 | initial_message |
| 538 | linked_branches |
| 539 | watches |
| 540 | + has_cves |
| 541 | cves |
| 542 | cve_links |
| 543 | duplicates |
| 544 | |
| 545 | === modified file 'lib/lp/bugs/doc/bugsubscription.txt' |
| 546 | --- lib/lp/bugs/doc/bugsubscription.txt 2010-09-09 21:00:54 +0000 |
| 547 | +++ lib/lp/bugs/doc/bugsubscription.txt 2010-09-23 11:12:49 +0000 |
| 548 | @@ -49,7 +49,7 @@ |
| 549 | >>> personset = getUtility(IPersonSet) |
| 550 | |
| 551 | >>> linux_source = ubuntu.getSourcePackage("linux-source-2.6.15") |
| 552 | - >>> linux_source.bug_subscriptions |
| 553 | + >>> list(linux_source.bug_subscriptions) |
| 554 | [] |
| 555 | >>> print linux_source.distribution.bug_supervisor |
| 556 | None |
| 557 | @@ -781,7 +781,7 @@ |
| 558 | contacts: |
| 559 | |
| 560 | >>> evolution = ubuntu.getSourcePackage("evolution") |
| 561 | - >>> evolution.bug_subscriptions |
| 562 | + >>> list(evolution.bug_subscriptions) |
| 563 | [] |
| 564 | |
| 565 | >>> params = CreateBugParams( |
| 566 | |
| 567 | === modified file 'lib/lp/bugs/doc/initial-bug-contacts.txt' |
| 568 | --- lib/lp/bugs/doc/initial-bug-contacts.txt 2010-08-20 01:29:08 +0000 |
| 569 | +++ lib/lp/bugs/doc/initial-bug-contacts.txt 2010-09-23 11:12:49 +0000 |
| 570 | @@ -16,7 +16,7 @@ |
| 571 | >>> debian = getUtility(IDistributionSet).getByName("debian") |
| 572 | >>> debian_firefox = debian.getSourcePackage("mozilla-firefox") |
| 573 | |
| 574 | - >>> debian_firefox.bug_subscriptions |
| 575 | + >>> list(debian_firefox.bug_subscriptions) |
| 576 | [] |
| 577 | |
| 578 | Adding a package subscription is done with the |
| 579 | |
| 580 | === modified file 'lib/lp/bugs/interfaces/bug.py' |
| 581 | --- lib/lp/bugs/interfaces/bug.py 2010-08-30 23:50:41 +0000 |
| 582 | +++ lib/lp/bugs/interfaces/bug.py 2010-09-23 11:12:49 +0000 |
| 583 | @@ -70,6 +70,7 @@ |
| 584 | ) |
| 585 | from canonical.launchpad.validators.name import name_validator |
| 586 | from lp.app.errors import NotFoundError |
| 587 | +from lp.bugs.interfaces.bugactivity import IBugActivity |
| 588 | from lp.bugs.interfaces.bugattachment import IBugAttachment |
| 589 | from lp.bugs.interfaces.bugbranch import IBugBranch |
| 590 | from lp.bugs.interfaces.bugtask import ( |
| 591 | @@ -243,7 +244,11 @@ |
| 592 | required=False, default=False, readonly=True)) |
| 593 | displayname = TextLine(title=_("Text of the form 'Bug #X"), |
| 594 | readonly=True) |
| 595 | - activity = Attribute('SQLObject.Multijoin of IBugActivity') |
| 596 | + activity = exported( |
| 597 | + CollectionField( |
| 598 | + title=_('Log of activity that has occurred on this bug.'), |
| 599 | + value_type=Reference(schema=IBugActivity), |
| 600 | + readonly=True)) |
| 601 | initial_message = Attribute( |
| 602 | "The message that was specified when creating the bug") |
| 603 | bugtasks = exported( |
| 604 | @@ -269,6 +274,7 @@ |
| 605 | title=_('CVE entries related to this bug.'), |
| 606 | value_type=Reference(schema=ICve), |
| 607 | readonly=True)) |
| 608 | + has_cves = Bool(title=u"True if the bug has cve entries.") |
| 609 | cve_links = Attribute('Links between this bug and CVE entries.') |
| 610 | subscriptions = exported( |
| 611 | doNotSnapshot(CollectionField( |
| 612 | @@ -406,6 +412,8 @@ |
| 613 | |
| 614 | latest_patch = Attribute("The most recent patch of this bug.") |
| 615 | |
| 616 | + official_tags = Attribute("The official bug tags relevant to this bug.") |
| 617 | + |
| 618 | @operation_parameters( |
| 619 | subject=optional_message_subject_field(), |
| 620 | content=copy_field(IMessage['content'])) |
| 621 | |
| 622 | === modified file 'lib/lp/bugs/interfaces/bugactivity.py' |
| 623 | --- lib/lp/bugs/interfaces/bugactivity.py 2010-08-20 20:31:18 +0000 |
| 624 | +++ lib/lp/bugs/interfaces/bugactivity.py 2010-09-23 11:12:49 +0000 |
| 625 | @@ -15,24 +15,59 @@ |
| 626 | from zope.interface import Interface |
| 627 | from zope.schema import ( |
| 628 | Datetime, |
| 629 | - Int, |
| 630 | Text, |
| 631 | TextLine, |
| 632 | ) |
| 633 | |
| 634 | +from lazr.restful.declarations import ( |
| 635 | + export_as_webservice_entry, |
| 636 | + exported, |
| 637 | + ) |
| 638 | + |
| 639 | +from lp.services.fields import ( |
| 640 | + BugField, |
| 641 | + PersonChoice, |
| 642 | + ) |
| 643 | + |
| 644 | from canonical.launchpad import _ |
| 645 | |
| 646 | |
| 647 | class IBugActivity(Interface): |
| 648 | """A log of all things that have happened to a bug.""" |
| 649 | - |
| 650 | - bug = Int(title=_('Bug ID')) |
| 651 | - datechanged = Datetime(title=_('Date Changed')) |
| 652 | - person = Int(title=_('Person')) |
| 653 | - whatchanged = TextLine(title=_('What Changed')) |
| 654 | - oldvalue = TextLine(title=_('Old Value')) |
| 655 | - newvalue = TextLine(title=_('New Value')) |
| 656 | - message = Text(title=_('Message')) |
| 657 | + export_as_webservice_entry() |
| 658 | + |
| 659 | + bug = exported( |
| 660 | + BugField(title=_('Bug'), readonly=True)) |
| 661 | + |
| 662 | + datechanged = exported( |
| 663 | + Datetime(title=_('Date Changed'), |
| 664 | + description=_("The date on which this activity occurred."), |
| 665 | + readonly=True)) |
| 666 | + |
| 667 | + person = exported(PersonChoice( |
| 668 | + title=_('Person'), required=True, vocabulary='ValidPersonOrTeam', |
| 669 | + readonly=True, description=_("The person's Launchpad ID or " |
| 670 | + "e-mail address."))) |
| 671 | + |
| 672 | + whatchanged = exported( |
| 673 | + TextLine(title=_('What Changed'), |
| 674 | + description=_("The property of the bug that changed."), |
| 675 | + readonly=True)) |
| 676 | + |
| 677 | + oldvalue = exported( |
| 678 | + TextLine(title=_('Old Value'), |
| 679 | + description=_("The value before the change."), |
| 680 | + readonly=True)) |
| 681 | + |
| 682 | + newvalue = exported( |
| 683 | + TextLine(title=_('New Value'), |
| 684 | + description=_("The value after the change."), |
| 685 | + readonly=True)) |
| 686 | + |
| 687 | + message = exported( |
| 688 | + Text(title=_('Message'), |
| 689 | + description=_("Additional information about what changed."), |
| 690 | + readonly=True)) |
| 691 | |
| 692 | |
| 693 | class IBugActivitySet(Interface): |
| 694 | |
| 695 | === modified file 'lib/lp/bugs/interfaces/bugtarget.py' |
| 696 | --- lib/lp/bugs/interfaces/bugtarget.py 2010-08-26 20:22:48 +0000 |
| 697 | +++ lib/lp/bugs/interfaces/bugtarget.py 2010-09-23 11:12:49 +0000 |
| 698 | @@ -363,6 +363,9 @@ |
| 699 | bugs will be returned. |
| 700 | """ |
| 701 | |
| 702 | + def _getOfficialTagClause(): |
| 703 | + """Get the storm clause for finding this targets tags.""" |
| 704 | + |
| 705 | |
| 706 | class IOfficialBugTagTargetPublic(IHasOfficialBugTags): |
| 707 | """Public attributes for `IOfficialBugTagTarget`.""" |
| 708 | |
| 709 | === modified file 'lib/lp/bugs/interfaces/bugtask.py' |
| 710 | --- lib/lp/bugs/interfaces/bugtask.py 2010-08-30 19:30:45 +0000 |
| 711 | +++ lib/lp/bugs/interfaces/bugtask.py 2010-09-23 11:12:49 +0000 |
| 712 | @@ -454,6 +454,7 @@ |
| 713 | title=_('Project'), required=False, vocabulary='Product') |
| 714 | productseries = Choice( |
| 715 | title=_('Series'), required=False, vocabulary='ProductSeries') |
| 716 | + productseriesID = Attribute('The product series ID') |
| 717 | sourcepackagename = Choice( |
| 718 | title=_("Package"), required=False, |
| 719 | vocabulary='SourcePackageName') |
| 720 | |
| 721 | === modified file 'lib/lp/bugs/model/bug.py' |
| 722 | --- lib/lp/bugs/model/bug.py 2010-09-15 23:40:13 +0000 |
| 723 | +++ lib/lp/bugs/model/bug.py 2010-09-23 11:12:49 +0000 |
| 724 | @@ -70,6 +70,7 @@ |
| 725 | implements, |
| 726 | providedBy, |
| 727 | ) |
| 728 | +from zope.security.proxy import removeSecurityProxy |
| 729 | |
| 730 | from canonical.config import config |
| 731 | from canonical.database.constants import UTC_NOW |
| 732 | @@ -161,6 +162,7 @@ |
| 733 | get_bug_privacy_filter, |
| 734 | NullBugTask, |
| 735 | ) |
| 736 | +from lp.bugs.model.bugtarget import OfficialBugTag |
| 737 | from lp.bugs.model.bugwatch import BugWatch |
| 738 | from lp.hardwaredb.interfaces.hwdb import IHWSubmissionBugSet |
| 739 | from lp.registry.enum import BugNotificationLevel |
| 740 | @@ -191,6 +193,7 @@ |
| 741 | from lp.services.propertycache import ( |
| 742 | cachedproperty, |
| 743 | IPropertyCache, |
| 744 | + IPropertyCacheManager, |
| 745 | ) |
| 746 | |
| 747 | |
| 748 | @@ -348,6 +351,21 @@ |
| 749 | heat_last_updated = UtcDateTimeCol(default=None) |
| 750 | latest_patch_uploaded = UtcDateTimeCol(default=None) |
| 751 | |
| 752 | + @cachedproperty |
| 753 | + def _subscriber_cache(self): |
| 754 | + """Caches known subscribers.""" |
| 755 | + return set() |
| 756 | + |
| 757 | + @cachedproperty |
| 758 | + def _subscriber_dups_cache(self): |
| 759 | + """Caches known subscribers to dupes.""" |
| 760 | + return set() |
| 761 | + |
| 762 | + @cachedproperty |
| 763 | + def _unsubscribed_cache(self): |
| 764 | + """Cache known non-subscribers.""" |
| 765 | + return set() |
| 766 | + |
| 767 | @property |
| 768 | def latest_patch(self): |
| 769 | """See `IBug`.""" |
| 770 | @@ -531,7 +549,7 @@ |
| 771 | dn += ' ('+self.name+')' |
| 772 | return dn |
| 773 | |
| 774 | - @property |
| 775 | + @cachedproperty |
| 776 | def bugtasks(self): |
| 777 | """See `IBug`.""" |
| 778 | result = BugTask.select('BugTask.bug = %s' % sqlvalues(self.id)) |
| 779 | @@ -541,7 +559,8 @@ |
| 780 | # Do not use the default orderBy as the prejoins cause ambiguities |
| 781 | # across the tables. |
| 782 | result = result.orderBy("id") |
| 783 | - return sorted(result, key=bugtask_sort_key) |
| 784 | + result = sorted(result, key=bugtask_sort_key) |
| 785 | + return result |
| 786 | |
| 787 | @property |
| 788 | def default_bugtask(self): |
| 789 | @@ -665,6 +684,33 @@ |
| 790 | BugMessage.message == Message.id).order_by('id') |
| 791 | return messages.first() |
| 792 | |
| 793 | + @cachedproperty |
| 794 | + def official_tags(self): |
| 795 | + """See `IBug`.""" |
| 796 | + # Da circle of imports forces the locals. |
| 797 | + from lp.registry.model.distribution import Distribution |
| 798 | + from lp.registry.model.product import Product |
| 799 | + table = OfficialBugTag |
| 800 | + table = LeftJoin( |
| 801 | + table, |
| 802 | + Distribution, |
| 803 | + OfficialBugTag.distribution_id==Distribution.id) |
| 804 | + table = LeftJoin( |
| 805 | + table, |
| 806 | + Product, |
| 807 | + OfficialBugTag.product_id==Product.id) |
| 808 | + # When this method is typically called it already has the necessary |
| 809 | + # info in memory, so rather than rejoin with Product etc, we do this |
| 810 | + # bit in Python. If reviewing performance here feel free to change. |
| 811 | + clauses = [] |
| 812 | + for task in self.bugtasks: |
| 813 | + clauses.append( |
| 814 | + # Storm cannot compile proxied objects. |
| 815 | + removeSecurityProxy(task.target._getOfficialTagClause())) |
| 816 | + clause = Or(*clauses) |
| 817 | + return list(Store.of(self).using(table).find(OfficialBugTag.tag, |
| 818 | + clause).order_by(OfficialBugTag.tag).config(distinct=True)) |
| 819 | + |
| 820 | def followup_subject(self): |
| 821 | """See `IBug`.""" |
| 822 | return 'Re: '+ self.title |
| 823 | @@ -698,6 +744,8 @@ |
| 824 | |
| 825 | def unsubscribe(self, person, unsubscribed_by): |
| 826 | """See `IBug`.""" |
| 827 | + # Drop cached subscription info. |
| 828 | + IPropertyCacheManager(self).clear() |
| 829 | if person is None: |
| 830 | person = unsubscribed_by |
| 831 | |
| 832 | @@ -737,21 +785,11 @@ |
| 833 | |
| 834 | def isSubscribed(self, person): |
| 835 | """See `IBug`.""" |
| 836 | - if person is None: |
| 837 | - return False |
| 838 | - |
| 839 | - bs = BugSubscription.selectBy(bug=self, person=person) |
| 840 | - return bool(bs) |
| 841 | + return self.personIsDirectSubscriber(person) |
| 842 | |
| 843 | def isSubscribedToDupes(self, person): |
| 844 | """See `IBug`.""" |
| 845 | - if person is None: |
| 846 | - return False |
| 847 | - |
| 848 | - return bool( |
| 849 | - BugSubscription.select(""" |
| 850 | - bug IN (SELECT id FROM Bug WHERE duplicateof = %d) AND |
| 851 | - person = %d""" % (self.id, person.id))) |
| 852 | + return self.personIsSubscribedToDuplicate(person) |
| 853 | |
| 854 | def getDirectSubscriptions(self): |
| 855 | """See `IBug`.""" |
| 856 | @@ -868,9 +906,23 @@ |
| 857 | def getSubscribersForPerson(self, person): |
| 858 | """See `IBug.""" |
| 859 | assert person is not None |
| 860 | - return Store.of(self).find( |
| 861 | - # return people |
| 862 | - Person, |
| 863 | + def cache_unsubscribed(rows): |
| 864 | + if not rows: |
| 865 | + self._unsubscribed_cache.add(person) |
| 866 | + def cache_subscriber(row): |
| 867 | + _, subscriber, subscription = row |
| 868 | + if subscription.bugID == self.id: |
| 869 | + self._subscriber_cache.add(subscriber) |
| 870 | + else: |
| 871 | + self._subscriber_dups_cache.add(subscriber) |
| 872 | + return subscriber |
| 873 | + return DecoratedResultSet(Store.of(self).find( |
| 874 | + # XXX: RobertCollins 2010-09-22 bug=374777: This SQL(...) is a |
| 875 | + # hack; it does not seem to be possible to express DISTINCT ON |
| 876 | + # with Storm. |
| 877 | + (SQL("DISTINCT ON (Person.name, BugSubscription.person) 0 AS ignore"), |
| 878 | + # return people and subscribptions |
| 879 | + Person, BugSubscription), |
| 880 | # For this bug or its duplicates |
| 881 | Or( |
| 882 | Bug.id == self.id, |
| 883 | @@ -890,7 +942,8 @@ |
| 884 | # bug=https://bugs.edge.launchpad.net/storm/+bug/627137 |
| 885 | # RBC 20100831 |
| 886 | SQL("""Person.id = TeamParticipation.team"""), |
| 887 | - ).order_by(Person.name).config(distinct=True) |
| 888 | + ).order_by(Person.name), |
| 889 | + cache_subscriber, pre_iter_hook=cache_unsubscribed) |
| 890 | |
| 891 | def getAlsoNotifiedSubscribers(self, recipients=None, level=None): |
| 892 | """See `IBug`. |
| 893 | @@ -1216,6 +1269,11 @@ |
| 894 | notify(ObjectDeletedEvent(bug_branch, user=user)) |
| 895 | bug_branch.destroySelf() |
| 896 | |
| 897 | + @cachedproperty |
| 898 | + def has_cves(self): |
| 899 | + """See `IBug`.""" |
| 900 | + return bool(self.cves) |
| 901 | + |
| 902 | def linkCVE(self, cve, user): |
| 903 | """See `IBug`.""" |
| 904 | if cve not in self.cves: |
| 905 | @@ -1314,18 +1372,23 @@ |
| 906 | question_target = IQuestionTarget(bugtask.target) |
| 907 | question = question_target.createQuestionFromBug(self) |
| 908 | self.addChange(BugConvertedToQuestion(UTC_NOW, person, question)) |
| 909 | + IPropertyCache(self)._question_from_bug = question |
| 910 | |
| 911 | notify(BugBecameQuestionEvent(self, question, person)) |
| 912 | return question |
| 913 | |
| 914 | - def getQuestionCreatedFromBug(self): |
| 915 | - """See `IBug`.""" |
| 916 | + @cachedproperty |
| 917 | + def _question_from_bug(self): |
| 918 | for question in self.questions: |
| 919 | - if (question.owner == self.owner |
| 920 | + if (question.ownerID == self.ownerID |
| 921 | and question.datecreated == self.datecreated): |
| 922 | return question |
| 923 | return None |
| 924 | |
| 925 | + def getQuestionCreatedFromBug(self): |
| 926 | + """See `IBug`.""" |
| 927 | + return self._question_from_bug |
| 928 | + |
| 929 | def canMentor(self, user): |
| 930 | """See `ICanBeMentored`.""" |
| 931 | if user is None: |
| 932 | @@ -1624,10 +1687,13 @@ |
| 933 | |
| 934 | def _getTags(self): |
| 935 | """Get the tags as a sorted list of strings.""" |
| 936 | - tags = [ |
| 937 | - bugtag.tag |
| 938 | - for bugtag in BugTag.selectBy(bug=self, orderBy='tag')] |
| 939 | - return tags |
| 940 | + return self._cached_tags |
| 941 | + |
| 942 | + @cachedproperty |
| 943 | + def _cached_tags(self): |
| 944 | + return list(Store.of(self).find( |
| 945 | + BugTag.tag, |
| 946 | + BugTag.bugID==self.id).order_by(BugTag.tag)) |
| 947 | |
| 948 | def _setTags(self, tags): |
| 949 | """Set the tags from a list of strings.""" |
| 950 | @@ -1635,6 +1701,7 @@ |
| 951 | # and insert the new ones. |
| 952 | new_tags = set([tag.lower() for tag in tags]) |
| 953 | old_tags = set(self.tags) |
| 954 | + del IPropertyCache(self)._cached_tags |
| 955 | added_tags = new_tags.difference(old_tags) |
| 956 | removed_tags = old_tags.difference(new_tags) |
| 957 | for removed_tag in removed_tags: |
| 958 | @@ -1782,12 +1849,17 @@ |
| 959 | |
| 960 | def personIsDirectSubscriber(self, person): |
| 961 | """See `IBug`.""" |
| 962 | + if person in self._subscriber_cache: |
| 963 | + return True |
| 964 | + if person in self._unsubscribed_cache: |
| 965 | + return False |
| 966 | + if person is None: |
| 967 | + return False |
| 968 | store = Store.of(self) |
| 969 | subscriptions = store.find( |
| 970 | BugSubscription, |
| 971 | BugSubscription.bug == self, |
| 972 | BugSubscription.person == person) |
| 973 | - |
| 974 | return not subscriptions.is_empty() |
| 975 | |
| 976 | def personIsAlsoNotifiedSubscriber(self, person): |
| 977 | @@ -1803,6 +1875,12 @@ |
| 978 | |
| 979 | def personIsSubscribedToDuplicate(self, person): |
| 980 | """See `IBug`.""" |
| 981 | + if person in self._subscriber_dups_cache: |
| 982 | + return True |
| 983 | + if person in self._unsubscribed_cache: |
| 984 | + return False |
| 985 | + if person is None: |
| 986 | + return False |
| 987 | store = Store.of(self) |
| 988 | subscriptions_from_dupes = store.find( |
| 989 | BugSubscription, |
| 990 | @@ -2012,13 +2090,13 @@ |
| 991 | |
| 992 | # Create the task on a product if one was passed. |
| 993 | if params.product: |
| 994 | - BugTaskSet().createTask( |
| 995 | + getUtility(IBugTaskSet).createTask( |
| 996 | bug=bug, product=params.product, owner=params.owner, |
| 997 | status=params.status) |
| 998 | |
| 999 | # Create the task on a source package name if one was passed. |
| 1000 | if params.distribution: |
| 1001 | - BugTaskSet().createTask( |
| 1002 | + getUtility(IBugTaskSet).createTask( |
| 1003 | bug=bug, distribution=params.distribution, |
| 1004 | sourcepackagename=params.sourcepackagename, |
| 1005 | owner=params.owner, status=params.status) |
| 1006 | |
| 1007 | === added file 'lib/lp/bugs/model/bugsubscriptionfilter.py' |
| 1008 | --- lib/lp/bugs/model/bugsubscriptionfilter.py 1970-01-01 00:00:00 +0000 |
| 1009 | +++ lib/lp/bugs/model/bugsubscriptionfilter.py 2010-09-23 11:12:49 +0000 |
| 1010 | @@ -0,0 +1,36 @@ |
| 1011 | +# Copyright 2009 Canonical Ltd. This software is licensed under the |
| 1012 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
| 1013 | + |
| 1014 | +# pylint: disable-msg=E0611,W0212 |
| 1015 | + |
| 1016 | +__metaclass__ = type |
| 1017 | +__all__ = ['BugSubscriptionFilter'] |
| 1018 | + |
| 1019 | +from storm.base import Storm |
| 1020 | +from storm.locals import ( |
| 1021 | + Bool, |
| 1022 | + Int, |
| 1023 | + Reference, |
| 1024 | + Unicode, |
| 1025 | + ) |
| 1026 | + |
| 1027 | + |
| 1028 | +class BugSubscriptionFilter(Storm): |
| 1029 | + """A filter to specialize a *structural* subscription.""" |
| 1030 | + |
| 1031 | + __storm_table__ = "BugSubscriptionFilter" |
| 1032 | + |
| 1033 | + id = Int(primary=True) |
| 1034 | + |
| 1035 | + structural_subscription_id = Int( |
| 1036 | + "structuralsubscription", allow_none=False) |
| 1037 | + structural_subscription = Reference( |
| 1038 | + structural_subscription_id, "StructuralSubscription.id") |
| 1039 | + |
| 1040 | + find_all_tags = Bool(allow_none=False, default=False) |
| 1041 | + include_any_tags = Bool(allow_none=False, default=False) |
| 1042 | + exclude_any_tags = Bool(allow_none=False, default=False) |
| 1043 | + |
| 1044 | + other_parameters = Unicode() |
| 1045 | + |
| 1046 | + description = Unicode() |
| 1047 | |
| 1048 | === added file 'lib/lp/bugs/model/bugsubscriptionfilterimportance.py' |
| 1049 | --- lib/lp/bugs/model/bugsubscriptionfilterimportance.py 1970-01-01 00:00:00 +0000 |
| 1050 | +++ lib/lp/bugs/model/bugsubscriptionfilterimportance.py 2010-09-23 11:12:49 +0000 |
| 1051 | @@ -0,0 +1,29 @@ |
| 1052 | +# Copyright 2009 Canonical Ltd. This software is licensed under the |
| 1053 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
| 1054 | + |
| 1055 | +# pylint: disable-msg=E0611,W0212 |
| 1056 | + |
| 1057 | +__metaclass__ = type |
| 1058 | +__all__ = ['BugSubscriptionFilterImportance'] |
| 1059 | + |
| 1060 | +from storm.base import Storm |
| 1061 | +from storm.locals import ( |
| 1062 | + Int, |
| 1063 | + Reference, |
| 1064 | + ) |
| 1065 | + |
| 1066 | +from canonical.database.enumcol import DBEnum |
| 1067 | +from lp.bugs.interfaces.bugtask import BugTaskImportance |
| 1068 | + |
| 1069 | + |
| 1070 | +class BugSubscriptionFilterImportance(Storm): |
| 1071 | + """Importances to filter.""" |
| 1072 | + |
| 1073 | + __storm_table__ = "BugSubscriptionFilterImportance" |
| 1074 | + |
| 1075 | + id = Int(primary=True) |
| 1076 | + |
| 1077 | + filter_id = Int("filter", allow_none=False) |
| 1078 | + filter = Reference(filter_id, "BugSubscriptionFilter.id") |
| 1079 | + |
| 1080 | + importance = DBEnum(enum=BugTaskImportance, allow_none=False) |
| 1081 | |
| 1082 | === added file 'lib/lp/bugs/model/bugsubscriptionfilterstatus.py' |
| 1083 | --- lib/lp/bugs/model/bugsubscriptionfilterstatus.py 1970-01-01 00:00:00 +0000 |
| 1084 | +++ lib/lp/bugs/model/bugsubscriptionfilterstatus.py 2010-09-23 11:12:49 +0000 |
| 1085 | @@ -0,0 +1,29 @@ |
| 1086 | +# Copyright 2009 Canonical Ltd. This software is licensed under the |
| 1087 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
| 1088 | + |
| 1089 | +# pylint: disable-msg=E0611,W0212 |
| 1090 | + |
| 1091 | +__metaclass__ = type |
| 1092 | +__all__ = ['BugSubscriptionFilterStatus'] |
| 1093 | + |
| 1094 | +from storm.base import Storm |
| 1095 | +from storm.locals import ( |
| 1096 | + Int, |
| 1097 | + Reference, |
| 1098 | + ) |
| 1099 | + |
| 1100 | +from canonical.database.enumcol import DBEnum |
| 1101 | +from lp.bugs.interfaces.bugtask import BugTaskStatus |
| 1102 | + |
| 1103 | + |
| 1104 | +class BugSubscriptionFilterStatus(Storm): |
| 1105 | + """Statuses to filter.""" |
| 1106 | + |
| 1107 | + __storm_table__ = "BugSubscriptionFilterStatus" |
| 1108 | + |
| 1109 | + id = Int(primary=True) |
| 1110 | + |
| 1111 | + filter_id = Int("filter", allow_none=False) |
| 1112 | + filter = Reference(filter_id, "BugSubscriptionFilter.id") |
| 1113 | + |
| 1114 | + status = DBEnum(enum=BugTaskStatus, allow_none=False) |
| 1115 | |
| 1116 | === added file 'lib/lp/bugs/model/bugsubscriptionfiltertag.py' |
| 1117 | --- lib/lp/bugs/model/bugsubscriptionfiltertag.py 1970-01-01 00:00:00 +0000 |
| 1118 | +++ lib/lp/bugs/model/bugsubscriptionfiltertag.py 2010-09-23 11:12:49 +0000 |
| 1119 | @@ -0,0 +1,29 @@ |
| 1120 | +# Copyright 2009 Canonical Ltd. This software is licensed under the |
| 1121 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
| 1122 | + |
| 1123 | +# pylint: disable-msg=E0611,W0212 |
| 1124 | + |
| 1125 | +__metaclass__ = type |
| 1126 | +__all__ = ['BugSubscriptionFilterTag'] |
| 1127 | + |
| 1128 | +from storm.base import Storm |
| 1129 | +from storm.locals import ( |
| 1130 | + Bool, |
| 1131 | + Int, |
| 1132 | + Reference, |
| 1133 | + Unicode, |
| 1134 | + ) |
| 1135 | + |
| 1136 | + |
| 1137 | +class BugSubscriptionFilterTag(Storm): |
| 1138 | + """Tags to filter.""" |
| 1139 | + |
| 1140 | + __storm_table__ = "BugSubscriptionFilterTag" |
| 1141 | + |
| 1142 | + id = Int(primary=True) |
| 1143 | + |
| 1144 | + filter_id = Int("filter", allow_none=False) |
| 1145 | + filter = Reference(filter_id, "BugSubscriptionFilter.id") |
| 1146 | + |
| 1147 | + include = Bool(allow_none=False) |
| 1148 | + tag = Unicode(allow_none=False) |
| 1149 | |
| 1150 | === modified file 'lib/lp/bugs/model/bugtarget.py' |
| 1151 | --- lib/lp/bugs/model/bugtarget.py 2010-09-02 22:08:12 +0000 |
| 1152 | +++ lib/lp/bugs/model/bugtarget.py 2010-09-23 11:12:49 +0000 |
| 1153 | @@ -338,22 +338,26 @@ |
| 1154 | |
| 1155 | Using this call in ProjectGroup requires a fix of bug 341203, see |
| 1156 | below, class OfficialBugTag. |
| 1157 | + |
| 1158 | + See also `Bug.official_bug_tags` which calculates this efficiently for |
| 1159 | + a single bug. |
| 1160 | """ |
| 1161 | |
| 1162 | + def _getOfficialTagClause(self): |
| 1163 | + if IDistribution.providedBy(self): |
| 1164 | + return (OfficialBugTag.distribution == self) |
| 1165 | + elif IProduct.providedBy(self): |
| 1166 | + return (OfficialBugTag.product == self) |
| 1167 | + else: |
| 1168 | + raise AssertionError( |
| 1169 | + '%s is not a valid official bug target' % self) |
| 1170 | + |
| 1171 | def _getOfficialTags(self): |
| 1172 | """Get the official bug tags as a sorted list of strings.""" |
| 1173 | store = getUtility(IStoreSelector).get(MAIN_STORE, DEFAULT_FLAVOR) |
| 1174 | - if IDistribution.providedBy(self): |
| 1175 | - target_clause = (OfficialBugTag.distribution == self) |
| 1176 | - elif IProduct.providedBy(self): |
| 1177 | - target_clause = (OfficialBugTag.product == self) |
| 1178 | - else: |
| 1179 | - raise AssertionError( |
| 1180 | - '%s is not a valid official bug target' % self) |
| 1181 | - tags = [ |
| 1182 | - obt.tag for obt |
| 1183 | - in store.find(OfficialBugTag, target_clause).order_by('tag')] |
| 1184 | - return tags |
| 1185 | + target_clause = self._getOfficialTagClause() |
| 1186 | + return list(store.find( |
| 1187 | + OfficialBugTag.tag, target_clause).order_by(OfficialBugTag.tag)) |
| 1188 | |
| 1189 | def _setOfficialTags(self, tags): |
| 1190 | """Set the official bug tags from a list of strings.""" |
| 1191 | @@ -374,10 +378,7 @@ |
| 1192 | If the tag is not defined for this target, None is returned. |
| 1193 | """ |
| 1194 | store = getUtility(IStoreSelector).get(MAIN_STORE, DEFAULT_FLAVOR) |
| 1195 | - if IDistribution.providedBy(self): |
| 1196 | - target_clause = (OfficialBugTag.distribution == self) |
| 1197 | - else: |
| 1198 | - target_clause = (OfficialBugTag.product == self) |
| 1199 | + target_clause = self._getOfficialTagClause() |
| 1200 | return store.find( |
| 1201 | OfficialBugTag, OfficialBugTag.tag==tag, target_clause).one() |
| 1202 | |
| 1203 | |
| 1204 | === modified file 'lib/lp/bugs/model/bugtask.py' |
| 1205 | --- lib/lp/bugs/model/bugtask.py 2010-09-09 17:02:33 +0000 |
| 1206 | +++ lib/lp/bugs/model/bugtask.py 2010-09-23 11:12:49 +0000 |
| 1207 | @@ -43,7 +43,10 @@ |
| 1208 | Or, |
| 1209 | SQL, |
| 1210 | ) |
| 1211 | -from storm.store import EmptyResultSet |
| 1212 | +from storm.store import ( |
| 1213 | + EmptyResultSet, |
| 1214 | + Store, |
| 1215 | + ) |
| 1216 | from storm.zope.interfaces import ( |
| 1217 | IResultSet, |
| 1218 | ISQLObjectResultSet, |
| 1219 | @@ -399,6 +402,7 @@ |
| 1220 | sourcepackagename=None, distribution=None, |
| 1221 | distroseries=None): |
| 1222 | """Initialize a NullBugTask.""" |
| 1223 | + self.id = None |
| 1224 | self.bug = bug |
| 1225 | self.product = product |
| 1226 | self.productseries = productseries |
| 1227 | @@ -756,11 +760,11 @@ |
| 1228 | conjoined_master = bugtask |
| 1229 | break |
| 1230 | elif IUpstreamBugTask.providedBy(self): |
| 1231 | - assert self.product.development_focus is not None, ( |
| 1232 | + assert self.product.development_focusID is not None, ( |
| 1233 | 'A product should always have a development series.') |
| 1234 | - devel_focus = self.product.development_focus |
| 1235 | + devel_focusID = self.product.development_focusID |
| 1236 | for bugtask in bugtasks: |
| 1237 | - if bugtask.productseries == devel_focus: |
| 1238 | + if bugtask.productseriesID == devel_focusID: |
| 1239 | conjoined_master = bugtask |
| 1240 | break |
| 1241 | |
| 1242 | @@ -2367,7 +2371,11 @@ |
| 1243 | bugtask._syncFromConjoinedSlave() |
| 1244 | |
| 1245 | bugtask.updateTargetNameCache() |
| 1246 | - |
| 1247 | + del IPropertyCache(bug).bugtasks |
| 1248 | + # Because of block_implicit_flushes, it is possible for a new bugtask |
| 1249 | + # to be queued in appropriately, which leads to Bug.bugtasks not |
| 1250 | + # finding the bugtask. |
| 1251 | + Store.of(bugtask).flush() |
| 1252 | return bugtask |
| 1253 | |
| 1254 | def getStatusCountsForProductSeries(self, user, product_series): |
| 1255 | |
| 1256 | === added directory 'lib/lp/bugs/model/tests' |
| 1257 | === added file 'lib/lp/bugs/model/tests/__init__.py' |
| 1258 | === added file 'lib/lp/bugs/model/tests/test_bugsubscriptionfilter.py' |
| 1259 | --- lib/lp/bugs/model/tests/test_bugsubscriptionfilter.py 1970-01-01 00:00:00 +0000 |
| 1260 | +++ lib/lp/bugs/model/tests/test_bugsubscriptionfilter.py 2010-09-23 11:12:49 +0000 |
| 1261 | @@ -0,0 +1,66 @@ |
| 1262 | +# Copyright 2010 Canonical Ltd. This software is licensed under the |
| 1263 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
| 1264 | + |
| 1265 | +"""Tests for the bugsubscription module.""" |
| 1266 | + |
| 1267 | +__metaclass__ = type |
| 1268 | + |
| 1269 | +from canonical.launchpad.interfaces.lpstorm import IStore |
| 1270 | +from canonical.testing import DatabaseFunctionalLayer |
| 1271 | +from lp.bugs.model.bugsubscriptionfilter import BugSubscriptionFilter |
| 1272 | +from lp.testing import ( |
| 1273 | + login_person, |
| 1274 | + TestCaseWithFactory, |
| 1275 | + ) |
| 1276 | + |
| 1277 | + |
| 1278 | +class TestBugSubscriptionFilter(TestCaseWithFactory): |
| 1279 | + |
| 1280 | + layer = DatabaseFunctionalLayer |
| 1281 | + |
| 1282 | + def setUp(self): |
| 1283 | + super(TestBugSubscriptionFilter, self).setUp() |
| 1284 | + self.target = self.factory.makeProduct() |
| 1285 | + self.subscriber = self.target.owner |
| 1286 | + login_person(self.subscriber) |
| 1287 | + self.subscription = self.target.addBugSubscription( |
| 1288 | + self.subscriber, self.subscriber) |
| 1289 | + |
| 1290 | + def test_basics(self): |
| 1291 | + """Test the basic operation of `BugSubscriptionFilter` objects.""" |
| 1292 | + # Create. |
| 1293 | + bug_subscription_filter = BugSubscriptionFilter() |
| 1294 | + bug_subscription_filter.structural_subscription = self.subscription |
| 1295 | + bug_subscription_filter.find_all_tags = True |
| 1296 | + bug_subscription_filter.include_any_tags = True |
| 1297 | + bug_subscription_filter.exclude_any_tags = True |
| 1298 | + bug_subscription_filter.other_parameters = u"foo" |
| 1299 | + bug_subscription_filter.description = u"bar" |
| 1300 | + # Flush and reload. |
| 1301 | + IStore(bug_subscription_filter).flush() |
| 1302 | + IStore(bug_subscription_filter).reload(bug_subscription_filter) |
| 1303 | + # Check. |
| 1304 | + self.assertIsNot(None, bug_subscription_filter.id) |
| 1305 | + self.assertEqual( |
| 1306 | + self.subscription.id, |
| 1307 | + bug_subscription_filter.structural_subscription_id) |
| 1308 | + self.assertEqual( |
| 1309 | + self.subscription, |
| 1310 | + bug_subscription_filter.structural_subscription) |
| 1311 | + self.assertIs(True, bug_subscription_filter.find_all_tags) |
| 1312 | + self.assertIs(True, bug_subscription_filter.include_any_tags) |
| 1313 | + self.assertIs(True, bug_subscription_filter.exclude_any_tags) |
| 1314 | + self.assertEqual(u"foo", bug_subscription_filter.other_parameters) |
| 1315 | + self.assertEqual(u"bar", bug_subscription_filter.description) |
| 1316 | + |
| 1317 | + def test_defaults(self): |
| 1318 | + """Test the default values of `BugSubscriptionFilter` objects.""" |
| 1319 | + # Create. |
| 1320 | + bug_subscription_filter = BugSubscriptionFilter() |
| 1321 | + bug_subscription_filter.structural_subscription = self.subscription |
| 1322 | + # Check. |
| 1323 | + self.assertIs(False, bug_subscription_filter.find_all_tags) |
| 1324 | + self.assertIs(False, bug_subscription_filter.include_any_tags) |
| 1325 | + self.assertIs(False, bug_subscription_filter.exclude_any_tags) |
| 1326 | + self.assertIs(None, bug_subscription_filter.other_parameters) |
| 1327 | + self.assertIs(None, bug_subscription_filter.description) |
| 1328 | |
| 1329 | === added file 'lib/lp/bugs/model/tests/test_bugsubscriptionfilterimportance.py' |
| 1330 | --- lib/lp/bugs/model/tests/test_bugsubscriptionfilterimportance.py 1970-01-01 00:00:00 +0000 |
| 1331 | +++ lib/lp/bugs/model/tests/test_bugsubscriptionfilterimportance.py 2010-09-23 11:12:49 +0000 |
| 1332 | @@ -0,0 +1,54 @@ |
| 1333 | +# Copyright 2010 Canonical Ltd. This software is licensed under the |
| 1334 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
| 1335 | + |
| 1336 | +"""Tests for the bugsubscription module.""" |
| 1337 | + |
| 1338 | +__metaclass__ = type |
| 1339 | + |
| 1340 | +from canonical.launchpad.interfaces.lpstorm import IStore |
| 1341 | +from canonical.testing import DatabaseFunctionalLayer |
| 1342 | +from lp.bugs.interfaces.bugtask import BugTaskImportance |
| 1343 | +from lp.bugs.model.bugsubscriptionfilter import BugSubscriptionFilter |
| 1344 | +from lp.bugs.model.bugsubscriptionfilterimportance import ( |
| 1345 | + BugSubscriptionFilterImportance, |
| 1346 | + ) |
| 1347 | +from lp.testing import ( |
| 1348 | + login_person, |
| 1349 | + TestCaseWithFactory, |
| 1350 | + ) |
| 1351 | + |
| 1352 | + |
| 1353 | +class TestBugSubscriptionFilterImportance(TestCaseWithFactory): |
| 1354 | + |
| 1355 | + layer = DatabaseFunctionalLayer |
| 1356 | + |
| 1357 | + def setUp(self): |
| 1358 | + super(TestBugSubscriptionFilterImportance, self).setUp() |
| 1359 | + self.target = self.factory.makeProduct() |
| 1360 | + self.subscriber = self.target.owner |
| 1361 | + login_person(self.subscriber) |
| 1362 | + self.subscription = self.target.addBugSubscription( |
| 1363 | + self.subscriber, self.subscriber) |
| 1364 | + self.subscription_filter = BugSubscriptionFilter() |
| 1365 | + self.subscription_filter.structural_subscription = self.subscription |
| 1366 | + |
| 1367 | + def test_basics(self): |
| 1368 | + """Test the basics of `BugSubscriptionFilterImportance` objects.""" |
| 1369 | + # Create. |
| 1370 | + bug_sub_filter_importance = BugSubscriptionFilterImportance() |
| 1371 | + bug_sub_filter_importance.filter = self.subscription_filter |
| 1372 | + bug_sub_filter_importance.importance = BugTaskImportance.HIGH |
| 1373 | + # Flush and reload. |
| 1374 | + IStore(bug_sub_filter_importance).flush() |
| 1375 | + IStore(bug_sub_filter_importance).reload(bug_sub_filter_importance) |
| 1376 | + # Check. |
| 1377 | + self.assertIsNot(None, bug_sub_filter_importance.id) |
| 1378 | + self.assertEqual( |
| 1379 | + self.subscription_filter.id, |
| 1380 | + bug_sub_filter_importance.filter_id) |
| 1381 | + self.assertEqual( |
| 1382 | + self.subscription_filter, |
| 1383 | + bug_sub_filter_importance.filter) |
| 1384 | + self.assertEqual( |
| 1385 | + BugTaskImportance.HIGH, |
| 1386 | + bug_sub_filter_importance.importance) |
| 1387 | |
| 1388 | === added file 'lib/lp/bugs/model/tests/test_bugsubscriptionfilterstatus.py' |
| 1389 | --- lib/lp/bugs/model/tests/test_bugsubscriptionfilterstatus.py 1970-01-01 00:00:00 +0000 |
| 1390 | +++ lib/lp/bugs/model/tests/test_bugsubscriptionfilterstatus.py 2010-09-23 11:12:49 +0000 |
| 1391 | @@ -0,0 +1,52 @@ |
| 1392 | +# Copyright 2010 Canonical Ltd. This software is licensed under the |
| 1393 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
| 1394 | + |
| 1395 | +"""Tests for the bugsubscription module.""" |
| 1396 | + |
| 1397 | +__metaclass__ = type |
| 1398 | + |
| 1399 | +from canonical.launchpad.interfaces.lpstorm import IStore |
| 1400 | +from canonical.testing import DatabaseFunctionalLayer |
| 1401 | +from lp.bugs.interfaces.bugtask import BugTaskStatus |
| 1402 | +from lp.bugs.model.bugsubscriptionfilter import BugSubscriptionFilter |
| 1403 | +from lp.bugs.model.bugsubscriptionfilterstatus import ( |
| 1404 | + BugSubscriptionFilterStatus, |
| 1405 | + ) |
| 1406 | +from lp.testing import ( |
| 1407 | + login_person, |
| 1408 | + TestCaseWithFactory, |
| 1409 | + ) |
| 1410 | + |
| 1411 | + |
| 1412 | +class TestBugSubscriptionFilterStatus(TestCaseWithFactory): |
| 1413 | + |
| 1414 | + layer = DatabaseFunctionalLayer |
| 1415 | + |
| 1416 | + def setUp(self): |
| 1417 | + super(TestBugSubscriptionFilterStatus, self).setUp() |
| 1418 | + self.target = self.factory.makeProduct() |
| 1419 | + self.subscriber = self.target.owner |
| 1420 | + login_person(self.subscriber) |
| 1421 | + self.subscription = self.target.addBugSubscription( |
| 1422 | + self.subscriber, self.subscriber) |
| 1423 | + self.subscription_filter = BugSubscriptionFilter() |
| 1424 | + self.subscription_filter.structural_subscription = self.subscription |
| 1425 | + |
| 1426 | + def test_basics(self): |
| 1427 | + """Test the basics of `BugSubscriptionFilterStatus` objects.""" |
| 1428 | + # Create. |
| 1429 | + bug_sub_filter_status = BugSubscriptionFilterStatus() |
| 1430 | + bug_sub_filter_status.filter = self.subscription_filter |
| 1431 | + bug_sub_filter_status.status = BugTaskStatus.NEW |
| 1432 | + # Flush and reload. |
| 1433 | + IStore(bug_sub_filter_status).flush() |
| 1434 | + IStore(bug_sub_filter_status).reload(bug_sub_filter_status) |
| 1435 | + # Check. |
| 1436 | + self.assertIsNot(None, bug_sub_filter_status.id) |
| 1437 | + self.assertEqual( |
| 1438 | + self.subscription_filter.id, |
| 1439 | + bug_sub_filter_status.filter_id) |
| 1440 | + self.assertEqual( |
| 1441 | + self.subscription_filter, |
| 1442 | + bug_sub_filter_status.filter) |
| 1443 | + self.assertEqual(BugTaskStatus.NEW, bug_sub_filter_status.status) |
| 1444 | |
| 1445 | === added file 'lib/lp/bugs/model/tests/test_bugsubscriptionfiltertag.py' |
| 1446 | --- lib/lp/bugs/model/tests/test_bugsubscriptionfiltertag.py 1970-01-01 00:00:00 +0000 |
| 1447 | +++ lib/lp/bugs/model/tests/test_bugsubscriptionfiltertag.py 2010-09-23 11:12:49 +0000 |
| 1448 | @@ -0,0 +1,51 @@ |
| 1449 | +# Copyright 2010 Canonical Ltd. This software is licensed under the |
| 1450 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
| 1451 | + |
| 1452 | +"""Tests for the bugsubscription module.""" |
| 1453 | + |
| 1454 | +__metaclass__ = type |
| 1455 | + |
| 1456 | +from canonical.launchpad.interfaces.lpstorm import IStore |
| 1457 | +from canonical.testing import DatabaseFunctionalLayer |
| 1458 | +from lp.bugs.model.bugsubscriptionfilter import BugSubscriptionFilter |
| 1459 | +from lp.bugs.model.bugsubscriptionfiltertag import BugSubscriptionFilterTag |
| 1460 | +from lp.testing import ( |
| 1461 | + login_person, |
| 1462 | + TestCaseWithFactory, |
| 1463 | + ) |
| 1464 | + |
| 1465 | + |
| 1466 | +class TestBugSubscriptionFilterTag(TestCaseWithFactory): |
| 1467 | + |
| 1468 | + layer = DatabaseFunctionalLayer |
| 1469 | + |
| 1470 | + def setUp(self): |
| 1471 | + super(TestBugSubscriptionFilterTag, self).setUp() |
| 1472 | + self.target = self.factory.makeProduct() |
| 1473 | + self.subscriber = self.target.owner |
| 1474 | + login_person(self.subscriber) |
| 1475 | + self.subscription = self.target.addBugSubscription( |
| 1476 | + self.subscriber, self.subscriber) |
| 1477 | + self.subscription_filter = BugSubscriptionFilter() |
| 1478 | + self.subscription_filter.structural_subscription = self.subscription |
| 1479 | + |
| 1480 | + def test_basics(self): |
| 1481 | + """Test the basics of `BugSubscriptionFilterTag` objects.""" |
| 1482 | + # Create. |
| 1483 | + bug_sub_filter_tag = BugSubscriptionFilterTag() |
| 1484 | + bug_sub_filter_tag.filter = self.subscription_filter |
| 1485 | + bug_sub_filter_tag.include = True |
| 1486 | + bug_sub_filter_tag.tag = u"foo" |
| 1487 | + # Flush and reload. |
| 1488 | + IStore(bug_sub_filter_tag).flush() |
| 1489 | + IStore(bug_sub_filter_tag).reload(bug_sub_filter_tag) |
| 1490 | + # Check. |
| 1491 | + self.assertIsNot(None, bug_sub_filter_tag.id) |
| 1492 | + self.assertEqual( |
| 1493 | + self.subscription_filter.id, |
| 1494 | + bug_sub_filter_tag.filter_id) |
| 1495 | + self.assertEqual( |
| 1496 | + self.subscription_filter, |
| 1497 | + bug_sub_filter_tag.filter) |
| 1498 | + self.assertIs(True, bug_sub_filter_tag.include) |
| 1499 | + self.assertEqual(u"foo", bug_sub_filter_tag.tag) |
| 1500 | |
| 1501 | === modified file 'lib/lp/bugs/stories/webservice/xx-bug.txt' |
| 1502 | --- lib/lp/bugs/stories/webservice/xx-bug.txt 2010-09-03 20:28:36 +0000 |
| 1503 | +++ lib/lp/bugs/stories/webservice/xx-bug.txt 2010-09-23 11:12:49 +0000 |
| 1504 | @@ -18,6 +18,7 @@ |
| 1505 | 15 |
| 1506 | >>> bug_entries = sorted(bugs['entries'], key=itemgetter('id')) |
| 1507 | >>> pprint_entry(bug_entries[0]) |
| 1508 | + activity_collection_link: u'http://.../bugs/11/activity' |
| 1509 | attachments_collection_link: u'http://.../bugs/11/attachments' |
| 1510 | bug_tasks_collection_link: u'http://.../bugs/11/bug_tasks' |
| 1511 | bug_watches_collection_link: u'http://.../bugs/11/bug_watches' |
| 1512 | @@ -2084,3 +2085,33 @@ |
| 1513 | Traceback (most recent call last): |
| 1514 | ... |
| 1515 | NameError: name 'can_expire' is not defined |
| 1516 | + |
| 1517 | + |
| 1518 | +Bug activity |
| 1519 | +------------ |
| 1520 | + |
| 1521 | +Each bug has a collection of activities that have taken place with it. |
| 1522 | + |
| 1523 | + >>> from lazr.restful.testing.webservice import ( |
| 1524 | + ... pprint_collection, pprint_entry) |
| 1525 | + >>> activity = webservice.get( |
| 1526 | + ... bug_one['activity_collection_link']).jsonBody() |
| 1527 | + >>> pprint_collection(activity) |
| 1528 | + next_collection_link: u'http://.../bugs/1/activity?ws.start=5&ws.size=5' |
| 1529 | + resource_type_link: u'http://.../#bug_activity-page-resource' |
| 1530 | + start: 0 |
| 1531 | + total_size: 26 |
| 1532 | + ... |
| 1533 | + |
| 1534 | + >>> bug_nine_activity = webservice.get( |
| 1535 | + ... "/bugs/9/activity").jsonBody() |
| 1536 | + >>> pprint_entry(bug_nine_activity['entries'][1]) |
| 1537 | + bug_link: u'http://.../bugs/9' |
| 1538 | + datechanged: u'2006-02-23T16:42:40.288553+00:00' |
| 1539 | + message: None |
| 1540 | + newvalue: u'Confirmed' |
| 1541 | + oldvalue: u'Unconfirmed' |
| 1542 | + person_link: u'http://.../~name12' |
| 1543 | + resource_type_link: u'http://.../#bug_activity' |
| 1544 | + self_link: u'http://.../bugs/9/activity' |
| 1545 | + whatchanged: u'thunderbird: status' |
| 1546 | |
| 1547 | === modified file 'lib/lp/bugs/templates/bug-portlet-actions.pt' |
| 1548 | --- lib/lp/bugs/templates/bug-portlet-actions.pt 2010-08-02 17:49:45 +0000 |
| 1549 | +++ lib/lp/bugs/templates/bug-portlet-actions.pt 2010-09-23 11:12:49 +0000 |
| 1550 | @@ -18,8 +18,8 @@ |
| 1551 | class="menu-link-mark-dupe">Mark as duplicate</a> |
| 1552 | </span> |
| 1553 | <tal:block |
| 1554 | - tal:condition="context/duplicates" |
| 1555 | - tal:define="number_of_dupes context/duplicates/count" |
| 1556 | + tal:condition="context/number_of_duplicates" |
| 1557 | + tal:define="number_of_dupes context/number_of_duplicates" |
| 1558 | > |
| 1559 | </tal:block> |
| 1560 | <tal:block |
| 1561 | |
| 1562 | === modified file 'lib/lp/bugs/templates/bugtask-index.pt' |
| 1563 | --- lib/lp/bugs/templates/bugtask-index.pt 2010-09-22 19:35:18 +0000 |
| 1564 | +++ lib/lp/bugs/templates/bugtask-index.pt 2010-09-23 11:12:49 +0000 |
| 1565 | @@ -195,7 +195,7 @@ |
| 1566 | </tal:branches> |
| 1567 | </div><!-- bug-branch-container --> |
| 1568 | |
| 1569 | - <div tal:condition="context/bug/cves" class="cves"> |
| 1570 | + <div tal:condition="context/bug/has_cves" class="cves"> |
| 1571 | <h2>CVE References</h2> |
| 1572 | <ul> |
| 1573 | <li class="sprite cve" tal:repeat="cve context/bug/cves"> |
| 1574 | |
| 1575 | === modified file 'lib/lp/bugs/tests/has-bug-supervisor.txt' |
| 1576 | --- lib/lp/bugs/tests/has-bug-supervisor.txt 2009-08-25 11:21:05 +0000 |
| 1577 | +++ lib/lp/bugs/tests/has-bug-supervisor.txt 2010-09-23 11:12:49 +0000 |
| 1578 | @@ -5,7 +5,7 @@ |
| 1579 | structural subscription targets. When the bug supervisor for such an object |
| 1580 | is set, a new bug subscription is created as well. |
| 1581 | |
| 1582 | - >>> target.bug_subscriptions |
| 1583 | + >>> list(target.bug_subscriptions) |
| 1584 | [] |
| 1585 | |
| 1586 | >>> print target.bug_supervisor |
| 1587 | |
| 1588 | === modified file 'lib/lp/buildmaster/tests/mock_slaves.py' |
| 1589 | --- lib/lp/buildmaster/tests/mock_slaves.py 2010-09-22 11:27:40 +0000 |
| 1590 | +++ lib/lp/buildmaster/tests/mock_slaves.py 2010-09-23 11:12:49 +0000 |
| 1591 | @@ -122,13 +122,11 @@ |
| 1592 | return ('1.0', self.arch_tag, 'debian') |
| 1593 | |
| 1594 | def sendFileToSlave(self, sha1, url, username="", password=""): |
| 1595 | - self.call_log.append('sendFileToSlave') |
| 1596 | present, info = self.ensurepresent(sha1, url, username, password) |
| 1597 | if not present: |
| 1598 | raise CannotFetchFile(url, info) |
| 1599 | |
| 1600 | def cacheFile(self, logger, libraryfilealias): |
| 1601 | - self.call_log.append('cacheFile') |
| 1602 | return self.sendFileToSlave( |
| 1603 | libraryfilealias.content.sha1, libraryfilealias.http_url) |
| 1604 | |
| 1605 | |
| 1606 | === modified file 'lib/lp/code/browser/sourcepackagerecipe.py' |
| 1607 | --- lib/lp/code/browser/sourcepackagerecipe.py 2010-09-02 14:28:57 +0000 |
| 1608 | +++ lib/lp/code/browser/sourcepackagerecipe.py 2010-09-23 11:12:49 +0000 |
| 1609 | @@ -60,6 +60,7 @@ |
| 1610 | BuildAlreadyPending, |
| 1611 | NoSuchBranch, |
| 1612 | PrivateBranchRecipe, |
| 1613 | + TooNewRecipeFormat |
| 1614 | ) |
| 1615 | from lp.code.interfaces.sourcepackagerecipe import ( |
| 1616 | ISourcePackageRecipe, |
| 1617 | @@ -333,6 +334,11 @@ |
| 1618 | data['recipe_text'], data['description'], data['distros'], |
| 1619 | data['daily_build_archive'], data['build_daily']) |
| 1620 | Store.of(source_package_recipe).flush() |
| 1621 | + except TooNewRecipeFormat: |
| 1622 | + self.setFieldError( |
| 1623 | + 'recipe_text', |
| 1624 | + 'The recipe format version specified is not available.') |
| 1625 | + return |
| 1626 | except ForbiddenInstructionError: |
| 1627 | # XXX: bug=592513 We shouldn't be hardcoding "run" here. |
| 1628 | self.setFieldError( |
| 1629 | |
| 1630 | === modified file 'lib/lp/code/browser/tests/test_sourcepackagerecipe.py' |
| 1631 | --- lib/lp/code/browser/tests/test_sourcepackagerecipe.py 2010-09-20 19:12:35 +0000 |
| 1632 | +++ lib/lp/code/browser/tests/test_sourcepackagerecipe.py 2010-09-23 11:12:49 +0000 |
| 1633 | @@ -40,6 +40,7 @@ |
| 1634 | SourcePackageRecipeBuildView, |
| 1635 | ) |
| 1636 | from lp.code.interfaces.sourcepackagerecipe import MINIMAL_RECIPE_TEXT |
| 1637 | +from lp.code.tests.helpers import recipe_parser_newest_version |
| 1638 | from lp.registry.interfaces.pocket import PackagePublishingPocket |
| 1639 | from lp.soyuz.model.processor import ProcessorFamily |
| 1640 | from lp.testing import ( |
| 1641 | @@ -304,6 +305,24 @@ |
| 1642 | self.assertEqual( |
| 1643 | get_message_text(browser, 2), 'foo is not a branch on Launchpad.') |
| 1644 | |
| 1645 | + def test_create_recipe_format_too_new(self): |
| 1646 | + # If the recipe's format version is too new, we should notify the |
| 1647 | + # user. |
| 1648 | + product = self.factory.makeProduct( |
| 1649 | + name='ratatouille', displayname='Ratatouille') |
| 1650 | + branch = self.factory.makeBranch( |
| 1651 | + owner=self.chef, product=product, name='veggies') |
| 1652 | + |
| 1653 | + with recipe_parser_newest_version(145.115): |
| 1654 | + recipe = dedent(u'''\ |
| 1655 | + # bzr-builder format 145.115 deb-version 0+{revno} |
| 1656 | + %s |
| 1657 | + ''') % branch.bzr_identity |
| 1658 | + browser = self.createRecipe(recipe, branch) |
| 1659 | + self.assertEqual( |
| 1660 | + get_message_text(browser, 2), |
| 1661 | + 'The recipe format version specified is not available.') |
| 1662 | + |
| 1663 | def test_create_dupe_recipe(self): |
| 1664 | # You shouldn't be able to create a duplicate recipe owned by the same |
| 1665 | # person with the same name. |
| 1666 | |
| 1667 | === modified file 'lib/lp/code/mail/sourcepackagerecipebuild.py' |
| 1668 | --- lib/lp/code/mail/sourcepackagerecipebuild.py 2010-08-20 20:31:18 +0000 |
| 1669 | +++ lib/lp/code/mail/sourcepackagerecipebuild.py 2010-09-23 11:12:49 +0000 |
| 1670 | @@ -67,6 +67,7 @@ |
| 1671 | 'duration': '', |
| 1672 | 'builder_url': '', |
| 1673 | 'build_url': canonical_url(self.build), |
| 1674 | + 'upload_log_url': '', |
| 1675 | }) |
| 1676 | if self.build.builder is not None: |
| 1677 | params['builder_url'] = canonical_url(self.build.builder) |
| 1678 | @@ -75,6 +76,8 @@ |
| 1679 | params['duration'] = duration_formatter.approximateduration() |
| 1680 | if self.build.log is not None: |
| 1681 | params['log_url'] = self.build.log.getURL() |
| 1682 | + if self.build.upload_log is not None: |
| 1683 | + params['upload_log_url'] = self.build.upload_log_url |
| 1684 | return params |
| 1685 | |
| 1686 | def _getFooter(self, params): |
| 1687 | |
| 1688 | === modified file 'lib/lp/code/mail/tests/test_sourcepackagerecipebuild.py' |
| 1689 | --- lib/lp/code/mail/tests/test_sourcepackagerecipebuild.py 2010-08-27 11:19:54 +0000 |
| 1690 | +++ lib/lp/code/mail/tests/test_sourcepackagerecipebuild.py 2010-09-23 11:12:49 +0000 |
| 1691 | @@ -27,6 +27,7 @@ |
| 1692 | * Distroseries: distroseries |
| 1693 | * Duration: five minutes |
| 1694 | * Build Log: %s |
| 1695 | + * Upload Log: |
| 1696 | * Builder: http://launchpad.dev/builders/bob |
| 1697 | """ |
| 1698 | |
| 1699 | @@ -37,6 +38,7 @@ |
| 1700 | * Distroseries: distroseries |
| 1701 | * Duration: |
| 1702 | * Build Log: |
| 1703 | + * Upload Log: |
| 1704 | * Builder: |
| 1705 | """ |
| 1706 | |
| 1707 | @@ -44,6 +46,11 @@ |
| 1708 | |
| 1709 | layer = LaunchpadFunctionalLayer |
| 1710 | |
| 1711 | + def makeStatusEmail(self, build): |
| 1712 | + mailer = SourcePackageRecipeBuildMailer.forStatus(build) |
| 1713 | + email = build.requester.preferredemail.email |
| 1714 | + return mailer.generateEmail(email, build.requester) |
| 1715 | + |
| 1716 | def test_generateEmail(self): |
| 1717 | """GenerateEmail produces the right headers and body.""" |
| 1718 | person = self.factory.makePerson(name='person') |
| 1719 | @@ -59,9 +66,7 @@ |
| 1720 | naked_build.builder = self.factory.makeBuilder(name='bob') |
| 1721 | naked_build.log = self.factory.makeLibraryFileAlias() |
| 1722 | Store.of(build).flush() |
| 1723 | - mailer = SourcePackageRecipeBuildMailer.forStatus(build) |
| 1724 | - email = build.requester.preferredemail.email |
| 1725 | - ctrl = mailer.generateEmail(email, build.requester) |
| 1726 | + ctrl = self.makeStatusEmail(build) |
| 1727 | self.assertEqual( |
| 1728 | u'[recipe build #%d] of ~person recipe in distroseries: ' |
| 1729 | 'Successfully built' % (build.id), ctrl.subject) |
| 1730 | @@ -93,9 +98,7 @@ |
| 1731 | recipe=cake, distroseries=secret, archive=pantry, |
| 1732 | status=BuildStatus.SUPERSEDED) |
| 1733 | Store.of(build).flush() |
| 1734 | - mailer = SourcePackageRecipeBuildMailer.forStatus(build) |
| 1735 | - email = build.requester.preferredemail.email |
| 1736 | - ctrl = mailer.generateEmail(email, build.requester) |
| 1737 | + ctrl = self.makeStatusEmail(build) |
| 1738 | self.assertEqual( |
| 1739 | u'[recipe build #%d] of ~person recipe in distroseries: ' |
| 1740 | 'Build for superseded Source' % (build.id), ctrl.subject) |
| 1741 | @@ -114,6 +117,15 @@ |
| 1742 | self.assertEqual( |
| 1743 | 'SUPERSEDED', ctrl.headers['X-Launchpad-Build-State']) |
| 1744 | |
| 1745 | + def test_generateEmail_upload_failure(self): |
| 1746 | + """GenerateEmail works when many fields are NULL.""" |
| 1747 | + build = self.factory.makeSourcePackageRecipeBuild() |
| 1748 | + removeSecurityProxy(build).upload_log = ( |
| 1749 | + self.factory.makeLibraryFileAlias()) |
| 1750 | + upload_log_fragment = 'Upload Log: %s' % build.upload_log_url |
| 1751 | + ctrl = self.makeStatusEmail(build) |
| 1752 | + self.assertTrue(upload_log_fragment in ctrl.body) |
| 1753 | + |
| 1754 | |
| 1755 | def test_suite(): |
| 1756 | return TestLoader().loadTestsFromName(__name__) |
| 1757 | |
| 1758 | === modified file 'lib/lp/code/model/tests/test_recipebuilder.py' |
| 1759 | --- lib/lp/code/model/tests/test_recipebuilder.py 2010-09-22 11:26:19 +0000 |
| 1760 | +++ lib/lp/code/model/tests/test_recipebuilder.py 2010-09-23 11:12:49 +0000 |
| 1761 | @@ -12,16 +12,20 @@ |
| 1762 | import unittest |
| 1763 | |
| 1764 | import transaction |
| 1765 | +from twisted.internet import defer |
| 1766 | +from twisted.trial.unittest import TestCase as TrialTestCase |
| 1767 | from zope.security.proxy import removeSecurityProxy |
| 1768 | |
| 1769 | from canonical.launchpad.scripts.logger import BufferLogger |
| 1770 | -from canonical.testing import LaunchpadFunctionalLayer |
| 1771 | +from canonical.testing import ( |
| 1772 | + LaunchpadFunctionalLayer, |
| 1773 | + TwistedLaunchpadZopelessLayer, |
| 1774 | + ) |
| 1775 | from lp.buildmaster.enums import BuildFarmJobType |
| 1776 | from lp.buildmaster.interfaces.builder import CannotBuild |
| 1777 | from lp.buildmaster.interfaces.buildfarmjobbehavior import ( |
| 1778 | IBuildFarmJobBehavior, |
| 1779 | ) |
| 1780 | -from lp.buildmaster.manager import RecordingSlave |
| 1781 | from lp.buildmaster.model.buildqueue import BuildQueue |
| 1782 | from lp.buildmaster.tests.mock_slaves import ( |
| 1783 | MockBuilder, |
| 1784 | @@ -36,25 +40,16 @@ |
| 1785 | from lp.soyuz.model.processor import ProcessorFamilySet |
| 1786 | from lp.soyuz.tests.test_publishing import SoyuzTestPublisher |
| 1787 | from lp.testing import ( |
| 1788 | + ANONYMOUS, |
| 1789 | + login_as, |
| 1790 | + logout, |
| 1791 | person_logged_in, |
| 1792 | TestCaseWithFactory, |
| 1793 | ) |
| 1794 | - |
| 1795 | - |
| 1796 | -class TestRecipeBuilder(TestCaseWithFactory): |
| 1797 | - |
| 1798 | - layer = LaunchpadFunctionalLayer |
| 1799 | - |
| 1800 | - def test_providesInterface(self): |
| 1801 | - # RecipeBuildBehavior provides IBuildFarmJobBehavior. |
| 1802 | - recipe_builder = RecipeBuildBehavior(None) |
| 1803 | - self.assertProvides(recipe_builder, IBuildFarmJobBehavior) |
| 1804 | - |
| 1805 | - def test_adapts_ISourcePackageRecipeBuildJob(self): |
| 1806 | - # IBuildFarmJobBehavior adapts a ISourcePackageRecipeBuildJob |
| 1807 | - job = self.factory.makeSourcePackageRecipeBuild().makeJob() |
| 1808 | - job = IBuildFarmJobBehavior(job) |
| 1809 | - self.assertProvides(job, IBuildFarmJobBehavior) |
| 1810 | +from lp.testing.factory import LaunchpadObjectFactory |
| 1811 | + |
| 1812 | + |
| 1813 | +class RecipeBuilderTestsMixin: |
| 1814 | |
| 1815 | def makeJob(self, recipe_registrant=None, recipe_owner=None): |
| 1816 | """Create a sample `ISourcePackageRecipeBuildJob`.""" |
| 1817 | @@ -87,6 +82,22 @@ |
| 1818 | job = IBuildFarmJobBehavior(job) |
| 1819 | return job |
| 1820 | |
| 1821 | + |
| 1822 | +class TestRecipeBuilder(TestCaseWithFactory, RecipeBuilderTestsMixin): |
| 1823 | + |
| 1824 | + layer = LaunchpadFunctionalLayer |
| 1825 | + |
| 1826 | + def test_providesInterface(self): |
| 1827 | + # RecipeBuildBehavior provides IBuildFarmJobBehavior. |
| 1828 | + recipe_builder = RecipeBuildBehavior(None) |
| 1829 | + self.assertProvides(recipe_builder, IBuildFarmJobBehavior) |
| 1830 | + |
| 1831 | + def test_adapts_ISourcePackageRecipeBuildJob(self): |
| 1832 | + # IBuildFarmJobBehavior adapts a ISourcePackageRecipeBuildJob |
| 1833 | + job = self.factory.makeSourcePackageRecipeBuild().makeJob() |
| 1834 | + job = IBuildFarmJobBehavior(job) |
| 1835 | + self.assertProvides(job, IBuildFarmJobBehavior) |
| 1836 | + |
| 1837 | def test_display_name(self): |
| 1838 | # display_name contains a sane description of the job |
| 1839 | job = self.makeJob() |
| 1840 | @@ -241,32 +252,51 @@ |
| 1841 | job.build, distroarchseries, None) |
| 1842 | self.assertEqual(args["archives"], expected_archives) |
| 1843 | |
| 1844 | + def test_getById(self): |
| 1845 | + job = self.makeJob() |
| 1846 | + transaction.commit() |
| 1847 | + self.assertEquals( |
| 1848 | + job.build, SourcePackageRecipeBuild.getById(job.build.id)) |
| 1849 | + |
| 1850 | + |
| 1851 | +class TestDispatchBuildToSlave(TrialTestCase, RecipeBuilderTestsMixin): |
| 1852 | + |
| 1853 | + layer = TwistedLaunchpadZopelessLayer |
| 1854 | + |
| 1855 | + def setUp(self): |
| 1856 | + super(TestDispatchBuildToSlave, self).setUp() |
| 1857 | + self.factory = LaunchpadObjectFactory() |
| 1858 | + login_as(ANONYMOUS) |
| 1859 | + self.addCleanup(logout) |
| 1860 | + self.layer.switchDbUser('testadmin') |
| 1861 | |
| 1862 | def test_dispatchBuildToSlave(self): |
| 1863 | # Ensure dispatchBuildToSlave will make the right calls to the slave |
| 1864 | job = self.makeJob() |
| 1865 | test_publisher = SoyuzTestPublisher() |
| 1866 | test_publisher.addFakeChroots(job.build.distroseries) |
| 1867 | - slave = RecordingSlave("i386-slave-1", "http://myurl", "vmhost") |
| 1868 | + slave = OkSlave() |
| 1869 | builder = MockBuilder("bob-de-bouwer", slave) |
| 1870 | processorfamily = ProcessorFamilySet().getByProcessorName('386') |
| 1871 | builder.processor = processorfamily.processors[0] |
| 1872 | job.setBuilder(builder) |
| 1873 | logger = BufferLogger() |
| 1874 | - job.dispatchBuildToSlave("someid", logger) |
| 1875 | - logger.buffer.seek(0) |
| 1876 | - self.assertEquals( |
| 1877 | - "DEBUG: Initiating build 1-someid on http://fake:0000\n", |
| 1878 | - logger.buffer.readline()) |
| 1879 | - self.assertEquals(["ensurepresent", "build"], |
| 1880 | - [call[0] for call in slave.calls]) |
| 1881 | - build_args = slave.calls[1][1] |
| 1882 | - self.assertEquals( |
| 1883 | - build_args[0], job.buildfarmjob.generateSlaveBuildCookie()) |
| 1884 | - self.assertEquals(build_args[1], "sourcepackagerecipe") |
| 1885 | - self.assertEquals(build_args[3], {}) |
| 1886 | - distroarchseries = job.build.distroseries.architectures[0] |
| 1887 | - self.assertEqual(build_args[4], job._extraBuildArgs(distroarchseries)) |
| 1888 | + d = defer.maybeDeferred(job.dispatchBuildToSlave, "someid", logger) |
| 1889 | + def check_dispatch(ignored): |
| 1890 | + logger.buffer.seek(0) |
| 1891 | + self.assertEquals( |
| 1892 | + "DEBUG: Initiating build 1-someid on http://fake:0000\n", |
| 1893 | + logger.buffer.readline()) |
| 1894 | + self.assertEquals(["ensurepresent", "build"], |
| 1895 | + [call[0] for call in slave.call_log]) |
| 1896 | + build_args = slave.call_log[1][1:] |
| 1897 | + self.assertEquals( |
| 1898 | + build_args[0], job.buildfarmjob.generateSlaveBuildCookie()) |
| 1899 | + self.assertEquals(build_args[1], "sourcepackagerecipe") |
| 1900 | + self.assertEquals(build_args[3], []) |
| 1901 | + distroarchseries = job.build.distroseries.architectures[0] |
| 1902 | + self.assertEqual(build_args[4], job._extraBuildArgs(distroarchseries)) |
| 1903 | + return d.addCallback(check_dispatch) |
| 1904 | |
| 1905 | def test_dispatchBuildToSlave_nochroot(self): |
| 1906 | # dispatchBuildToSlave will fail when there is not chroot tarball |
| 1907 | @@ -277,14 +307,8 @@ |
| 1908 | builder.processor = processorfamily.processors[0] |
| 1909 | job.setBuilder(builder) |
| 1910 | logger = BufferLogger() |
| 1911 | - self.assertRaises(CannotBuild, job.dispatchBuildToSlave, |
| 1912 | - "someid", logger) |
| 1913 | - |
| 1914 | - def test_getById(self): |
| 1915 | - job = self.makeJob() |
| 1916 | - transaction.commit() |
| 1917 | - self.assertEquals( |
| 1918 | - job.build, SourcePackageRecipeBuild.getById(job.build.id)) |
| 1919 | + d = defer.maybeDeferred(job.dispatchBuildToSlave, "someid", logger) |
| 1920 | + return self.assertFailure(d, CannotBuild) |
| 1921 | |
| 1922 | |
| 1923 | def test_suite(): |
| 1924 | |
| 1925 | === modified file 'lib/lp/code/model/tests/test_sourcepackagerecipe.py' |
| 1926 | --- lib/lp/code/model/tests/test_sourcepackagerecipe.py 2010-09-21 19:02:44 +0000 |
| 1927 | +++ lib/lp/code/model/tests/test_sourcepackagerecipe.py 2010-09-23 11:12:49 +0000 |
| 1928 | @@ -53,6 +53,7 @@ |
| 1929 | SourcePackageRecipe, |
| 1930 | ) |
| 1931 | from lp.code.model.sourcepackagerecipebuild import SourcePackageRecipeBuildJob |
| 1932 | +from lp.code.tests.helpers import recipe_parser_newest_version |
| 1933 | from lp.registry.interfaces.pocket import PackagePublishingPocket |
| 1934 | from lp.services.job.interfaces.job import ( |
| 1935 | IJob, |
| 1936 | @@ -246,11 +247,13 @@ |
| 1937 | old_branches, list(sp_recipe.getReferencedBranches())) |
| 1938 | |
| 1939 | def test_reject_newer_formats(self): |
| 1940 | - builder_recipe = self.factory.makeRecipe() |
| 1941 | - builder_recipe.format = 0.3 |
| 1942 | - self.assertRaises( |
| 1943 | - TooNewRecipeFormat, |
| 1944 | - self.factory.makeSourcePackageRecipe, recipe=str(builder_recipe)) |
| 1945 | + with recipe_parser_newest_version(145.115): |
| 1946 | + builder_recipe = self.factory.makeRecipe() |
| 1947 | + builder_recipe.format = 145.115 |
| 1948 | + self.assertRaises( |
| 1949 | + TooNewRecipeFormat, |
| 1950 | + self.factory.makeSourcePackageRecipe, |
| 1951 | + recipe=str(builder_recipe)) |
| 1952 | |
| 1953 | def test_requestBuild(self): |
| 1954 | recipe = self.factory.makeSourcePackageRecipe() |
| 1955 | |
| 1956 | === modified file 'lib/lp/code/tests/helpers.py' |
| 1957 | --- lib/lp/code/tests/helpers.py 2010-09-15 20:41:46 +0000 |
| 1958 | +++ lib/lp/code/tests/helpers.py 2010-09-23 11:12:49 +0000 |
| 1959 | @@ -14,11 +14,13 @@ |
| 1960 | ] |
| 1961 | |
| 1962 | |
| 1963 | +from contextlib import contextmanager |
| 1964 | from datetime import timedelta |
| 1965 | from difflib import unified_diff |
| 1966 | from itertools import count |
| 1967 | import transaction |
| 1968 | |
| 1969 | +from bzrlib.plugins.builder.recipe import RecipeParser |
| 1970 | from zope.component import getUtility |
| 1971 | from zope.security.proxy import ( |
| 1972 | isinstance as zisinstance, |
| 1973 | @@ -301,3 +303,13 @@ |
| 1974 | make_project_branch_with_revisions( |
| 1975 | factory, gen, project, revision_count=num_commits) |
| 1976 | transaction.commit() |
| 1977 | + |
| 1978 | + |
| 1979 | +@contextmanager |
| 1980 | +def recipe_parser_newest_version(version): |
| 1981 | + old_version = RecipeParser.NEWEST_VERSION |
| 1982 | + RecipeParser.NEWEST_VERSION = version |
| 1983 | + try: |
| 1984 | + yield |
| 1985 | + finally: |
| 1986 | + RecipeParser.NEWEST_VERSION = old_version |
| 1987 | |
| 1988 | === modified file 'lib/lp/codehosting/branchdistro.py' |
| 1989 | --- lib/lp/codehosting/branchdistro.py 2010-09-21 03:47:19 +0000 |
| 1990 | +++ lib/lp/codehosting/branchdistro.py 2010-09-23 11:12:49 +0000 |
| 1991 | @@ -27,7 +27,7 @@ |
| 1992 | from zope.component import getUtility |
| 1993 | |
| 1994 | from canonical.config import config |
| 1995 | -from canonical.launchpad.interfaces import ILaunchpadCelebrities |
| 1996 | +from canonical.launchpad.interfaces import ILaunchpadCelebrities, IMasterStore |
| 1997 | from lp.code.enums import BranchLifecycleStatus, BranchType |
| 1998 | from lp.code.errors import BranchExists |
| 1999 | from lp.code.interfaces.branchcollection import IAllBranches |
| 2000 | @@ -35,6 +35,7 @@ |
| 2001 | from lp.code.interfaces.seriessourcepackagebranch import ( |
| 2002 | IFindOfficialBranchLinks, |
| 2003 | ) |
| 2004 | +from lp.code.model.branchrevision import BranchRevision |
| 2005 | from lp.codehosting.vfs import branch_id_to_path |
| 2006 | from lp.registry.interfaces.distribution import IDistributionSet |
| 2007 | from lp.registry.interfaces.pocket import PackagePublishingPocket |
| 2008 | @@ -350,4 +351,25 @@ |
| 2009 | switch_branches( |
| 2010 | config.codehosting.mirrored_branches_root, |
| 2011 | 'lp-internal', old_db_branch, new_db_branch) |
| 2012 | + # Directly copy the branch revisions from the old branch to the new branch. |
| 2013 | + store = IMasterStore(BranchRevision) |
| 2014 | + store.execute( |
| 2015 | + """ |
| 2016 | + INSERT INTO BranchRevision (branch, revision, sequence) |
| 2017 | + SELECT %s, BranchRevision.revision, BranchRevision.sequence |
| 2018 | + FROM BranchRevision |
| 2019 | + WHERE branch = %s |
| 2020 | + """ % (new_db_branch.id, old_db_branch.id)) |
| 2021 | + |
| 2022 | + # Update the scanned details first, that way when hooking into |
| 2023 | + # branchChanged, it won't try to create a new scan job. |
| 2024 | + tip_revision = old_db_branch.getTipRevision() |
| 2025 | + new_db_branch.updateScannedDetails( |
| 2026 | + tip_revision, old_db_branch.revision_count) |
| 2027 | + new_db_branch.branchChanged( |
| 2028 | + '', tip_revision.revision_id, |
| 2029 | + old_db_branch.control_format, |
| 2030 | + old_db_branch.branch_format, |
| 2031 | + old_db_branch.repository_format) |
| 2032 | + transaction.commit() |
| 2033 | return new_db_branch |
| 2034 | |
| 2035 | === modified file 'lib/lp/codehosting/tests/test_branchdistro.py' |
| 2036 | --- lib/lp/codehosting/tests/test_branchdistro.py 2010-09-20 04:32:49 +0000 |
| 2037 | +++ lib/lp/codehosting/tests/test_branchdistro.py 2010-09-23 11:12:49 +0000 |
| 2038 | @@ -27,6 +27,8 @@ |
| 2039 | from bzrlib.transport.chroot import ChrootServer |
| 2040 | from lazr.uri import URI |
| 2041 | import transaction |
| 2042 | +from zope.component import getUtility |
| 2043 | +from zope.security.proxy import removeSecurityProxy |
| 2044 | |
| 2045 | from canonical.config import config |
| 2046 | from canonical.launchpad.scripts.logger import ( |
| 2047 | @@ -35,6 +37,7 @@ |
| 2048 | ) |
| 2049 | from canonical.testing.layers import LaunchpadZopelessLayer |
| 2050 | from lp.code.enums import BranchLifecycleStatus |
| 2051 | +from lp.code.interfaces.branchjob import IBranchScanJobSource |
| 2052 | from lp.codehosting.branchdistro import ( |
| 2053 | DistroBrancher, |
| 2054 | switch_branches, |
| 2055 | @@ -127,6 +130,7 @@ |
| 2056 | """Make an official package branch with an underlying bzr branch.""" |
| 2057 | db_branch = self.factory.makePackageBranch(distroseries=distroseries) |
| 2058 | db_branch.sourcepackage.setBranch(RELEASE, db_branch, db_branch.owner) |
| 2059 | + self.factory.makeRevisionsForBranch(db_branch, count=1) |
| 2060 | |
| 2061 | transaction.commit() |
| 2062 | |
| 2063 | @@ -241,6 +245,42 @@ |
| 2064 | self.assertEqual( |
| 2065 | BranchLifecycleStatus.MATURE, db_branch.lifecycle_status) |
| 2066 | |
| 2067 | + def test_makeOneNewBranch_avoids_need_for_scan(self): |
| 2068 | + # makeOneNewBranch sets the appropriate properties of the new branch |
| 2069 | + # so a scan is unnecessary. This can be done because we are making a |
| 2070 | + # copy of the source branch. |
| 2071 | + db_branch = self.makeOfficialPackageBranch() |
| 2072 | + self.factory.makeRevisionsForBranch(db_branch, count=10) |
| 2073 | + tip_revision_id = db_branch.last_mirrored_id |
| 2074 | + self.assertIsNot(None, tip_revision_id) |
| 2075 | + # The makeRevisionsForBranch will create a scan job for the db_branch. |
| 2076 | + # We don't really care about that, but what we do care about is that |
| 2077 | + # no new jobs are created. |
| 2078 | + existing_scan_job_count = len( |
| 2079 | + list(getUtility(IBranchScanJobSource).iterReady())) |
| 2080 | + |
| 2081 | + brancher = self.makeNewSeriesAndBrancher(db_branch.distroseries) |
| 2082 | + brancher.makeOneNewBranch(db_branch) |
| 2083 | + new_branch = brancher.new_distroseries.getSourcePackage( |
| 2084 | + db_branch.sourcepackage.name).getBranch(RELEASE) |
| 2085 | + |
| 2086 | + self.assertEqual(tip_revision_id, new_branch.last_mirrored_id) |
| 2087 | + self.assertEqual(tip_revision_id, new_branch.last_scanned_id) |
| 2088 | + # Make sure that the branch revisions have been copied. |
| 2089 | + old_ancestry, old_history = removeSecurityProxy( |
| 2090 | + db_branch).getScannerData() |
| 2091 | + new_ancestry, new_history = removeSecurityProxy( |
| 2092 | + new_branch).getScannerData() |
| 2093 | + self.assertEqual(old_ancestry, new_ancestry) |
| 2094 | + self.assertEqual(old_history, new_history) |
| 2095 | + self.assertFalse(new_branch.pending_writes) |
| 2096 | + # The script doesn't have permission to create branch jobs, but just |
| 2097 | + # to be insanely paradoid. |
| 2098 | + transaction.commit() |
| 2099 | + self.layer.switchDbUser('launchpad') |
| 2100 | + scan_jobs = list(getUtility(IBranchScanJobSource).iterReady()) |
| 2101 | + self.assertEqual(existing_scan_job_count, len(scan_jobs)) |
| 2102 | + |
| 2103 | def test_makeOneNewBranch_inconsistent_branch(self): |
| 2104 | # makeOneNewBranch skips over an inconsistent official package branch |
| 2105 | # (see `checkConsistentOfficialPackageBranch` for precisely what an |
| 2106 | |
| 2107 | === modified file 'lib/lp/registry/browser/product.py' |
| 2108 | --- lib/lp/registry/browser/product.py 2010-09-15 16:03:14 +0000 |
| 2109 | +++ lib/lp/registry/browser/product.py 2010-09-23 11:12:49 +0000 |
| 2110 | @@ -468,6 +468,7 @@ |
| 2111 | # Add the branch configuration in separately. |
| 2112 | set_branch = series_menu['set_branch'] |
| 2113 | set_branch.text = 'Configure project branch' |
| 2114 | + set_branch.summary = "Specify the location of this projects code." |
| 2115 | set_branch.configured = ( |
| 2116 | ) |
| 2117 | config_list.append( |
| 2118 | |
| 2119 | === modified file 'lib/lp/registry/configure.zcml' |
| 2120 | --- lib/lp/registry/configure.zcml 2010-09-23 02:15:42 +0000 |
| 2121 | +++ lib/lp/registry/configure.zcml 2010-09-23 11:12:49 +0000 |
| 2122 | @@ -453,6 +453,7 @@ |
| 2123 | getReleasesAndPublishingHistory |
| 2124 | upstream_product |
| 2125 | target_type_display |
| 2126 | + _getOfficialTagClause |
| 2127 | official_bug_tags |
| 2128 | findRelatedArchives |
| 2129 | findRelatedArchivePublications |
| 2130 | |
| 2131 | === modified file 'lib/lp/registry/interfaces/product.py' |
| 2132 | --- lib/lp/registry/interfaces/product.py 2010-09-09 17:02:33 +0000 |
| 2133 | +++ lib/lp/registry/interfaces/product.py 2010-09-23 11:12:49 +0000 |
| 2134 | @@ -660,6 +660,7 @@ |
| 2135 | 'The series that represents the master or trunk branch. ' |
| 2136 | 'The Bazaar URL lp:<project> points to the development focus ' |
| 2137 | 'series branch.'))) |
| 2138 | + development_focusID = Attribute("The development focus ID.") |
| 2139 | |
| 2140 | name_with_project = Attribute(_("Returns the product name prefixed " |
| 2141 | "by the project name, if a project is associated with this " |
| 2142 | |
| 2143 | === modified file 'lib/lp/registry/interfaces/projectgroup.py' |
| 2144 | --- lib/lp/registry/interfaces/projectgroup.py 2010-08-21 18:41:57 +0000 |
| 2145 | +++ lib/lp/registry/interfaces/projectgroup.py 2010-09-23 11:12:49 +0000 |
| 2146 | @@ -328,6 +328,8 @@ |
| 2147 | def getSeries(series_name): |
| 2148 | """Return a ProjectGroupSeries object with name `series_name`.""" |
| 2149 | |
| 2150 | + product_milestones = Attribute('all the milestones for all the products.') |
| 2151 | + |
| 2152 | |
| 2153 | class IProjectGroup(IProjectGroupPublic, |
| 2154 | IProjectGroupModerate, |
| 2155 | |
| 2156 | === modified file 'lib/lp/registry/model/distributionsourcepackage.py' |
| 2157 | --- lib/lp/registry/model/distributionsourcepackage.py 2010-09-12 17:43:49 +0000 |
| 2158 | +++ lib/lp/registry/model/distributionsourcepackage.py 2010-09-23 11:12:49 +0000 |
| 2159 | @@ -494,6 +494,9 @@ |
| 2160 | BugTask.sourcepackagename == self.sourcepackagename), |
| 2161 | user) |
| 2162 | |
| 2163 | + def _getOfficialTagClause(self): |
| 2164 | + return self.distribution._getOfficialTagClause() |
| 2165 | + |
| 2166 | @property |
| 2167 | def official_bug_tags(self): |
| 2168 | """See `IHasBugs`.""" |
| 2169 | |
| 2170 | === modified file 'lib/lp/registry/model/distroseries.py' |
| 2171 | --- lib/lp/registry/model/distroseries.py 2010-09-21 19:34:45 +0000 |
| 2172 | +++ lib/lp/registry/model/distroseries.py 2010-09-23 11:12:49 +0000 |
| 2173 | @@ -743,6 +743,9 @@ |
| 2174 | """Customize `search_params` for this distribution series.""" |
| 2175 | search_params.setDistroSeries(self) |
| 2176 | |
| 2177 | + def _getOfficialTagClause(self): |
| 2178 | + return self.distribution._getOfficialTagClause() |
| 2179 | + |
| 2180 | @property |
| 2181 | def official_bug_tags(self): |
| 2182 | """See `IHasBugs`.""" |
| 2183 | |
| 2184 | === modified file 'lib/lp/registry/model/milestone.py' |
| 2185 | --- lib/lp/registry/model/milestone.py 2010-09-14 15:04:06 +0000 |
| 2186 | +++ lib/lp/registry/model/milestone.py 2010-09-23 11:12:49 +0000 |
| 2187 | @@ -15,6 +15,7 @@ |
| 2188 | |
| 2189 | import datetime |
| 2190 | |
| 2191 | +from lazr.restful.error import expose |
| 2192 | from sqlobject import ( |
| 2193 | AND, |
| 2194 | BoolCol, |
| 2195 | @@ -28,6 +29,7 @@ |
| 2196 | And, |
| 2197 | Store, |
| 2198 | ) |
| 2199 | +from storm.zope import IResultSet |
| 2200 | from zope.component import getUtility |
| 2201 | from zope.interface import implements |
| 2202 | |
| 2203 | @@ -36,7 +38,6 @@ |
| 2204 | sqlvalues, |
| 2205 | ) |
| 2206 | from canonical.launchpad.webapp.sorting import expand_numbers |
| 2207 | -from lazr.restful.error import expose |
| 2208 | from lp.app.errors import NotFoundError |
| 2209 | from lp.blueprints.model.specification import Specification |
| 2210 | from lp.bugs.interfaces.bugtarget import IHasBugs |
| 2211 | @@ -107,8 +108,7 @@ |
| 2212 | result = store.find(Milestone, self._getMilestoneCondition()) |
| 2213 | return result.order_by(self._milestone_order) |
| 2214 | |
| 2215 | - @property |
| 2216 | - def milestones(self): |
| 2217 | + def _get_milestones(self): |
| 2218 | """See `IHasMilestones`.""" |
| 2219 | store = Store.of(self) |
| 2220 | result = store.find(Milestone, |
| 2221 | @@ -116,6 +116,8 @@ |
| 2222 | Milestone.active == True)) |
| 2223 | return result.order_by(self._milestone_order) |
| 2224 | |
| 2225 | + milestones = property(_get_milestones) |
| 2226 | + |
| 2227 | |
| 2228 | class MultipleProductReleases(Exception): |
| 2229 | """Raised when a second ProductRelease is created for a milestone.""" |
| 2230 | @@ -229,7 +231,8 @@ |
| 2231 | """See `IMilestone`.""" |
| 2232 | params = BugTaskSearchParams(milestone=self, user=None) |
| 2233 | bugtasks = getUtility(IBugTaskSet).search(params) |
| 2234 | - assert len(self.getSubscriptions()) == 0, ( |
| 2235 | + subscriptions = IResultSet(self.getSubscriptions()) |
| 2236 | + assert subscriptions.is_empty(), ( |
| 2237 | "You cannot delete a milestone which has structural " |
| 2238 | "subscriptions.") |
| 2239 | assert bugtasks.count() == 0, ( |
| 2240 | @@ -256,7 +259,7 @@ |
| 2241 | """See lp.registry.interfaces.milestone.IMilestoneSet.""" |
| 2242 | try: |
| 2243 | return Milestone.get(milestoneid) |
| 2244 | - except SQLObjectNotFound, err: |
| 2245 | + except SQLObjectNotFound: |
| 2246 | raise NotFoundError( |
| 2247 | "Milestone with ID %d does not exist" % milestoneid) |
| 2248 | |
| 2249 | |
| 2250 | === modified file 'lib/lp/registry/model/person.py' |
| 2251 | --- lib/lp/registry/model/person.py 2010-09-10 20:27:19 +0000 |
| 2252 | +++ lib/lp/registry/model/person.py 2010-09-23 11:12:49 +0000 |
| 2253 | @@ -1265,6 +1265,8 @@ |
| 2254 | # The owner is not a member but must retain his rights over |
| 2255 | # this team. This person may be a member of the owner, and in this |
| 2256 | # case it'll also have rights over this team. |
| 2257 | + # Note that this query and the tp query above can be consolidated |
| 2258 | + # when we get to a finer grained level of optimisations. |
| 2259 | in_team = self.inTeam(team.teamowner) |
| 2260 | else: |
| 2261 | in_team = False |
| 2262 | |
| 2263 | === modified file 'lib/lp/registry/model/productseries.py' |
| 2264 | --- lib/lp/registry/model/productseries.py 2010-09-21 19:34:45 +0000 |
| 2265 | +++ lib/lp/registry/model/productseries.py 2010-09-23 11:12:49 +0000 |
| 2266 | @@ -423,6 +423,9 @@ |
| 2267 | """Customize `search_params` for this product series.""" |
| 2268 | search_params.setProductSeries(self) |
| 2269 | |
| 2270 | + def _getOfficialTagClause(self): |
| 2271 | + return self.product._getOfficialTagClause() |
| 2272 | + |
| 2273 | @property |
| 2274 | def official_bug_tags(self): |
| 2275 | """See `IHasBugs`.""" |
| 2276 | |
| 2277 | === modified file 'lib/lp/registry/model/projectgroup.py' |
| 2278 | --- lib/lp/registry/model/projectgroup.py 2010-09-09 15:26:11 +0000 |
| 2279 | +++ lib/lp/registry/model/projectgroup.py 2010-09-23 11:12:49 +0000 |
| 2280 | @@ -93,6 +93,7 @@ |
| 2281 | from lp.registry.model.karma import KarmaContextMixin |
| 2282 | from lp.registry.model.mentoringoffer import MentoringOffer |
| 2283 | from lp.registry.model.milestone import ( |
| 2284 | + HasMilestonesMixin, |
| 2285 | Milestone, |
| 2286 | ProjectMilestone, |
| 2287 | ) |
| 2288 | @@ -110,7 +111,8 @@ |
| 2289 | MakesAnnouncements, HasSprintsMixin, HasAliasMixin, |
| 2290 | KarmaContextMixin, BranchVisibilityPolicyMixin, |
| 2291 | StructuralSubscriptionTargetMixin, |
| 2292 | - HasBranchesMixin, HasMergeProposalsMixin, HasBugHeatMixin): |
| 2293 | + HasBranchesMixin, HasMergeProposalsMixin, HasBugHeatMixin, |
| 2294 | + HasMilestonesMixin): |
| 2295 | """A ProjectGroup""" |
| 2296 | |
| 2297 | implements(IProjectGroup, IFAQCollection, IHasBugHeat, IHasIcon, IHasLogo, |
| 2298 | @@ -309,6 +311,11 @@ |
| 2299 | """Customize `search_params` for this milestone.""" |
| 2300 | search_params.setProject(self) |
| 2301 | |
| 2302 | + def _getOfficialTagClause(self): |
| 2303 | + """See `OfficialBugTagTargetMixin`.""" |
| 2304 | + And(ProjectGroup.id == Product.projectID, |
| 2305 | + Product.id == OfficialBugTag.productID) |
| 2306 | + |
| 2307 | @property |
| 2308 | def official_bug_tags(self): |
| 2309 | """See `IHasBugs`.""" |
| 2310 | @@ -396,6 +403,11 @@ |
| 2311 | """ |
| 2312 | return self.products.count() != 0 |
| 2313 | |
| 2314 | + def _getMilestoneCondition(self): |
| 2315 | + """See `HasMilestonesMixin`.""" |
| 2316 | + return And(Milestone.productID == Product.id, |
| 2317 | + Product.projectID == self.id) |
| 2318 | + |
| 2319 | def _getMilestones(self, only_active): |
| 2320 | """Return a list of milestones for this project group. |
| 2321 | |
| 2322 | @@ -445,6 +457,13 @@ |
| 2323 | return self._getMilestones(True) |
| 2324 | |
| 2325 | @property |
| 2326 | + def product_milestones(self): |
| 2327 | + """Hack to avoid the ProjectMilestone in MilestoneVocabulary.""" |
| 2328 | + # XXX: bug=644977 Robert Collins - this is a workaround for |
| 2329 | + # insconsistency in project group milestone use. |
| 2330 | + return self._get_milestones() |
| 2331 | + |
| 2332 | + @property |
| 2333 | def all_milestones(self): |
| 2334 | """See `IProjectGroup`.""" |
| 2335 | return self._getMilestones(False) |
| 2336 | |
| 2337 | === modified file 'lib/lp/registry/model/sourcepackage.py' |
| 2338 | --- lib/lp/registry/model/sourcepackage.py 2010-09-07 00:51:44 +0000 |
| 2339 | +++ lib/lp/registry/model/sourcepackage.py 2010-09-23 11:12:49 +0000 |
| 2340 | @@ -481,6 +481,9 @@ |
| 2341 | """Customize `search_params` for this source package.""" |
| 2342 | search_params.setSourcePackage(self) |
| 2343 | |
| 2344 | + def _getOfficialTagClause(self): |
| 2345 | + return self.distroseries._getOfficialTagClause() |
| 2346 | + |
| 2347 | @property |
| 2348 | def official_bug_tags(self): |
| 2349 | """See `IHasBugs`.""" |
| 2350 | |
| 2351 | === modified file 'lib/lp/registry/model/structuralsubscription.py' |
| 2352 | --- lib/lp/registry/model/structuralsubscription.py 2010-08-20 20:31:18 +0000 |
| 2353 | +++ lib/lp/registry/model/structuralsubscription.py 2010-09-23 11:12:49 +0000 |
| 2354 | @@ -266,47 +266,36 @@ |
| 2355 | min_blueprint_notification_level= |
| 2356 | BlueprintNotificationLevel.NOTHING): |
| 2357 | """See `IStructuralSubscriptionTarget`.""" |
| 2358 | - target_clause_parts = [] |
| 2359 | - for key, value in self._target_args.items(): |
| 2360 | + clauses = [ |
| 2361 | + "StructuralSubscription.subscriber = Person.id", |
| 2362 | + "StructuralSubscription.bug_notification_level " |
| 2363 | + ">= %s" % quote(min_bug_notification_level), |
| 2364 | + "StructuralSubscription.blueprint_notification_level " |
| 2365 | + ">= %s" % quote(min_blueprint_notification_level), |
| 2366 | + ] |
| 2367 | + for key, value in self._target_args.iteritems(): |
| 2368 | if value is None: |
| 2369 | - target_clause_parts.append( |
| 2370 | - "StructuralSubscription.%s IS NULL " % (key, )) |
| 2371 | + clauses.append( |
| 2372 | + "StructuralSubscription.%s IS NULL" % (key,)) |
| 2373 | else: |
| 2374 | - target_clause_parts.append( |
| 2375 | - "StructuralSubscription.%s = %s " % (key, quote(value))) |
| 2376 | - target_clause = " AND ".join(target_clause_parts) |
| 2377 | - query = target_clause + """ |
| 2378 | - AND StructuralSubscription.subscriber = Person.id |
| 2379 | - """ |
| 2380 | - all_subscriptions = StructuralSubscription.select( |
| 2381 | - query, |
| 2382 | - orderBy='Person.displayname', |
| 2383 | - clauseTables=['Person']) |
| 2384 | - subscriptions = [sub for sub |
| 2385 | - in all_subscriptions |
| 2386 | - if ((sub.bug_notification_level >= |
| 2387 | - min_bug_notification_level) and |
| 2388 | - (sub.blueprint_notification_level >= |
| 2389 | - min_blueprint_notification_level))] |
| 2390 | - return subscriptions |
| 2391 | + clauses.append( |
| 2392 | + "StructuralSubscription.%s = %s" % (key, quote(value))) |
| 2393 | + query = " AND ".join(clauses) |
| 2394 | + return StructuralSubscription.select( |
| 2395 | + query, orderBy='Person.displayname', clauseTables=['Person']) |
| 2396 | |
| 2397 | def getBugNotificationsRecipients(self, recipients=None, level=None): |
| 2398 | """See `IStructuralSubscriptionTarget`.""" |
| 2399 | - subscribers = set() |
| 2400 | if level is None: |
| 2401 | subscriptions = self.bug_subscriptions |
| 2402 | else: |
| 2403 | subscriptions = self.getSubscriptions( |
| 2404 | min_bug_notification_level=level) |
| 2405 | - for subscription in subscriptions: |
| 2406 | - if (level is not None and |
| 2407 | - subscription.bug_notification_level < level): |
| 2408 | - continue |
| 2409 | - subscriber = subscription.subscriber |
| 2410 | - subscribers.add(subscriber) |
| 2411 | - if recipients is not None: |
| 2412 | - recipients.addStructuralSubscriber( |
| 2413 | - subscriber, self) |
| 2414 | + subscribers = set( |
| 2415 | + subscription.subscriber for subscription in subscriptions) |
| 2416 | + if recipients is not None: |
| 2417 | + for subscriber in subscribers: |
| 2418 | + recipients.addStructuralSubscriber(subscriber, self) |
| 2419 | parent = self.parent_subscription_target |
| 2420 | if parent is not None: |
| 2421 | subscribers.update( |
| 2422 | |
| 2423 | === modified file 'lib/lp/registry/vocabularies.py' |
| 2424 | --- lib/lp/registry/vocabularies.py 2010-08-31 11:11:09 +0000 |
| 2425 | +++ lib/lp/registry/vocabularies.py 2010-09-23 11:12:49 +0000 |
| 2426 | @@ -1161,16 +1161,18 @@ |
| 2427 | elif IDistroSeriesBugTask.providedBy(milestone_context): |
| 2428 | target = milestone_context.distroseries |
| 2429 | elif IProductSeriesBugTask.providedBy(milestone_context): |
| 2430 | - target = milestone_context.productseries |
| 2431 | + target = milestone_context.productseries.product |
| 2432 | elif IDistributionSourcePackage.providedBy(milestone_context): |
| 2433 | target = milestone_context.distribution |
| 2434 | elif ISourcePackage.providedBy(milestone_context): |
| 2435 | target = milestone_context.distroseries |
| 2436 | elif ISpecification.providedBy(milestone_context): |
| 2437 | target = milestone_context.target |
| 2438 | + elif IProductSeries.providedBy(milestone_context): |
| 2439 | + # Show all the milestones of the product for a product series. |
| 2440 | + target = milestone_context.product |
| 2441 | elif (IProjectGroup.providedBy(milestone_context) or |
| 2442 | IProduct.providedBy(milestone_context) or |
| 2443 | - IProductSeries.providedBy(milestone_context) or |
| 2444 | IDistribution.providedBy(milestone_context) or |
| 2445 | IDistroSeries.providedBy(milestone_context)): |
| 2446 | target = milestone_context |
| 2447 | @@ -1196,23 +1198,10 @@ |
| 2448 | # should be revisited after we've unblocked users. |
| 2449 | if target is not None: |
| 2450 | if IProjectGroup.providedBy(target): |
| 2451 | - milestones = shortlist( |
| 2452 | - (milestone for product in target.products |
| 2453 | - for milestone in product.milestones), |
| 2454 | - longest_expected=40) |
| 2455 | - elif IProductSeries.providedBy(target): |
| 2456 | - # While some milestones may be associated with a |
| 2457 | - # productseries, we want to show all milestones for |
| 2458 | - # the product. Since the database constraint |
| 2459 | - # "valid_target" ensures that a milestone associated |
| 2460 | - # with a series is also associated with the product |
| 2461 | - # itself, we don't need to look up series-related |
| 2462 | - # milestones. |
| 2463 | - milestones = shortlist(target.product.milestones, |
| 2464 | - longest_expected=40) |
| 2465 | + milestones_source = target.product_milestones |
| 2466 | else: |
| 2467 | - milestones = shortlist( |
| 2468 | - target.milestones, longest_expected=40) |
| 2469 | + milestones_source = target.milestones |
| 2470 | + milestones = shortlist(milestones_source, longest_expected=40) |
| 2471 | else: |
| 2472 | # We can't use context to reasonably filter the |
| 2473 | # milestones, so let's either just grab all of them, |
| 2474 | @@ -1249,9 +1238,11 @@ |
| 2475 | product_ids = set( |
| 2476 | removeSecurityProxy(milestone).productID |
| 2477 | for milestone in milestones) |
| 2478 | + product_ids.discard(None) |
| 2479 | distro_ids = set( |
| 2480 | removeSecurityProxy(milestone).distributionID |
| 2481 | for milestone in milestones) |
| 2482 | + distro_ids.discard(None) |
| 2483 | if len(product_ids) > 0: |
| 2484 | list(Product.select("id IN %s" % sqlvalues(product_ids))) |
| 2485 | if len(distro_ids) > 0: |
| 2486 | |
| 2487 | === modified file 'lib/lp/soyuz/tests/test_binarypackagebuildbehavior.py' |
| 2488 | --- lib/lp/soyuz/tests/test_binarypackagebuildbehavior.py 2010-09-22 11:29:08 +0000 |
| 2489 | +++ lib/lp/soyuz/tests/test_binarypackagebuildbehavior.py 2010-09-23 11:12:49 +0000 |
| 2490 | @@ -94,7 +94,7 @@ |
| 2491 | extra_urls = [] |
| 2492 | |
| 2493 | upload_logs = [ |
| 2494 | - ['cacheFile', 'sendFileToSlave', ('ensurepresent', url, '', '')] |
| 2495 | + ('ensurepresent', url, '', '') |
| 2496 | for url in [chroot.http_url] + extra_urls] |
| 2497 | |
| 2498 | extra_args = { |
| 2499 | @@ -110,7 +110,7 @@ |
| 2500 | build_log = [ |
| 2501 | ('build', build_id, 'binarypackage', chroot.content.sha1, |
| 2502 | filemap_names, extra_args)] |
| 2503 | - return sum(upload_logs, []) + build_log |
| 2504 | + return upload_logs + build_log |
| 2505 | |
| 2506 | def startBuild(self, builder, candidate): |
| 2507 | builder = removeSecurityProxy(builder) |

Hi Gavin,
This branch looks good, but I have a few comments below.
-Edwin
>=== modified file 'lib/lp/ bugs/model/ bugsubscription .py' bugs/model/ bugsubscription .py 2010-09-09 21:00:54 +0000 bugs/model/ bugsubscription .py 2010-09-15 18:09:22 +0000 is_team: self.person)
>--- lib/lp/
>+++ lib/lp/
>@@ -66,3 +80,63 @@
> if self.person.
> return user.inTeam(
> return user == self.person
Unless something has changed, each model class with its own table
needs to have its own file.
>+class BugSubscription Filter( Storm): nFilter" subscription_ id = Int("structural subscription" , allow_none=False)
>+ """A filter to specialize a *structural* subscription."""
>+
>+ __storm_table__ = "BugSubscriptio
>+
>+ id = Int(primary=True)
>+
>+ structural_
Long line.
>+ structural_ subscription = Reference( subscription_ id, "StructuralSubs cription. id") none=False, default=False) none=False, default=False) none=False, default=False) FilterStatus( Storm): nFilterStatus" filter_ id, "BugSubscriptio nFilter. id") enum=BugTaskSta tus, allow_none=False) FilterImportanc e(Storm) : nFilterImportan ce" filter_ id, "BugSubscriptio nFilter. id") enum=BugTaskImp ortance, allow_none=False) FilterTag( Storm): nFilterTag" filter_ id, "BugSubscriptio nFilter. id") none=False) allow_none= False) bugs/model/ tests' bugs/model/ tests/_ _init__ .py' bugs/model/ tests/test_ bugsubscription .py' bugs/model/ tests/test_ bugsubscription .py 1970-01-01 00:00:00 +0000 bugs/model/ tests/test_ bugsubscription .py 2010-09-15 18:09:22 +0000 launchpad. interfaces. lpstorm import IStore nalLayer interfaces. bugtask import ( model.bugsubscr iption import ( Filter, FilterImportanc e, FilterStatus, FilterTa. ..
>+ structural_
>+
>+ find_all_tags = Bool(allow_
>+ include_any_tags = Bool(allow_
>+ exclude_any_tags = Bool(allow_
>+
>+ other_parameters = Unicode()
>+
>+ description = Unicode()
>+
>+
>+class BugSubscription
>+ """Statuses to filter."""
>+
>+ __storm_table__ = "BugSubscriptio
>+
>+ id = Int(primary=True)
>+
>+ filter_id = Int("filter", allow_none=False)
>+ filter = Reference(
>+
>+ status = DBEnum(
>+
>+
>+class BugSubscription
>+ """Importances to filter."""
>+
>+ __storm_table__ = "BugSubscriptio
>+
>+ id = Int(primary=True)
>+
>+ filter_id = Int("filter", allow_none=False)
>+ filter = Reference(
>+
>+ importance = DBEnum(
>+
>+
>+class BugSubscription
>+ """Tags to filter."""
>+
>+ __storm_table__ = "BugSubscriptio
>+
>+ id = Int(primary=True)
>+
>+ filter_id = Int("filter", allow_none=False)
>+ filter = Reference(
>+
>+ include = Bool(allow_
>+ tag = Unicode(
>
>=== added directory 'lib/lp/
>=== added file 'lib/lp/
>=== added file 'lib/lp/
>--- lib/lp/
>+++ lib/lp/
>@@ -0,0 +1,182 @@
>+# Copyright 2010 Canonical Ltd. This software is licensed under the
>+# GNU Affero General Public License version 3 (see the file LICENSE).
>+
>+"""Tests for the bugsubscription module."""
>+
>+__metaclass__ = type
>+
>+from canonical.
>+from canonical.testing import DatabaseFunctio
>+from lp.bugs.
>+ BugTaskImportance,
>+ BugTaskStatus,
>+ )
>+from lp.bugs.
>+ BugSubscription
>+ BugSubscription
>+ BugSubscription
>+ BugSubscription