Django QuerySet: Format query result as custom dictionary - django

I am working on a RBAC system for my Django-React app. I have this certain object structure that I want to generate but I am not sure how to do so directly from Django's QuerySet API.
Currently, I have this query
permissions |= PermissionAssignment.objects.filter(role=role['role__id']).values(
'permission__object__id',
'permission__object__name',
'permission__operation__id',
'permission__operation__name'
)
which returns this object on my frontend:
permissions: [
{
permission__object__id: 1,
permission__object__name: 'Post',
permission__operation__id: 1,
permission__operation__name: 'Read'
},
{
permission__object__id: 1,
permission__object__name: 'Post',
permission__operation__id: 2,
permission__operation__name: 'Write'
},
{
permission__object__id: 2,
permission__object__name: 'Event',
permission__operation__id: 2,
permission__operation__name: 'Read'
},
]
I do not require the permission object to have that complex structure. I only need to take hold of the object name and the operations enabled to this object for the current user. So basically, I just need it to be structured as:
permissions:[
"Post": {
operations: ['Read', 'Write']
},
"Event": {
operations: ['Read']
}
]
I know I can manipulate the original result on my frontend to get what I want, but I don't see that is necessary when I can format the result on the server-side level right away, except that I am not sure how to do that in Django.

You might be able to use list comprehension to achieve the desired outcome.
# NOTE: we use prefetch_related to avoid extra queries every time we access the object
permissions_qs = PermissionAssignment.objects.filter(role=role['role__id']).prefetch_related('object', 'operation')
d = {}
[d.update({p.object.name: {'operations': [p.operation.name]}}) if p.object.name not in d else d[p.object.name]['operations'].append(p.operations.name) for p in permissions_qs]

I have figured out the solution for what I wanted... The structure of the JSON response is different from what I defined in my question above, but I guess it is so much better.
However, I am posting the code here because I am having second thoughts with my approach i.e. I am starting to think the query is very expensive... and maybe someone can help check whether it is fine overall, or a bad practice indeed.
Code:
roles = RoleAssignment.objects.none()
modules = RoleModule.objects.none()
module_objects = ModuleObject.objects.none()
objects = Object.objects.none()
permissions = PermissionAssignment.objects.none()
roles = RoleAssignment.objects.filter(user=user).values('role__id', 'role__name')
for role in roles:
modules |= RoleModule.objects.filter(role=role['role__id']).values(
'role__id',
'module__id',
'module__name',
'module__slug',
'module__fontawesome_icon'
)
permissions |= PermissionAssignment.objects.filter(role=role['role__id']).values(
'permission__object__id',
'permission__object__name',
'permission__operation__id',
'permission__operation__name'
)
for module in modules:
module_objects |= ModuleObject.objects.filter(module=module['module__id']).values(
'module__id',
'module__name',
'object__id',
'object__name',
'object__slug'
)
# format modules to include respective objects and permissions
# this logic is primarily designed so that sidebar links can be...
# ...generated seamlessly thru frontend
formatted_modules = [
{
"name": x['module__name'],
"slug": x['module__slug'],
"icon": x['module__fontawesome_icon'],
"objects": [
{
"name": y['object__name'],
"slug": y['object__slug'],
"operations": [z['permission__operation__name'] for z in permissions if z['permission__object__id']==y['object__id']]
}
for y in module_objects if x['module__id']==y['module__id']
]
}
for x in modules
]
Then send it as response:
return Response({
"modules": formatted_modules
})
The JSON equivalence:
{
modules: [
{
name: 'Web Content',
slug: 'web-content',
icon: 'fas fa-newspaper',
objects: [
{
name: 'Post',
slug: 'post',
operations: [
'Read',
'Write',
'Modify',
'Delete'
]
}
]
},
{
name: 'Bids and Awards',
slug: 'bids-and-awards',
icon: 'fas fa-project-diagram',
objects: [
{
name: 'SRBP Goods & Services',
slug: 'srbp-goods-services',
operations: [
'Read',
'Write',
'Modify',
'Delete'
]
},
{
name: 'SRBP Infrastructure',
slug: 'srbp-infrastructure',
operations: [
'Read',
'Write',
'Modify',
'Delete'
]
}
]
}
]
}
In my UI:

