Hi I want to share some work that's being done to support some of the new disclosure/sharing functionality. I've been doing some prototyping and it's at the point where I think it may turn out to be a viable option to move forward with. The info that follows will be a little raw but hopefully interesting enough for those who care to put in their 2c worth.
tl;dr; It's possible to easily implement a named service which exports methods to the api and which can provide json data to the client for rendering. Our standard lazr.restful library is used, but we are not attaching service methods to domain objects which I believe is the wrong approach. This new implementation is more in line with a SOA approach which we are looking to adopt and allows the launchpadlib users and browser clients to access flattened data via the same api. Many of you will be aware from my past ramblings that one of the big issues I have about how Launchpad hangs together is that we have conflated domain objects with remoting abstractions (the same mistake that EJB made in version 1). This is wrong for so many reasons; to try and keep this email concise, buy me a beer if you want to hear my extended thoughts in this area. This work also lends itself to a POPO based approach for modelling our business domain objects. Warning - pseudo code included below. So we have a new sharing/permissions model and want to (for example) create an an observer for a product. The current or "wrong" way would be: IProduct @export_write_operation() def addObserver(person) But distributions can also have observers added and so we would need to add the same method to IDistribution as well. And the consumers of these apis would need to have this distinction coded in and..... The new way - define an AccessPolicyService: ** Defining and registering a Service ** ---------------------------------------- class IAccessPolicyService(IService): export_as_webservice_entry(publish_web_link=False, as_of='beta') @export_write_operation() @call_with(user=REQUEST_USER) @operation_parameters( pillar=Reference(IPillar, title=_('Pillar'), required=True), observer=Reference(IPerson, title=_('Observer'), required=True), access_policy_type=Choice(vocabulary=AccessPolicyType)) @operation_for_version('devel') def addPillarObserver(pillar, observer, access_policy_type, user): """Add an observer with the access policy to a pillar.""" The addPillarObserver method returns json data which representing the result of the operation, sufficient to allow (for example) the view to be updated in the appropriate way. For the +sharing view, that would be to update the json request cache and add a new row to the observer table. To expose the service, simply register it as a utility in zcml with a name: <securedutility name="accesspolicy" class="lp.registry.services.accesspolicyservice.AccessPolicyService" provides="lp.app.interfaces.services.IService"> <allow interface="lp.registry.interfaces.accesspolicyservice.IAccessPolicyService"/> </securedutility> Services are traversed to via +services/<servicename> ** Service Invocation ** ------------------------ There's 3 (consistent) ways to invoke the service apis. 1. Server side service = getUtility(IService, 'accesspolicy') service.addPillarObserver(....) 2. launchpadlib api from launchpadlib.launchpad import Launchpad lp = Launchpad.login_with('testing', version='devel') # Launchpadlib can't do relative url's service = self.launchpad.load( '%s/+services/accesspolicy' % self.launchpad._root_uri) service.addPillarObserver(....) 3. javascript client var lp_client = new Y.lp.client.Launchpad(); lp_client').named_post( '/+services/accesspolicy', 'addPillarObserver', y_config); ** View rendering/form submission ** ------------------------------------ The new +sharing page does not use any server side rendering. It has a bit of TAL used for the page chrome but the business data is rendered in the browser using mustache (and soon handlebars). Data for the view model is obtained via a getPillarObservers() method on the service. This is invoked by the LaunchpadView instance for +sharing and poked into the json request cache, from where it is accessed when the YUI view widget renders. The point here is that the exact same getPillarObservers() api could be used by a launchpadlib client to get json data for use in a script or tool or whatever. eg the view initialize() method def initialize(self): super(ProductSharingView, self).initialize() cache = IJSONRequestCache(self.request) cache.objects['access_policies'] = self.access_policies cache.objects['sharing_permissions'] = self.sharing_permissions cache.objects['observer_data'] = self.observer_data where, for example: def _getAccessPolicyService(self): return getUtility(IService, 'accesspolicy') @property def observer_data(self): service = self._getAccessPolicyService() return service.getPillarObservers(self.context) When it comes time to write data from the view back to the server, either to initiate an action or save a form or whatever, you can either: i. use our standard LaunchpadFormView and associated infrastructure and delegate the submit processing to an instance of the service via invocation method 1 above, or ii. invoke the service directly via an XHR call using lp client named_post In conclusion, the approach outlined in this email is what we are using to prototype the +sharing view for the disclosure project. So far, it's working very nicely. One key advantage of this approach over just adding methods to domain objects is that a service contract often will call for aggregated and/or flattened data to be returned to a caller (eg view or api user) and this requires gathering data from several places. So the service acts as a fascade, allowing this to happen in a consistent, single sourced way for the different consumers of that data. As I said earlier, those unfortunate enough to hear me bang on previously about this approach know I'm a fan of it and this lib/lp/registry/interfaces/accesspolicyservice.py greenfields disclosure work has finally provided an opportunity to allow us/me to step outside the normal bounds of how we have previously done things in Launchpad to do something new. If you have any feedback, good or bad, please share. The approach is being evaluated (criteria TBA) and we need to look at how we measure it's success or otherwise and whether we want to continue down this road or revert to a more traditional approach etc. If you are interested in the core infrastructure (look for IService, ServiceFactory and friends): lib/lp/app/services.py lib/lp/app/browser/launchpad.py lib/lp/app/interfaces/services.py lib/lp/app/tests/test_services.py lib/lp/registry/configure.zcml lib/lp/registry/services/ lib/lp/registry/browser/configure.zcml lib/lp/registry/interfaces/webservice.py lib/lp/registry/services/configure.zcml lib/lp/services/webservice/services.py I've not landed the bespoke +sharing code yet which sits on top of all this - everything works nicely but I want to put it behind a feature flag etc. _______________________________________________ Mailing list: https://launchpad.net/~launchpad-dev Post to : launchpad-dev@lists.launchpad.net Unsubscribe : https://launchpad.net/~launchpad-dev More help : https://help.launchpad.net/ListHelp