I have created a REST API using DRF, and that works well enough. The frontend is a simple page that allows data to be viewed and updated. Now I am trying to add more interactivity to the site using WebSockets with django-channels. The Channels system is fairly simple to use and also works nicely.
However the issue I am now facing is trying to combine all of the moving pieces to work together. My idea is that the initial page refresh comes through the REST API, and any subsequent updates would automagically come through a WebSocket after every update (with the help of post_save signal). I have nice DRF Serializers for all my models, but alas those do not work without a Request object (for instance HyperLinkedIdentityField):
AssertionError: `HyperlinkedIdentityField` requires the request in the serializer context. Add `context={'request': request}` when instantiating the serializer.
So my question is, how do I somehow create/fake a proper Request object that the Serializers want when trying to serialize my model in a signal handler?
Edit
The more I think about this, the more obvious it becomes that this is not exactly the right way to go. There is no way to craft a single, generic Request object for the serializers, since the model updates which trigger them can come from any source. Thus it would not make sense to even try creating one. I think I have to separate the "base" serializers (without any hyperlinks) and use those to send updates to the clients. Since the hyperlinks won't ever change, I think this is the proper way to go.
In case anyone might be interested, here is how I solved the issue. The main bits and pieces of code are below.
First a simple model (myapp/models.py):
from django.db import models
class MyModel(models.Model):
name = models.TextField()
Then the serializers (myapp/serializers.py):
from rest_framework import serializers
MyModelSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = MyModel
fields = ('url', 'id', 'name')
extra_kwargs = {'url': {'view_name': 'mymodel-detail'}}
MyModelBaseSerializer(serializers.ModelSerializer):
class Meta:
model = MyModel
fields = ('id', 'name')
And the views (myapp/views.py):
from rest_framework import viewsets
from myapp.models import MyModel
from myapp.serializers import MyModelSerializer
class MyModelViewSet(viewsets.ModelViewSet):
queryset = MyModel.objects.all()
serializer_class = MyModelSerializer
And finally the Channels message consumer (myapp/consumers.py):
import json
from django.db.models.signals import pre_save
from django.dispatch import receiver
from channels import Group
from myapp.models import MyModel
from myapp.serializers import MyModelBaseSerializer
def ws_add(message):
message.reply_channel.send({"accept": True})
Group("mymodel").add(message.reply_channel)
def ws_disconnect(message):
Group("mymodel").discard(message.reply_channel)
#receiver(post_save, sender=MyModel)
def mymodel_handler(sender, instance, **kwargs):
Group("mymodel").send({
"text": json.dumps({
"model": "mymodel",
"data": MyModelBaseSerializer(instance).data
})
})
I have omitted things like urls.py and routing.py but those are not relevant to the issue. As can be seen, the regular view uses the normal MyModelSerializer which is includes the URL, and then the update handler MyModelBaseSerializer has only fields which are not dependent on any Request object.
Related
I'm working on building out permissions for an API built with Django REST Framework. Let's say I have the following models:
from django.db import models
class Study(models.Model):
pass
class Result(models.Model):
study = models.ForeignKey(Study)
value = models.IntegerField(null=False)
I have basic serializers and views for both of these models. I'll be using per-object permissions to grant users access to one or more studies. I want users to only be able to view Results for a Study which they have permissions to. There are two ways I can think of to do this, and neither seem ideal:
Keep per-object permissions on Results in sync with Study. This is just a non-starter since we want Study to always be the source of truth.
Write a custom permissions class which checks permissions on the related Study when a user tries to access a Result. This actually isn't too bad, but I couldn't find examples of others doing it this way and it got me thinking that I may be thinking about this fundamentally wrong.
Are there existing solutions for this out there? Or is a custom permissions class the way to go? If so, do you have examples of others who've implemented this way?
As you stated, you can make custom permission as per the second way:
And include the permission in your view:
I am considering your study model with some parameter course, based on that i am writing the solution you can consider for any element in study model
models.py
from django.db import models
class Study(models.Model):
course = models.CharField(max_length=50)
class Result(models.Model):
study = models.ForeignKey(Study)
value = models.IntegerField(null=False)
In permission.py
from rest_framework import permissions
class ResultOrReadOnly(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
if request.method in permissions.SAFE_METHODS and obj.study.course == request.GET.get('course') :
# read only requests
return True
else:
# other requests such as post, patch, put
return obj.study == request.GET.get('course')
And include ,
class ReviewDetail(viewsets.ViewSet):
permission_classes =[ResultOrReadOnly]
And in urls.py,
Modify it to accept the URL parameter course
I would create a new field called enrolled_users in the Study model to indicate which all user has access to the particular Study object.
from django.db import models
from django.conf import settings
class Study(models.Model):
enrolled_users = models.ManyToManyField(
settings.AUTH_USER_MODEL,
related_name="studies"
)
# other fields
class Result(models.Model):
study = models.ForeignKey(Study)
value = models.IntegerField(null=False)
Then, it will very easy in DRF to filter the queryset in the views
# views.py
from .models import Study, Result
from rest_framework.permissions import IsAuthenticated
class StudyModelViewSet(viewsets.ModelViewSet):
permission_classes = (IsAuthenticated,)
def get_queryset(self):
return Study.objects.filter(enrolled_users=self.request.user)
class ResultModelViewSet(viewsets.ModelViewSet):
permission_classes = (IsAuthenticated,)
def get_queryset(self):
return Result.objects.filter(study__enrolled_users=self.request.user)
Notes
This will handle the object permission (Detail View) request as well
This will not return a status code of HTTP 403, but HTTP 404
Here I used the ModelViewSet class, but, you can use any views, but the filter plays the role.
I have been using django-river(https://github.com/javrasya/django-river) application within my app (https://github.com/rupin/WorkflowEngine)
I have used the Django rest framework in my application and would like to get the data from the django-river tables. The name of the table is State.
My serializer is as follows
from river.models import State
from rest_framework import serializers
class StateSerializer(serializers.Serializer):
class Meta:
model = State
fields = ['id', 'label']
Also, my API view is as follows
from rest_framework import generics
from workflowengine.riverserializers.StateSerializer import StateSerializer
from river.models import State
class StateList(generics.ListAPIView):
serializer_class = StateSerializer
def get_queryset(self):
return State.objects.all()
Through the Admin console, I have added 11 states inside my state table, which I have checked with pgadmin.
When i access the API through the browser, I get 11 empty sections in my API ( no error, just the data is missing).
I cant seem to understand how the 11 data points presented in the API are empty. That it presented 11 elements, but no data, which is pretty weird.
I think you need to use:
class StateSerializer(serializers.ModelSerializer):
I know, this question has been already asked many times in SO, but most of the answers I read were either outdated (advising the now deprecated AUTH__PROFILE_MODULE method), or were lacking of a concrete example.
So, I read the Django documentation [1,2], but I lack a real example on how to use it properly.
In fact, my problem comes when a new user is created (or updated) through a form. The user is obviously created but, the fields from the extension are all unset. I know that the Django documentation is stating that:
These profile models are not special in any way - they are just Django models that happen to have a one-to-one link with a User model. As such, they do not get auto created when a user is created, but a django.db.models.signals.post_save could be used to create or update related models as appropriate.
But, I don't know how to do it in practice (should I add a a receiver and if 'yes', which one).
For now, I have the following (taken from the documentation for the sake of brevity):
File models.py
from django.contrib.auth.models import User
class Employee(models.Model):
user = models.OneToOneField(User)
department = models.CharField(max_length=100)
File admin.py
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from django.contrib.auth.models import User
from my_user_profile_app.models import Employee
# Define an inline admin descriptor for Employee model
class EmployeeInline(admin.StackedInline):
model = Employee
can_delete = False
verbose_name_plural = 'employee'
# Define a new User admin
class UserAdmin(UserAdmin):
inlines = (EmployeeInline, )
# Re-register UserAdmin
admin.site.unregister(User)
admin.site.register(User, UserAdmin)
File forms.py
class SignupForm(account.forms.SignupForm):
department = forms.CharField(label="Department", max_length=100)
class SettingsForm(account.forms.SignupForm):
department = forms.CharField(label="Department", max_length=100)
Then, in my code, I use it like this:
u = User.objects.get(username='fsmith')
freds_department = u.employee.department
But, Signup and Settings forms do not operates as expected and new values for the departement is not recorded.
Any hint is welcome !
I have looked at all the answers but none does really hold the solution for my problem (though some of you gave me quite good hints for looking in the right direction). I will summarize here the solution I have found to solve my problem.
First of all, I have to admit I didn't tell everything about my problem. I wanted to insert extra fields in the User model and use other apps such as the default authentication scheme of Django. So, extending the default User by inheritance and setting AUTH_USER_MODEL was a problem because the other Django applications were stopping to work properly (I believe they didn't use user = models.OneToOneField(settings.AUTH_USER_MODEL) but user = models.OneToOneField(User)).
As, it would have been too long to rewrite properly the other applications I am using, I decided to add this extra field through a One-to-One field. But, the documentation miss several points that I would like to fill in the following.
So, here is a complete example of adding an extra field to the User model with other applications using the same model.
First, write the description of the model gathering the extra fields that you want to add to your models.py file:
from django.contrib.auth.models import User
class UserProfile(models.Model):
user = models.OneToOneField(User)
extra_field = models.CharField(max_length=100)
Then, we need to trigger the addition of an object UserProfile each time a User is created. This is done through attaching this code to the proper signal in the receiver.py file:
from django.contrib.auth.models import User
from django.db.models.signals import post_save
from django.dispatch import receiver
from my_user_profile_app.models import UserProfile
#receiver(post_save, sender=User)
def handle_user_save(sender, instance, created, **kwargs):
if created:
UserProfile.objects.create(user=instance)
Now, if you want to be able to modify it through the administration interface, just stack it with the usual UserAdmin form in the admin.py file.
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from django.contrib.auth.models import User
from my_user_profile_app.models import UserProfile
# Define an inline admin descriptor for UserProfile model
class UserProfileInline(admin.StackedInline):
model = UserProfile
can_delete = False
# Define a new User admin
class UserAdmin(UserAdmin):
inlines = (UserProfileInline, )
# Re-register UserAdmin
admin.site.unregister(User)
admin.site.register(User, UserAdmin)
Then, it is time now to try to mix this extra field with the default Django authentication application. For this, we need to add an extra field to fill in the SignupForm and the SettingsForm through inheritance in the forms.py file:
import account.forms
from django import forms
class SignupForm(account.forms.SignupForm):
extra_field = forms.CharField(label="Extra Field", max_length=100)
class SettingsForm(account.forms.SignupForm):
extra_field = forms.CharField(label="Extra Field", max_length=100)
And, we also need to add some code to display and get properly the data that you have been added to the original User model. This is done through inheritance onto the SignupView and the SettingsView views in the views.py file:
import account.views
from my_user_profile_app.forms import Settings, SignupForm
from my_user_profile_app.models import UserProfile
class SettingsView(account.views.SettingsView):
form_class = SettingsForm
def get_initial(self):
initial = super(SettingsView, self).get_initial()
initial["extra_field"] = self.request.user.extra_field
return initial
def update_settings(self, form):
super(SettingsView, self).update_settings(form)
profile = self.request.user.userprofile
profile.extra_field = form_cleaned_data['extra_field']
profile.save()
class SignupView(account.views.SignupView):
form_class = SignupForm
def after_signup(self, form):
profile = self.created_user.userprofile
profile.extra_field = form_cleaned_data['extra_field']
profile.save()
super(SignupView, self).after_signup(form)
Once everything is in place, it should work nicely (hopefully).
I struggled with this topic for about a year off and on until I finally found a solution I was happy with, and I know exactly what you mean by "there is a lot out there, but it doesn't work". I had tried extending the User model in different ways, I had tried the UserProfile method, and some other 1-off solutions as well.
I finally figured out how to simply extend the AbstractUser class to create my custom user model which has been a great solution for many of my projects.
So, let me clarify one of your comments above, you really shouldn't be creating a link between 2 models, the generally accepted "best" solution is to have one model which is inherited from AbstractUser or AbstractBaseUser depending on your needs.
One tricky thing that got me was that "Extending the User Model" did not get me where I wanted and I needed to Substitute the User Model, which I'm sure you've seen/read multiple times, but possibly not absorbed it (at least I know I didn't).
Once you get the hang of it, there's really not that much code and it's not too complicated either.
# models.py
from django.contrib.auth.models import AbstractUser
from django.db import models
class User(AbstractUser):
'''
Here is your User class which is fully customizable and
based off of the AbstractUser from auth.models
'''
my_custom_field = models.CharField(max_length=20)
def my_custom_model_method(self):
# do stuff
return True
There are a couple things to look out for after this, some of which came up in django 1.7.
First of all, if you want the admin page to look like it did before, you have to use the UserAdmin
# admin.py
from django.contrib import admin
from django.contrib.auth import get_user_model
from django.contrib.auth.admin import UserAdmin
# Register your models here.
admin.site.register(get_user_model(), UserAdmin)
The other thing is that if you're wanting to import the User class in a models file, you have to import it from the settings and not with get_user_model(). If you run into this, it's easy to fix, so I just wanted to give you a heads up.
You can check out my seed project I use to start projects to get a full but simple project that uses a Custom User Model. The User stuff is in the main app.
From there all the Registration and Login stuff works the same way as with a normal Django User, so I won't go into detail on that topic. I hope this helps you as much as it has helped me!
I try to avoid to extend the user model as explained in the django docs.
I use this:
class UserExtension(models.Model):
user=models.OneToOneField(User, primary_key=True)
... your extra model fields come here
Docs of OneToOneField: https://docs.djangoproject.com/en/1.7/topics/db/examples/one_to_one/
I see these benefits:
the same pattern works for other models (e.g. Group)
If you have N apps, every app can extend the model on his own.
Creating the UserExtension should be possible without giving parameters. All fields must have sane defaults.
Then you can create a signal handler which creates UserExtension instances if a user gets created.
I prefer extend the User model. For example:
class UserProfile(User):
def __unicode__(self):
return self.last_name + self.first_name
department = models.CharField(max_length=100)
class SignupForm(forms.Form):
username = forms.CharField(max_length=30)
first_name = forms.CharField(max_length=30)
last_name = forms.CharField(max_length=30)
department = forms.CharField(label="Department", max_length=100)
To save the data
form = UserRegistrationForm(request.POST)
if form.is_valid():
client = UserProfile()
client.username = username
client.set_password(password)
client.first_name = first_name
client.department = department
client.save()
check how are you saving the data after validate the form
I have two models, say, Question and Topic.
I am trying to add methods to the Question model's custom manager, e.g. some method that filters by Topic.
I can't seem to use the other manager's code for this (cannot import Topic either, so I can't do Topic.objects...)
In class QuestionManager
def my_feed(self, user):
topics = TopicManager().filter(user=user) # 1st approach
#topics = Topic.objects.filter(user=user) # 2nd line
# do something with topics
class TopicManager
....
Using 1st approach, I get the following error:
virtualenv/local/lib/python2.7/site-packages/django/db/models/sql/query.pyc in get_meta(self)
219 by subclasses.
220 """
--> 221 return self.model._meta
222
223 def clone(self, klass=None, memo=None, **kwargs):
AttributeError: 'NoneType' object has no attribute '_meta'
I can't use the 2nd line since I can't import Topic, since Topic depends on the TopicManager in this file. Is there a workaround for this?
You can't use a manager directly, in any circumstance. You always access it via the model class.
If you can't import the model at the top of the file because of a circular dependency, you can simply import it inside the method.
You should be able to place this at the bottom of the managers.py module:
# Prevent circular import error between models.py and managers.py
from apps.drs import models
In your manager classes you can reference other models using models.<modelname>, and this should work, avoiding the circular import.
For example:
class QuestionManager(Manager):
def my_feed(self, user):
topics = models.Topic.objects.filter(user=user)
# do something with topics
# Prevent circular import error between models.py and managers.py
from apps.drs import models
This works because you are importing the module, not the model classes, which results in a lazy import. By the time the function is run the module will be imported and everything will work.
You could also load models by name using django.apps.get_model():
from django.apps import apps
apps.get_model('my_app', 'MyModel')
Details here
For example:
from django.apps import apps
class QuestionManager(Manager):
def my_feed(self, user):
topics = apps.get_model('my_app', 'Topic').objects.filter(user=user)
# do something with topics
I have a model that has a ForeignKey to the built-in user model in django.contrib.auth and I'm frustrated by the fact the select box in the admin always sorts by the user's primary key.
I'd much rather have it sort by username alphabetically, and while it's my instinct not to want to fiddle with the innards of Django, I can't seem to find a simpler way to reorder the users.
The most straightforward way I can think of would be to dip into my Django install and add
ordering = ('username',)
to the Meta class of the User model.
Is there some kind of monkeypatching that I could do or any other less invasive way to modify the ordering of the User model?
Alternatively, can anyone thing of anything that could break by making this change?
There is a way using ModelAdmin objects to specify your own form. By specifying your own form, you have complete control over the form's composition and validation.
Say that the model which has an FK to User is Foo.
Your myapp/models.py might look like this:
from django.db import models
from django.contrib.auth.models import User
class Foo(models.Model):
user = models.ForeignKey(User)
some_val = models.IntegerField()
You would then create a myapp/admin.py file containing something like this:
from django.contrib.auth.models import User
from django import forms
from django.contrib import admin
class FooAdminForm(forms.ModelForm):
user = forms.ModelChoiceField(queryset=User.objects.order_by('username'))
class Meta:
model = Foo
class FooAdmin(admin.ModelAdmin):
form = FooAdminForm
admin.site.register(Foo, FooAdmin)
Once you've done this, the <select> dropdown will order the user objects according to username. No need to worry about to other fields on Foo... you only need to specify the overrides in your FooAdminForm class. Unfortunately, you'll need to provide this custom form definition for every model having an FK to User that you wish to present in the admin site.
Jarret's answer above should actually read:
from django.contrib.auth.models import User
from django.contrib import admin
from django import forms
from yourapp.models import Foo
class FooAdminForm(forms.ModelForm):
class Meta:
model = Foo
def __init__(self, *args, **kwds):
super(FooAdminForm, self).__init__(*args, **kwds)
self.fields['user'].queryset = User.objects.order_by(...)
class FooAdmin(admin.ModelAdmin):
# other stuff here
form = FooAdminForm
admin.site.register(Foo, FooAdmin)
so the queryset gets re-evaluated each time you create the form, as opposed to once, when the module containing the form is imported.