Related

Ember - Only update fields returned in response JSON

we would like to add lazy loading functionality to our ember project, but it looks like ember will always override fields not returned by the response JSON with NULL. First I get a list of users:
GET https://example.com/users
{
"users": [
{
"id": 1,
"name": 'user1',
"email": 'email#user1.com',
"images": [],
"posts": []
},
{
"id": 2,
"name": 'user2',
"email": 'email#user2.com',
"images": [],
"posts": []
},
{
"id": 3,
"name": 'user3',
"email": 'email#user3.com',
"images": [],
"posts": []
},
]
}
This provides a minimal set of user information, with two empty hasMany relations "images" and "posts".
Now, if somebody want to see the users posts or images he would click a button which triggers the lazy loading:
GET https://example.com/userImages/1
{
"user": {
"id": 1,
"images": [1,2,3,4]
},
"images": [
{
"id": 1,
"name": "image1",
"path" "path/to/img1/"
},
{
"id": 2,
"name": "image2",
"path" "path/to/img2/"
},
{
"id": 3,
"name": "image3",
"path" "path/to/img3/"
},
{
"id": 4,
"name": "image4",
"path" "path/to/img4/"
}
]
}
To reduce traffic to a minimum, only the newly loaded information is included. After the adapter has deserialzed and pushed the new data to the store, all fields from User1 which are not included in the payload (name, email) are set to NULL in the ember store (tested with store.pushPayload('model', payload)).
Is there a possibility to update only incoming data? Or is there a common best practice to handle such a case?
Thanks in advance for your help
EDIT:
One possibility would be to extend the ember-data "_load()" function with the block
for (var key in record._data) {
var property = record._data[key];
if( typeof(data[key]) == 'object' && data[key] == null ) {
data[key] = property;
}
}
But this is the worst possible solution I can imagine.
I think what you want is the store's update method. It's like push (or pushPayload), except that it only updates the data that you give it.
Your property returns a promise and that promise returns whatever came back from the server.
foobar: function() {
return this.store.find('foobar');
}
When the promise resolves, you have two versions of the data, the one already rendered in the client (dataOld) and the one that just returned from the backend (dataNew). To update the client without removing what hasn't change, you have to merge the old and the new. Something along the lines of:
foobar: function() {
var dataOld = this.get('foobar');
return this.store.find('foobar').then(function(dataNew) {
return Ember.$.merge(dataOld, dataNew);
});
}

Ember Data serialize relationship without parent ID

