launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #03419
[Merge] lp:~abentley/launchpad/translation-sharing-details-permissions into lp:launchpad
Aaron Bentley has proposed merging lp:~abentley/launchpad/translation-sharing-details-permissions into lp:launchpad.
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
Related bugs:
Bug #758920 in Launchpad itself: "On translate sharing details page, usage settings behaves badly with permission issues"
https://bugs.launchpad.net/launchpad/+bug/758920
For more details, see:
https://code.launchpad.net/~abentley/launchpad/translation-sharing-details-permissions/+merge/58746
= Summary =
Fix bug #758920 and other permission-related issues.
== Proposed fix ==
Disable checklist items if user does not have permission to change them.
== Pre-implementation notes ==
None
== Implementation details ==
Provide new user_authorized flag on CheckItem, and do not enable checkitems if user is not authorized.
To ensure consistent behaviour at initialization and update time, implement TranslationSharingController.set_permissions() that acts on a dict of values and sets user_authorized.
Call set_permissions from both configure() and select_productseries().
Extract getSharingDetailPermissions from setPackagingReturnSharingDetailPermissions(). Use getSharingDetailPermissions to configure the IJSONRequestCache. Add can_change_product_series to getSharingDetailPermissions Update tests.
Driveby: remove non-existent SourcePackageTranslationSharingStatus from __all__.
== Tests ==
firefox lib/lp/translations/javascript/tests/test_sourcepackage_sharing_details.html
bin/test -t SharingDetailPermissions -t test_cache_contents_no_productseries
== Demo and Q/A ==
For a sourcepackage that has no packaging link and you do not have permissions on, go to +sharing-details. Select a project series. The change/delete project series items should be shown. Log in as another user. The change/delete project series items should not be shown.
Similarly, if you have access to the product/productseries, you should be able to edit the remaining items. Otherwise, no.
= Launchpad lint =
Checking for conflicts and issues in changed files.
Linting changed files:
lib/lp/registry/interfaces/sourcepackage.py
lib/lp/registry/tests/test_sourcepackage.py
lib/lp/translations/javascript/tests/test_sourcepackage_sharing_details.js
lib/lp/translations/browser/tests/test_sharing_details.py
lib/lp/registry/model/sourcepackage.py
lib/lp/translations/javascript/sourcepackage_sharing_details.js
lib/lp/translations/browser/sourcepackage.py
--
https://code.launchpad.net/~abentley/launchpad/translation-sharing-details-permissions/+merge/58746
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~abentley/launchpad/translation-sharing-details-permissions into lp:launchpad.
=== modified file 'lib/lp/registry/interfaces/sourcepackage.py'
--- lib/lp/registry/interfaces/sourcepackage.py 2011-04-20 16:54:48 +0000
+++ lib/lp/registry/interfaces/sourcepackage.py 2011-04-21 20:16:31 +0000
@@ -212,11 +212,7 @@
@export_write_operation()
@operation_for_version('devel')
def setPackagingReturnSharingDetailPermissions(productseries, owner):
- """Like setPackaging(), but returns a dictionary which says
- if the current user can change the series' target branch,
- if he can change the translation usage settings of the
- product and if he can change the translation synchronisation
- setting of the series.
+ """Like setPackaging(), but returns getSharingDetailPermissions().
This method is intended for AJAX usage on the +sharing-details
page.
@@ -227,6 +223,16 @@
def deletePackaging():
"""Delete the packaging for this sourcepackage."""
+ def getSharingDetailPermissions(self):
+ """Return a dictionary of user permissions for +sharing-details page.
+
+ This shows whether the user can change
+ - The project series
+ - The project series target branch
+ - The project series autoimport mode
+ - The project translation usage setting
+ """
+
def getSuiteSourcePackage(pocket):
"""Return the `ISuiteSourcePackage` for this package in 'pocket'.
=== modified file 'lib/lp/registry/model/sourcepackage.py'
--- lib/lp/registry/model/sourcepackage.py 2011-04-20 15:41:17 +0000
+++ lib/lp/registry/model/sourcepackage.py 2011-04-21 20:16:31 +0000
@@ -549,14 +549,35 @@
owner):
"""See `ISourcePackage`."""
self.setPackaging(productseries, owner)
+ return self.getSharingDetailPermissions()
+
+ def getSharingDetailPermissions(self):
user = getUtility(ILaunchBag).user
- return {
- 'user_can_change_branch': user.canWrite(productseries, 'branch'),
- 'user_can_change_translation_usage':
- user.canWrite(productseries.product, 'translations_usage'),
- 'user_can_change_translations_autoimport_mode':
- user.canWrite(productseries, 'translations_autoimport_mode'),
- }
+ productseries = self.productseries
+ permissions = {
+ 'user_can_change_product_series': False,
+ 'user_can_change_branch': False,
+ 'user_can_change_translation_usage': False,
+ 'user_can_change_translations_autoimport_mode': False}
+ if user is None:
+ pass
+ elif productseries is None:
+ permissions['user_can_change_product_series'] = user.canAccess(
+ self, 'setPackaging')
+ else:
+ permissions.update({
+ 'user_can_change_product_series':
+ self.direct_packaging.userCanDelete(),
+ 'user_can_change_branch':
+ user.canWrite(productseries, 'branch'),
+ 'user_can_change_translation_usage':
+ user.canWrite(
+ productseries.product, 'translations_usage'),
+ 'user_can_change_translations_autoimport_mode':
+ user.canWrite(
+ productseries, 'translations_autoimport_mode'),
+ })
+ return permissions
def deletePackaging(self):
"""See `ISourcePackage`."""
=== modified file 'lib/lp/registry/tests/test_sourcepackage.py'
--- lib/lp/registry/tests/test_sourcepackage.py 2011-04-20 15:41:17 +0000
+++ lib/lp/registry/tests/test_sourcepackage.py 2011-04-21 20:16:31 +0000
@@ -340,26 +340,60 @@
packaging_owner.canWrite(
productseries.product, 'translations_usage'))
expected = {
- 'user_can_change_branch': False,
- 'user_can_change_translation_usage': False,
- 'user_can_change_translations_autoimport_mode': False,
- }
- self.assertEqual(expected, permissions)
-
- def test_setPackagingReturnSharingDetailPermissions__series_owner(self):
+ 'user_can_change_product_series': True,
+ 'user_can_change_branch': False,
+ 'user_can_change_translation_usage': False,
+ 'user_can_change_translations_autoimport_mode': False,
+ }
+ self.assertEqual(expected, permissions)
+
+ def test_getSharingDetailPermissions__ordinary_user(self):
+ """An ordinary user cannot set the series' branch or translation
+ synchronisation settings, or the translation usage settings of the
+ product.
+ """
+ packaging = self.factory.makePackagingLink()
+ sourcepackage = packaging.sourcepackage
+ productseries = packaging.productseries
+ with person_logged_in(packaging.owner):
+ permissions = sourcepackage.getSharingDetailPermissions()
+ self.assertEqual(productseries, sourcepackage.productseries)
+ self.assertFalse(
+ packaging.owner.canWrite(productseries, 'branch'))
+ self.assertFalse(
+ packaging.owner.canWrite(
+ productseries, 'translations_autoimport_mode'))
+ self.assertFalse(
+ packaging.owner.canWrite(
+ productseries.product, 'translations_usage'))
+ expected = {
+ 'user_can_change_product_series': True,
+ 'user_can_change_branch': False,
+ 'user_can_change_translation_usage': False,
+ 'user_can_change_translations_autoimport_mode': False,
+ }
+ self.assertEqual(expected, permissions)
+
+ def makeDistinctOwnerProductSeries(self):
+ # Ensure productseries owner is distinct from product owner.
+ return self.factory.makeProductSeries(
+ owner=self.factory.makePerson())
+
+ def test_getSharingDetailPermissions__series_owner(self):
"""A product series owner can create a packaging link, and he can
set the series' branch or translation syncronisation settings,
but he cannot set the translation usage settings of the product.
"""
- series_owner = self.factory.makePerson()
- sourcepackage = self.factory.makeSourcePackage()
- product = self.factory.makeProduct()
+ productseries = self.makeDistinctOwnerProductSeries()
+ series_owner = productseries.owner
+ # Ensure productseries owner is distinct from product owner.
productseries = self.factory.makeProductSeries(
- product=product, owner=series_owner)
+ owner=series_owner)
with person_logged_in(series_owner):
- permissions = (
- sourcepackage.setPackagingReturnSharingDetailPermissions(
- productseries, series_owner))
+ packaging = self.factory.makePackagingLink(
+ productseries=productseries, owner=series_owner)
+ sourcepackage = packaging.sourcepackage
+ permissions = sourcepackage.getSharingDetailPermissions()
self.assertEqual(productseries, sourcepackage.productseries)
self.assertTrue(series_owner.canWrite(productseries, 'branch'))
self.assertTrue(
@@ -369,25 +403,25 @@
series_owner.canWrite(
productseries.product, 'translations_usage'))
expected = {
+ 'user_can_change_product_series': True,
'user_can_change_branch': True,
'user_can_change_translation_usage': False,
'user_can_change_translations_autoimport_mode': True,
}
self.assertEqual(expected, permissions)
- def test_setPackagingReturnSharingDetailPermissions__product_owner(self):
- """A product series owner can create a packaging link, and he can
- set the series' branch and the translation syncronisation settings,
- and the translation usage settings of the product.
+ def test_getSharingDetailPermissions__product_owner(self):
+ """A product owner can create a packaging link, and he can set the
+ series' branch and the translation syncronisation settings, and the
+ translation usage settings of the product.
"""
- sourcepackage = self.factory.makeSourcePackage()
- product = self.factory.makeProduct()
- productseries = self.factory.makeProductSeries(
- product=product, owner=self.factory.makePerson())
+ productseries = self.makeDistinctOwnerProductSeries()
+ product = productseries.product
with person_logged_in(product.owner):
- permissions = (
- sourcepackage.setPackagingReturnSharingDetailPermissions(
- productseries, product.owner))
+ packaging = self.factory.makePackagingLink(
+ productseries=productseries, owner=product.owner)
+ sourcepackage = packaging.sourcepackage
+ permissions = sourcepackage.getSharingDetailPermissions()
self.assertEqual(productseries, sourcepackage.productseries)
self.assertTrue(product.owner.canWrite(productseries, 'branch'))
self.assertTrue(
@@ -397,12 +431,58 @@
product.owner.canWrite(
productseries.product, 'translations_usage'))
expected = {
+ 'user_can_change_product_series': True,
'user_can_change_branch': True,
'user_can_change_translation_usage': True,
'user_can_change_translations_autoimport_mode': True,
}
self.assertEqual(expected, permissions)
+ def test_getSharingDetailPermissions_change_product(self):
+ """Test user_can_change_product_series.
+
+ Until a Packaging is created, anyone can change product series.
+ Afterward, random people cannot change product series.
+ """
+ sourcepackage = self.factory.makeSourcePackage()
+ person1 = self.factory.makePerson()
+ person2 = self.factory.makePerson()
+
+ def can_change_product_series():
+ return sourcepackage.getSharingDetailPermissions()[
+ 'user_can_change_product_series']
+ with person_logged_in(person1):
+ self.assertTrue(can_change_product_series())
+ with person_logged_in(person2):
+ self.assertTrue(can_change_product_series())
+ self.factory.makePackagingLink(
+ sourcepackage=sourcepackage, owner=person1)
+ with person_logged_in(person1):
+ self.assertTrue(can_change_product_series())
+ with person_logged_in(person2):
+ self.assertFalse(can_change_product_series())
+
+ def test_getSharingDetailPermissions_no_product_series(self):
+ sourcepackage = self.factory.makeSourcePackage()
+ expected = {
+ 'user_can_change_product_series': True,
+ 'user_can_change_branch': False,
+ 'user_can_change_translation_usage': False,
+ 'user_can_change_translations_autoimport_mode': False}
+ self.assertEqual(
+ expected, sourcepackage.getSharingDetailPermissions())
+
+ def test_getSharingDetailPermissions_no_user(self):
+ sourcepackage = self.factory.makeSourcePackage()
+ expected = {
+ 'user_can_change_product_series': False,
+ 'user_can_change_branch': False,
+ 'user_can_change_translation_usage': False,
+ 'user_can_change_translations_autoimport_mode': False}
+ logout()
+ self.assertEqual(
+ expected, sourcepackage.getSharingDetailPermissions())
+
class TestSourcePackageWebService(WebServiceTestCase):
=== modified file 'lib/lp/translations/browser/sourcepackage.py'
--- lib/lp/translations/browser/sourcepackage.py 2011-04-14 15:20:37 +0000
+++ lib/lp/translations/browser/sourcepackage.py 2011-04-21 20:16:31 +0000
@@ -8,7 +8,6 @@
__all__ = [
'SourcePackageTranslationsExportView',
'SourcePackageTranslationsView',
- 'SourcePackageTranslationSharingStatus',
]
@@ -145,6 +144,7 @@
'upstream_branch': self.upstream_branch,
'product': self.product,
})
+ cache.objects.update(self.context.getSharingDetailPermissions())
@property
def branch_link(self):
=== modified file 'lib/lp/translations/browser/tests/test_sharing_details.py'
--- lib/lp/translations/browser/tests/test_sharing_details.py 2011-04-14 15:20:37 +0000
+++ lib/lp/translations/browser/tests/test_sharing_details.py 2011-04-21 20:16:31 +0000
@@ -340,6 +340,10 @@
def test_cache_contents_no_productseries(self):
objects = self.getCacheObjects()
self.assertIs(None, objects['productseries'])
+ self.assertIn('user_can_change_product_series', objects)
+ self.assertIn('user_can_change_branch', objects)
+ self.assertIn('user_can_change_translation_usage', objects)
+ self.assertIn('user_can_change_translations_autoimport_mode', objects)
def test_cache_contents_no_branch(self):
self.configureSharing()
=== modified file 'lib/lp/translations/javascript/sourcepackage_sharing_details.js'
--- lib/lp/translations/javascript/sourcepackage_sharing_details.js 2011-04-18 13:39:13 +0000
+++ lib/lp/translations/javascript/sourcepackage_sharing_details.js 2011-04-21 20:16:31 +0000
@@ -19,12 +19,16 @@
// True if this item is enabled.
enabled: {
getter: function(){
+ if (!this.get('user_authorized')){
+ return false;
+ }
if (Y.Lang.isNull(this.get('dependency'))){
return true;
}
return this.get('dependency').get('complete');
}
},
+ user_authorized: {value: false},
// The HTML identifier of the item.
identifier: null,
pending: {value: false}
@@ -132,7 +136,7 @@
{identifier: 'upstream-sync', dependency: branch});
this.set('autoimport', autoimport);
var configuration = new CheckItem(
- {identifier: 'configuration'});
+ {identifier: 'configuration', user_authorized: true});
this.set('configuration', configuration);
this.set(
'all_items', [product_series, usage, branch, autoimport]);
@@ -313,6 +317,24 @@
this.replace_productseries(config.productseries);
this.replace_product(config.product);
this.set_branch(config.upstream_branch);
+ this.set_permissions(config);
+ },
+ set_permissions: function(permissions){
+ var usage = this.get('tsconfig').get('translations_usage');
+ usage.set(
+ 'user_authorized', permissions.user_can_change_translation_usage);
+ var branch = this.get('tsconfig').get('branch');
+ branch.set('user_authorized', permissions.user_can_change_branch);
+ var autoimport = this.get('tsconfig').get('autoimport');
+ autoimport.set(
+ 'user_authorized',
+ permissions.user_can_change_translations_autoimport_mode);
+ var product_series = this.get('tsconfig').get('product_series');
+ if (permissions.user_can_change_product_series !== undefined){
+ product_series.set(
+ 'user_authorized',
+ permissions.user_can_change_product_series);
+ }
},
set_productseries: function(productseries) {
if (Y.Lang.isValue(productseries)){
@@ -381,9 +403,11 @@
var source_package = that.get('source_package');
config.parameters = {
productseries: productseries_summary.api_uri};
- source_package.named_post('setPackaging', config);
+ source_package.named_post(
+ 'setPackagingReturnSharingDetailPermissions', config);
}
- function get_productseries(config) {
+ function get_productseries(config, permissions) {
+ that.set_permissions(permissions);
lp_client.get(productseries_summary.api_uri, config);
}
function cache_productseries(config, productseries) {
=== modified file 'lib/lp/translations/javascript/tests/test_sourcepackage_sharing_details.js'
--- lib/lp/translations/javascript/tests/test_sourcepackage_sharing_details.js 2011-04-18 13:39:13 +0000
+++ lib/lp/translations/javascript/tests/test_sourcepackage_sharing_details.js 2011-04-21 20:16:31 +0000
@@ -25,6 +25,7 @@
test_translations_usage_enabled: function() {
var sharing_config = new TranslationSharingConfig();
var usage = sharing_config.get('translations_usage');
+ usage.set('user_authorized', true);
Y.Assert.isFalse(usage.get('enabled'));
sharing_config.get('product_series').set_link('ps', 'http://');
Y.Assert.isTrue(usage.get('enabled'));
@@ -33,9 +34,11 @@
var sharing_config = new TranslationSharingConfig();
var product_series = sharing_config.get('product_series');
Y.Assert.isFalse(product_series.get('complete'));
- Y.Assert.isFalse(sharing_config.get('branch').get('enabled'));
+ var branch = sharing_config.get('branch');
+ branch.set('user_authorized', true);
+ Y.Assert.isFalse(branch.get('enabled'));
product_series.set_link('ps', 'http://');
- Y.Assert.isTrue(sharing_config.get('branch').get('enabled'));
+ Y.Assert.isTrue(branch.get('enabled'));
},
test_set_branch: function() {
var lp_client = new Y.lp.client.Launchpad();
@@ -51,7 +54,9 @@
},
test_autoimport: function() {
var sharing_config = new TranslationSharingConfig();
- Y.Assert.isFalse(sharing_config.get('autoimport').get('enabled'));
+ var autoimport = sharing_config.get('autoimport');
+ autoimport.set('user_authorized', true);
+ Y.Assert.isFalse(autoimport.get('enabled'));
sharing_config.get('branch').set_link('br', 'http://foo');
Y.Assert.isTrue(sharing_config.get('autoimport').get('enabled'));
},
@@ -77,12 +82,12 @@
Y.Assert.isTrue(lci.get('complete'));
},
test_CheckItem_enabled: function() {
- var ci = new CheckItem();
+ var ci = new CheckItem({user_authorized: true});
Y.Assert.isTrue(ci.get('enabled'));
},
test_CheckItem_enabled_dependency: function(){
var lci = new LinkCheckItem();
- var ci = new CheckItem({dependency: lci});
+ var ci = new CheckItem({dependency: lci, user_authorized: true});
Y.Assert.isFalse(ci.get('enabled'));
lci.set_link('text', 'http://example.com');
Y.Assert.isTrue(ci.get('enabled'));
@@ -91,6 +96,12 @@
var ci = new CheckItem({identifier: 'id1'});
Y.Assert.areEqual('id1', ci.get('identifier'));
},
+ test_CheckItem_user_authorized: function() {
+ var ci = new CheckItem({user_authorized: true});
+ Y.Assert.isTrue(ci.get('enabled'));
+ ci.set('user_authorized', false);
+ Y.Assert.isFalse(ci.get('enabled'));
+ },
test_configure_empty: function() {
var ctrl = new TranslationSharingController();
var cache = {
@@ -120,7 +131,8 @@
},
context: {
resource_type_link: 'http://sourcepackage'
- }
+ },
+ user_can_change_branch: true
};
var ctrl = new TranslationSharingController();
var lp_client = new Y.lp.client.Launchpad();
@@ -138,6 +150,7 @@
tsconfig.get('product_series').get('url'), 'http://web1');
Y.Assert.areEqual(
tsconfig.get('branch').get('text'), 'lp:title2');
+ Y.Assert.isTrue(tsconfig.get('branch').get('user_authorized'));
Y.Assert.isTrue(tsconfig.get('autoimport').get('complete'));
Y.Assert.isTrue(
tsconfig.get('translations_usage').get('complete'));
@@ -149,13 +162,69 @@
self_link: 'http://foo',
resource_type_link: 'http://foo_type'
},
- bar: null
+ bar: null,
+ baz: true
};
var lp_client = new Y.lp.client.Launchpad();
cache = test_ns.convert_cache(lp_client, cache);
Y.Assert.isNull(cache.bar);
+ Y.Assert.isTrue(cache.baz);
Y.Assert.areEqual('http://foo', cache.foo.get('self_link'));
},
+ test_set_permissions: function(){
+ var ctrl = new TranslationSharingController();
+ var config = ctrl.get('tsconfig');
+ var overall = config.get('configuration');
+ Y.Assert.isTrue(overall.get('user_authorized'));
+ ctrl.set_permissions({
+ user_can_change_translation_usage: true,
+ user_can_change_branch: true,
+ user_can_change_translations_autoimport_mode: true,
+ user_can_change_product_series: true
+ });
+ var usage = config.get('translations_usage');
+ Y.Assert.isTrue(usage.get('user_authorized'));
+ ctrl.set_permissions({
+ user_can_change_translation_usage: false,
+ user_can_change_branch: true,
+ user_can_change_translations_autoimport_mode: true,
+ user_can_change_product_series: true
+ });
+ Y.Assert.isFalse(usage.get('user_authorized'));
+ var branch = config.get('branch');
+ Y.Assert.isTrue(branch.get('user_authorized'));
+ ctrl.set_permissions({
+ user_can_change_translation_usage: false,
+ user_can_change_branch: false,
+ user_can_change_translations_autoimport_mode: true,
+ user_can_change_product_series: true
+ });
+ Y.Assert.isFalse(branch.get('user_authorized'));
+ var autoimport = config.get('autoimport');
+ Y.Assert.isTrue(autoimport.get('user_authorized'));
+ ctrl.set_permissions({
+ user_can_change_translation_usage: false,
+ user_can_change_branch: false,
+ user_can_change_translations_autoimport_mode: false,
+ user_can_change_product_series: true
+ });
+ Y.Assert.isFalse(autoimport.get('user_authorized'));
+ var product_series = config.get('product_series');
+ Y.Assert.isTrue(product_series.get('user_authorized'));
+ ctrl.set_permissions({
+ user_can_change_translation_usage: false,
+ user_can_change_branch: false,
+ user_can_change_translations_autoimport_mode: false,
+ user_can_change_product_series: false
+ });
+ Y.Assert.isFalse(product_series.get('user_authorized'));
+ ctrl.set_permissions({
+ user_can_change_translation_usage: false,
+ user_can_change_branch: false,
+ user_can_change_translations_autoimport_mode: false
+ });
+ Y.Assert.isFalse(product_series.get('user_authorized'));
+ },
test_update_branch: function(){
var complete = Y.one('#branch-complete');
var incomplete = Y.one('#branch-incomplete');
@@ -203,6 +272,7 @@
var incomplete = Y.one('#branch-incomplete');
var ctrl = new TranslationSharingController();
var branch = ctrl.get('tsconfig').get('branch');
+ branch.set('user_authorized', true);
ctrl.update_check(branch);
Y.Assert.isTrue(incomplete.hasClass('lowlight'));
var product_series = ctrl.get('tsconfig').get('product_series');
@@ -215,6 +285,7 @@
'#upstream-sync-incomplete-spinner');
var ctrl = new TranslationSharingController();
var autoimport = ctrl.get('tsconfig').get('autoimport');
+ autoimport.set('user_authorized', true);
var branch = ctrl.get('tsconfig').get('branch');
branch.set_link('a', 'b');
var incomplete_picker = Y.one(