What is the best way to display information from related objects on my Backbone.js wired front-end when on the backend these attributes are stored on separate Django models in a PostgreSQL database?
I am currently using Django, Tastypie, Django-Tastypie, Backbone.js, Backbone-Relational and Handlebars.js templates. I am open to doing things differently and I am willing to learn new technologies such as Riak if it's necessary or more efficient.
On the front-end what I'm trying to do would be very simple with standard Django templates: display a list of tags on a post and the author of that post.
On the back-end I have a Post model and Tag, User and UserProfile (author) models. Users and UserProfiles are 1-to-1, Post has a relation to UserProfile but what I want to display is stored on the User model under the attribute username. At the moment this involves two painstaking lookups to get the author's username for every post. The Post model:
class Post(models.Model):
author = models.ForeignKey(UserProfile)
topic = models.ForeignKey(Topic)
tags = models.ManyToManyField(Tag)
content = models.TextField()
title = models.CharField(max_length=250)
slug = models.SlugField()
description = models.TextField()
In Coffeescript I have my Backbone models. At present I am trying to fetch the relevant author and tag objects when a Post model is initialized. My current code is very sloppy and I apologize, my javascript foo is still under development!
class User extends Backbone.RelationalModel
class UserProfile extends Backbone.RelationalModel
urlRoot : '/api/v1/profile/?format=json'
class PostTag extends Backbone.RelationalModel
initialize: ->
this.get('tag').on 'change', ( model ) =>
this.get( 'post' ).trigger( 'change:tag', model )
class Tag extends Backbone.RelationalModel
urlRoot: '/api/v1/tag/?format=json'
idAttribute: 'id',
relations: [{
type: Backbone.HasMany,
key: 'post_tags',
relatedModel: PostTag,
reverseRelation: {
key: 'tag',
includeInJSON: 'id',
},
}],
class Post extends Backbone.RelationalModel
idAttribute: 'id',
relations: [{
type: Backbone.HasMany,
key: 'post_tags',
relatedModel: PostTag,
reverseRelation: {
key: 'post',
includeInJSON: 'id',
},
}]
initialize: ->
#.get('tags').forEach(#addTag, #)
#.addAuthor()
#.on 'change:tag', (model) ->
console.log('related tag=%o updated', model)
addAuthor: () ->
profile_id = #.get('author')
if app.UserProfiles.get(profile_id)?
profile = app.UserProfiles.get(profile_id)
user = app.Users.get(profile.user)
#.set('author_name',user.get('username'))
else
profile = new app.UserProfile(profile_id)
app.UserProfiles.add(profile)
profile.fetch(success: (model,response) =>
user_id = profile.get('user')
if app.Users.get(user_id)?
user = app.Users.get(user_id)
user.fetch(success: (model,response) =>
console.log(user.get('username'))
#.set('author_name',user.get('username'))
)
console.log("Existing user"+user_id)
console.log(user.get('username'))
##.set('author_name',user.get('username'))
else
user = new app.User('resource_uri':user_id)
app.Users.add(user)
console.log("New user"+user_id)
user.fetch(success: (model,response) =>
console.log(user.get('username'))
#.set('author_name',user.get('username'))
)
)
addTag: (tag_id) ->
console.log(tag_id)
if app.Tags.get(tag_id)?
tag = app.Tags.get(tag_id)
console.log("TAG" + tag)
else
console.log("NON EXISTENT")
console.log(tag_id)
tag = new app.Tag({'id':tag_id})
tag.fetch()
app.Tags.add(tag)
post_tag = new app.postTag({
'tag': tag_id,
'post': #.get('resource_uri')
})
#.get('post_tags').add(post_tag)
This code actually works fine for fetching and storing the related objects but it's incredibly messy and I'm sure there must be a better way. Further, I can't figure out a way to access the stored tag names to display in my Handlebars.js templates.
When writing this I found the related question How do I load sub-models with a foreign key relationship in Backbone.js?
Since I'd already written the question I figured I may as well post it in case it's useful for anyone.
The answer was as simple as adding full=True to my tastypie resources. I could then get rid of the addTags and addAuthor functions and since I don't need to save or update the related objects the rest of the answer in the above thread wasn't necessary for me.
Related
Considering I have a model like:
MyStore = (
id = 1,
name = 'Foobar',
information_as_json = {
'open_at': datetime.now(),
'close_at': datetime.now() + timedelta('+1 day'),
'workers' : {
'Person1' : 'Owner',
'Person2' : 'Boss',
'Person3' : 'Boss',
}
})
Inside Django admin forms, for every field is generated an input, but for the field "information_as_json", I don't want to show it as a string or as JSON. That is because the users who are accessing this store admin page, need to read the field 'information_as_json' easier since no one can edit these values because it is generated in another part of the application.
Is it possible to convert these values to a "div" or a plain text? The contents would be:
This store opens at: {information_as_json.open_at}
This store close at: {information_as_json.close_at}
And for the workers, iterate through keys and values:
for key, value in information_as_json.workers:
Worker {key} has the role: {value}
I'm a beginner at Django, so I'm struggling a little with this part.
Every help would be appreciated :D
I would suggest approaching the model a little differently. Rather than storing the opening and closing hours as JSON they can just be fields directly on the store model. The the workers can be a JSONfield [docs] containing name/role pairs. If you're using PostgreSQL for your database you could even use HStoreField [docs], which might be more appropriate.
Here's how I would write a similar model.
class Store(models.Model):
name = models.CharField(max_length=512, unique=True)
workers = models.JSONField(blank=True, default=dict, editable=False)
closing = models.TimeField(blank=True, null=True, editable=False)
opening = models.TimeField(blank=True, null=True, editable=False)
To display the details in the Django admin we just need to define a property which returns the correct string.
#mark_safe
def details(self):
roles = [
f'{x} has the role: {y}'
for x, y in self.workers.items()
]
return '<br>'.join([
f'This store opens at: {self.opening:%-H:%M}',
f'This store closes at: {self.closing:%-H:%M}',
] + roles)
This method can then be referenced in the ModelAdmin and used like a read-only field.
#admin.register(Store)
class StoreAdmin(admin.ModelAdmin):
list_display = ['name', 'opening', 'closing']
fields = ['name', 'details']
readonly_fields = ['details']
Despite having looked everywhere for similar issues I still cannot make the query working using INNER JOIN with the Django ORM... Sorry if this might sound stupid, but this is my first time with Django on a project and especially the ORM.
I have an Articles table with a Users table (named Fellows in my case), the Articles table has it's foreign key on author and references the user_id in Fellows table.
class Fellow(models.Model):
id = models.AutoField(db_column='ID', primary_key=True) # ID
user_id = models.PositiveBigIntegerField(db_column='User_ID', unique=True) # Global User ID.
nickname = models.CharField(db_column='Name', max_length=64, db_collation='utf8mb4_general_ci') # Display Name
user_password = models.CharField(db_column='User_Password', max_length=256, blank=True, null=True) # Passwd
gold = models.IntegerField(db_column='Gold') # Credits
faction = models.ForeignKey('Faction', models.RESTRICT, db_column='Faction', default=1) # ID Faction
class Meta:
managed = False
db_table = 'Fellows'
def __str__(self):
return self.nickname # Test.
class Article(models.Model):
id = models.AutoField(db_column='ID', primary_key=True) # ID
author = models.ForeignKey('Fellow', models.CASCADE, db_column='ID_User', default=1) # Global User ID
title = models.CharField(db_column='Title', max_length=32) # Title
content = models.TextField(db_column='Content') # Content
posted = models.DateTimeField(db_column='Posted') # Date Posted
source = models.CharField(db_column='Source', max_length=64, blank=True, null=True) # Source picture url of the article.
class Meta:
db_table = 'Articles'
I tried to get the display name of the related author that posted the article without success.
This is my views.py:
from .models import Article
def index(request):
"""
And then Vue.JS will take care of the rest.
"""
# articles = Article.objects.order_by('-posted')[:5] # Returns everything inside Articles table but nothing inside Fellows table.
# articles = Article.objects.select_related() # No Result.
# Still can't get display_name in index.html with this one.
articles = Article.objects.raw('SELECT Fellows.Name AS Display_Name, Articles.ID, Articles.Title, Articles.Content, Articles.Posted, Articles.Source FROM Articles INNER JOIN Fellows ON Fellows.User_ID = Articles.ID_User ORDER BY Articles.ID DESC LIMIT 5;')
data = {
'articles': articles,
}
return render(request, 'home/index.html', data)
The raw request returns everything fine only with sql interpreter, so there is two options:
Django won't perform the INNER JOIN.
I didn't figured out how to read the Display_Name in the template (index.html).
This is how I retrieve the data using VueJS (even with the raw query I can't get the display_name, it's empty).
<script>
const store = new Vuex.Store({
state: {
articles: [
{% for article in articles %}
{
title: '{{ article.title }}',
content: '{{ article.content | linebreaksbr }}',
source: "{% static 'home/img/' %}" + '{{article.source}}',
display_name: '{{article.display_name}}', // Maybe this is not how to retrieve the display_name?
},
{% endfor %}
],
},
});
// Components.
ArticleList = Vue.component('article-list', {
data: function () { return { articles: store.state.articles } },
template: '#article-list-template',
});
ArticleItem = Vue.component('article-item', {
delimiters: ['[[', ']]'],
props: ['id', 'title', 'content', 'source', 'display_name'],
template: '#article-item-template',
});
...
</script>
if someone could help me with this I would appreciate immensely! TT
Problem solved,
I had to change the foreign key constraint Articles.ID_User which now leads to Fellows.ID.
Previously the constraint led to Fellows.User_ID.
I can finally use:
articles = Article.objects.select_related('author').order_by('-posted')[:5]
And indeed finally accessing it in the front by article.author, simple as that.
Yet I still don't really understand why the raw sql query (using the mysql interpreter) with the INNER JOIN worked fine tho when referencing Fellows.User_ID, which was apparently not the case in the ORM.
Although it is working, my sql relational might be wrong or not ideal, therefore I am still open to suggestions!
I need to filter all Experts by past objectives.
I have a minimal runnable example at https://github.com/morenoh149/django-rest-datatables-relations-example (btw there are fixtures you can load with test data).
my models are
class Expert(models.Model):
name = models.CharField(blank=True, max_length=300)
class Meeting(models.Model):
user = models.ForeignKey(User, on_delete=models.SET_NULL, blank=True, null=True)
expert = models.ForeignKey(Expert, on_delete=models.SET_NULL, blank=True, null=True)
objective = models.TextField(null=True, blank=True)
my datatables javascript
$("#table-analyst-search").DataTable({
serverSide: true,
ajax: "/api/experts/?format=datatables",
ordering: false,
pagingType: "full_numbers",
responsive: true,
columns: [
{
data: "objectives",
name: "objectives",
visible: false,
searchable: true,
render: (objectives, type, row, meta) => {
return objectives;
}
},
],
});
My serializer
class ExpertSerializer(serializers.ModelSerializer):
id = serializers.IntegerField(read_only=True)
objectives = serializers.SerializerMethodField()
class Meta:
model = Expert
fields = (
"id",
"objectives",
)
def get_objectives(self, obj):
request = self.context["request"]
request = self.context["request"]
meetings = Meeting.objects.filter(
analyst_id=request.user.id, expert_id=obj.id
).distinct('objective')
if len(meetings) > 0:
objectives = meetings.values_list("objective", flat=True)
objectives = [x for x in objectives if x]
else:
objectives = []
return objectives
When I begin to type in the datatables.js searchbar I get an error like
FieldError at /api/experts/
Cannot resolve keyword 'objectives' into field. Choices are: bio, company, company_id, created_at, description, email, favoriteexpert, first_name, id, is_blocked, last_name, meeting, middle_name, network, network_id, position, updated_at
Request Method: GET
Request URL: http://localhost:8000/api/experts/?format=datatables&draw=3&columns%5B0%5D%5Bdata%5D=tags&columns%5B0%5D%5Bname%5D=favoriteexpert.tags.name&columns%5B0%5D%5Bsearchable%5D=true&columns%5B0%5D%5Borderable%5D=false&columns%5B0%5D%5Bsearch%5D%5Bvalue%5D=&columns%5B0%5D%5Bsearch%5D%5Bregex%5D=false&columns%5B1%5D%5Bdata%5D=desc&columns%5B1%5D%5Bname%5D=&columns%5B1%5D%5Bsearchable%5D=false&columns%5B1%5D%5Borderable%5D=false&columns%5B1%5D%5Bsearch%5D%5Bvalue%5D=&columns%
fwiw, in pure django orm what I want to accomplish would be something like
Expert.objects.filter(
pk__in=Meeting.objects.filter(
objective__icontains='Plastics', user=request.user
).values('expert')
)
How can I filter experts by historical meeting objectives?
The reason for the error is that django-rest-framework-datatables is trying to translate the request into a query which can be run against the Expert table.
In your JS, you're asking for a field called 'objectives' to be returned, but there is no such field on the Expert model.
You could probably achieve what you are trying to do using the django-filter integration. In this case, you could set up a filter on the FK reference to the Meeting table. The example app demonstrates how to do this.
I think the best way to understand what's going on is to get the example application running, and if possible, set breakpoints and step through.
Incidentally, if you want to get the search box to work correctly, then you need to define a global_q() method. This is also covered in the example app.
I ended up authoring a custom django-filter
class AssociatedMeetingCharFilter(filters.CharFilter):
def global_q(self):
"""
Uses the global filter to search a meeting field of meetings owned by the logged in user
"""
if not self._global_search_value:
return Q()
kw = "meeting__{}__{}".format(self.field_name, self.lookup_expr)
return Q(**{
kw: self._global_search_value,
"meeting__user_id": self.parent.request.user.id or -1,
})
class ExpertGlobalFilterSet(DatatablesFilterSet):
name = GlobalCharFilter(lookup_expr='icontains')
objectives = AssociatedMeetingCharFilter(field_name='objective', lookup_expr='icontains')
full example at https://github.com/morenoh149/django-rest-datatables-relations-example
In a project I'm working on for a little office building, I have models that include Floors, Locations, down to Assets (workstations and printers and such):
class Floor(models.Model):
name = models.CharField(max_length=20)
floornumber = models.IntegerField(default=1)
class Location(models.Model):
fkfloor = models.ForeignKey(Floor, on_delete=models.CASCADE)
name = models.CharField(max_length=30)
isroom = models.BooleanField(default=False)
class Asset(models.Model):
name = models.CharField(max_length=50)
serialnumber = models.CharField(max_length=50)
class Workstation(models.Model):
name = models.CharField(max_length=30)
owner = models.CharField(max_length=20)
asset = models.OneToOneField(Asset, on_delete=models.CASCADE, primary_key=True)
In the Admin interface, I need a way for adding Workstations in the OneToOne relationship with Assets, but with a filter for the location so that the entire list of Assets (including non-workstations) in every part of the building doesn't show up in the admin Add/Change form for Workstations.
I've read through two books and searched SO and Django docs for every combination of terms I can think of for what I'm trying to accomplish, and I haven't found a working solution yet. I can use list_filter just fine to show the existing items, but that doesn't carry over into the admin add/change form. inlines aren't what I'm looking to use.
What concept am I missing here? Many thanks in advance.
I got stubborn and figured it out after another few hours of Googling. So for the good of all humanity searching for this, I shall post it all. It's a very hacked method, but no doubt could be refactored DRY. (Note: Cross-posted from my same question on Reddit).
My classes remain nearly the same as above, but I added Buildings as a FK to Floors. Not that I have more than one building IRL, but it helped narrow down the test data with some contrived things. "ef" is just my prefix for "extra field".
#admin.py
class AssetForm(forms.ModelForm):
efbuilding = forms.ModelChoiceField(queryset=Building.objects.all(),label=u"Building",required=False)
effloor = forms.ModelChoiceField(queryset=Floor.objects.all(),label=u"Floor",required=False)
class Meta:
fields = '__all__'
model = Asset
class AssetAdmin(admin.ModelAdmin):
form = AssetForm
fieldsets = (
(None, {
'fields': ('efbuilding','effloor','fklocation'), #The 'extra' fields from above
}),
('Asset Information', {
'fields': ('name','serialnumber', ...)
}),
)
The fieldsets force the extra fields at the top of the Admin form, where they look quite natural. views.py and urls.py were next:
#views.py
from django.core import serializers
def addasset(request): # <-- note that this only needed "request". Putting something else there screwed things up.
if request.method == 'GET':
building_id = request.GET.get('id')
json_floor = serializers.serialize("json", Floor.objects.filter(fkbuilding_id=building_id))
return HttpResponse(json_floor) #Probably a terrible practice here with not using JsonResponse, but this worked
else:
return HttpResponse("No-go")
#urls.py (Needed to hook view, the URL doesn't really matter)
urlpatterns = [
...
path('addasset/', views.addasset), #<-- later versions of Django use "path" instead of regular expressions
]
Instead of extending the Admin change_form.html, I made a copy of it to its own folder to replace it for this test so that I didn't lose my bearings on the template tags. As this could be documented more clearly, here's exactly how that works:
From the virtual environment directory, copy:
/env_assettracker/Lib/site-packages/django/contrib/admin/templates/admin/change_form.html
...to a new directory inside the project (my project directory is in the env directory):
/env_assettracker/assettracker/templates/admin/assetapp/asset/change_form.html
In general terms, /PROJECT/templates/admin/APP/MODEL/change_form.html. This worked without having to add anything extra DIRS to the settings.py file.
At the bottom of the new copy of change_form.htmlin the last block after the script, I added my own scripts:
#change_form.html
{% block admin_change_form_document_ready %}
<script>...the existing script for in the template...</script>
//add jQuery from my static files
<script src="{% static "jquery-3.3.1.min.js" %}"></script>
First, since this is going to be using AJAX, I went with the Django recommended cookie method. The exact verbiage from their documentation worked fine. I pretty much copy-pasted their block of code into the beginning of my new script section.
Now for the good part:
#change_form.html
$(function() {
...all the csfr token / cookie stuff...
$('#id_efbuilding').on('change', function(e) {
e.preventDefault();
$.getJSON("http://127.0.0.1:8000/addasset/",{id: $(this).val()}, function(j) {
var options = '<option value="">---??---</option>'; //blank value when nothing is selected in the box
for (var i = 0; i < j.length; i++) {
options += '<option value="' + parseInt(j[i].pk) + '">' + j[i].fields['name'] + '</option>';
}
$("#id_effloor").html(options);
$("#id_effloor option:first").attr('selected', 'selected');
});
$("id_efbuilding").attr('selected', 'selected');
});
});
This was my first attempt with jQuery and AJAX, so I'm sure that the requests are muddled between Http and JSON. Far from elegant, but it works. Selecting a building now changes the choices available in the floors, and then I guess I'll make another one after that to change the locations, and then the same methodology should apply to the one-to-one models (though I'll try to put the code in a more reusable place).
These were compiled from multiple sources. The most helpful was an old blog post from 2009 here.
And with that, I should probably eat something.
I'm using Django.
Here is my .json file :
{
title: "foo",
id: 4,
taskhistories: [
"http://localhost:8000/taskhistories/33/",
"http://localhost:8000/taskhistories/34/"
],
url: "http://localhost:8000/tasks/4/"
}
I have tasks that have one-to-many taskhistories. The thing is, I use related-name in the definition of my TaskHistory model to display it in the tasks directory of my API :
class TaskHistory(models.Model):
task = models.ForeignKey(Task, related_name='taskhistories')
But, in the API, it doesn't display the taskhistory itself but the url to the API page of the taskhistory. How can I directly display a list of my task histories in my task API page instead of just urls ?
Edit : Adding the serializer :
class TaskSerializer(serializers.HyperlinkedModelSerializer):
projectname = serializers.Field(source='project.name')
projectid = serializers.Field(source='project.id')
class Meta:
model = Task
fields = ('title', 'description', 'status', 'created_on', 'duration',
'id', 'projectid', 'projectname', 'taskhistories')
Add TaskSerializer:
taskhistories = serializers.RelatedField(many=True)
that will use your unicode method of TaskHistory Model for display. For further references see here
I think I answered your question.