I'm building an adapter to wrap the Keen.io API, so far I've been able to successfully load the projects resource, however the returned object looks like this:
{
partners_url: "/3.0/projects/<ID>/partners",
name: "Project Name",
url: "/3.0/projects/<ID>",
saved_queries: [ ],
events_url: "/3.0/projects/<ID>/events",
id: "<ID>",
organization_id: "<ORG ID>",
queries_url: "/3.0/projects/<ID>/queries",
api_key: "<API KEY>",
events: [
{
url: "/3.0/projects/<ID>/events/user_signup",
name: "user_signup"
},
{
url: "/3.0/projects/<ID>/events/user_converted",
name: "user_converted"
},
{
url: "/3.0/projects/<ID>/events/user_created_project",
name: "user_created_project"
}
]
}
Excluding a lot of cruft, Ember has no trouble mapping the name and id attributes using the RESTSerializer, but if I add an events relation to my Project model it blows up with:
Error while loading route: TypeError: Cannot set property 'store' of undefined
at Ember.Object.extend.modelFor (http://localhost:3000/assets/ember-data.js?body=1:9813:23)
at Ember.Object.extend.recordForId (http://localhost:3000/assets/ember-data.js?body=1:9266:21)
at deserializeRecordId (http://localhost:3000/assets/ember-data.js?body=1:10197:27)
at deserializeRecordIds (http://localhost:3000/assets/ember-data.js?body=1:10211:9)
at http://localhost:3000/assets/ember-data.js?body=1:10177:11
at http://localhost:3000/assets/ember-data.js?body=1:8518:20
at http://localhost:3000/assets/ember.js?body=1:3404:16
at Object.OrderedSet.forEach (http://localhost:3000/assets/ember.js?body=1:3247:10)
at Object.Map.forEach (http://localhost:3000/assets/ember.js?body=1:3402:10)
at Function.Model.reopenClass.eachRelationship (http://localhost:3000/assets/ember-data.js?body=1:8517:42)
From my investigation this seems to be because it can't find the inverse relation to map an Event back to a Project because there's no parent ID.
Is it possible to create a relation in Ember Data to support this? Or is it possible to modify the Serializer to append a projectId to each event before loading?
I'm using Ember 1.5.0-beta.4 and Ember Data 1.0.0-beta.7+canary.f482da04.
Assuming your Project model is setup the following way:
App.Project = DS.Model.extend({
events: DS.hasMany('event');
});
You need to make sure that the JSON from your API is in a certain shape that Ember-Data expects.
{
"project": {
"id": 1,
"events": ["1", "2"],
},
"events": [{
"id": "1",
"name": "foo"
}, {
"id": "2",
"name": "bar"
}]
}
You can, however, implement extractArrayin your model's serializer to transform the JSON from the server into something similar like the above example.
There's a working example and an explanation in the Ember docs.

How to hit non REST endpoints with Ember Data 1.0 beta

My API is mostly restful except I have a /search endpoint on some resources. I'm using the DS.ActiveModelAdapter and DS.ActiveModelSerializer and everything is great.
My current implementation for search is somewhat like this:
makeAPICall: ->
#set('loading', true)
states = #get('selectedStates')
statesString = states.join(',')
query = #get('searchParam')
url = "/api/v1/organizations/search?#{statesString}&query=#{query}"
$.get(url).then (data) =>
#get('store').pushPayload(data)
# TODO this needs to go through the adapter.
orgs = data.organizations.map (org) =>
#store.find('organization', org.id)
#set('organizations', orgs)
#set('loading', false)
The problem is that I don't know how to do all the normalization/camelization that happens in the adapter in this case. Because the template relies on the #get('organizations') in this case, some underscored attributes don't show up.
What is the correct way to implement this?
The pushPayload is suposed to do the normalization/camelization but needs to know the type you are pushing, from the docs... but is in the v1.0.0-beta.3 version
var pushData = {
posts: [
{id: 1, post_title: "Great post", comment_ids: [2]}
],
comments: [
{id: 2, comment_body: "Insightful comment"}
]
}
store.pushPayload('post', pushData);
In your case the call should be
#get('store').pushPayload('organization', data)
And the data json an array of organizations
organizations:[
{id:1,...},
{id:2,...},
{id:3,...}
]

HasMany relationships between sideloaded models

Here's my Ember-Data model:
Lrt.Option = DS.Model.extend({
option_relation_value: hasMany('option')
});
Here is an example of the JSON: (Shortened for the sake of this question)
{
"optionGroups": [],
"optionSubGroups": [
{
"id": "3",
"optionType": [
"80",
"81",
"82",
"83",
"84",
"248",
"278"
],
"title": "Option Group for 80"
}
],
"options": [
{
"id": "45",
"option_relation_value": [
"80"
]
},
{
"id": "80",
"option_relation_value": []
}
]
}
There are also "OptionGroup" and "OptionSubGroup" models which are sideloading options in.
The issue I'm having is that after adding in the 'hasMany' into the model, I can no longer do query the store for Options like this:
this.get('store').find('option')
It simply returns "0", however in the Ember Inspector, I get 400+ entries so I know the data loaded.
When using the chrome inspector and break on ALL Exceptions, it breaks on line 2246 of Ember-Data at the following line:
2246: Ember.assert('The id ' + id + ' has already been used with another record of type ' + type.toString() + '.', !id || !idToRecord[id]);
The error is:
"Cannot call method 'toString' of undefined"
"type" in this line is 'undefined'.
What am I doing wrong with this hasMany relationship?
I am using Ember-Data 1.0 Beta 2.
Technically, this is not side loading, it's really more like nested loading. That may have something to do with it.
For true side loading you'd want a structure like this as your outer SON response :
{
"option" : {
"id" : 3,
...
"options" : [45,80]
}
"options" : [
{ "id":45 , ... },
{ "id":80 , ... }
]
}
Here are the docs about JSON conventions : http://emberjs.com/guides/models/connecting-to-an-http-server/#toc_json-conventions The comments are an example of side loading.
I know that this works for separate model structures (post -> comment), but I'm not sure about self referential tree type structures.

Ember-Data, embedded relation records and server JSON response

Working on an App with Ember RC6 and Ember-Data v0.13-54 along a custom server PHP API am running into some problems with Models relations (mainly many-to-many) and side loading.
The models in questions are:
App.Member = DS.Model.extend({
apiToken: DS.attr('string'),
apiTokenExpire: DS.attr('string'),
favourites: DS.hasMany('App.Presentation')
});
App.Presentation = DS.Model.extend(
{
title: DS.attr('string'),
description: DS.attr('string'),
date: DS.attr('date'),
category: DS.belongsTo('App.Category'),
tags: DS.hasMany('App.Tag'),
employees: DS.hasMany('App.Member'),
presentation: DS.belongsTo('App.File'),
presenterNotes: DS.belongsTo('App.File'),
cover: DS.belongsTo('App.Image')
});
To get the RESTAdapater to send the relation with the Member model I have:
App.APIRESTAdapter.map('App.Member', {
favourites: {embedded: 'always'}
});
When loading /members/1 the server returns:
{
"member": {
"api_token": "9fa236ad58726584d7b61bcf94b4dda37cbd3a24",
"api_token_expire": "1383832335",
"id": 1,
"favourite_ids": [ 3 ],
"group_ids": [ 2 ]
},
"presentations": [
{
"title": "That's a new one!",
"category_id": 2,
"id": 3,
"tag_ids": [ 1 ],
"employee_ids": [ 1 ]
}
]
}
But if I log get('member.favourites').mapProperty('id') I get an empty array and none of the favourites are actually added to the Member model.
If I disable the embedding of favourites on the RESTAdapter, all works fine. I am just wondering if there is something that I am missing or if there is some issues with how the JSON response is formatted?
EDIT:
Done some digging and seems that the issue comes from the fact that the relation names (favourites, employees) is different from the model names (Member, Presentation) which are used when sideloading data. Weird, since rev. 12 https://github.com/emberjs/data/blob/master/BREAKING_CHANGES.md models should be Sideload by Type.
Doing some tests I've added a new many-to-many relation for those 2 models:
App.Member gets presentations: DS.hasMany('App.Presentation')
App.Presentation get members: DS.hasMany('App.Member')
The JSON returned by the server is exactly the same, and when logging get('member.presentations') I now get the list of presentations as it should.
At this point this looks like a bug to me, but maybe am missing something? I've tried mappings on the RESTAdapater for favourites and employees but that didn't help... Maybe there is some other Adapter config that could help?
This isn't a sideloading issue but more a misunderstanding on my side about embedded data and what the configuration meant.
Since the Adapter was configured with:
App.APIRESTAdapter.map('App.Member', {
favourites: {embedded: 'always'}
});
The expected JSON response from the server is:
{
"member": {
"api_token": "b84fd204b37d3fa3cee8a2b2cac8bd4fd02635a1",
"api_token_expire": "1384027367",
"id": 1,
"favourites": [
{
"title": "Some kind of title",
"category_id": 1,
"id": 2,
"tag_ids": [ 1 , 2 ],
"employee_ids": [ 1 ]
}
]
}
}
So "favourite_ids": [ X, X, X ] should have been "favourites": [ HASH, HASH, HASH ] when records are flagged as embedded.