I'm creating API for Twitter like app in Django and since I implemented token authentication (rest-auth) I have problem with creating new tweets as it throws:
{
"author": [
"This field is required."
]
}
I've tried CreateAPIView:
class TweetCreateAPIView(CreateAPIView):
serializer_class = TweetModelSerializer
permission_classes = (IsAuthenticated,)
# I've also tried to add the author field mannualy
def perform_create(self, serializer):
serializer.save(author=self.request.user)
but it didn't work so I created custom post method:
class TweetCreateAPIView(APIView):
permission_classes = (IsAuthenticated,)
def post(self, request, format=None):
serializer = TweetModelSerializer(data=request.data)
if serializer.is_valid():
serializer.save(author=request.user)
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
but it still can't identify the user creating the tweet
Model:
class Tweet(models.Model):
retweeted_by = models.ManyToManyField(
TwitterUser, blank=True, related_name='retweets')
commented_tweet = models.ManyToManyField(
'self', related_name='comments', blank=True, symmetrical=False)
author = models.ForeignKey(
TwitterUser, on_delete=models.CASCADE, related_name='tweets')
content = models.CharField(max_length=280)
created_at = models.DateTimeField(auto_now_add=True)
liked_by = models.ManyToManyField(
TwitterUser, blank=True, related_name='likes')
objects = TweetManager()
def __str__(self):
return self.content
def get_comments(self):
comments = Tweet.objects.filter(commented_tweet__pk=self.pk)
return comments
def get_retweets(self):
retweets = TwitterUser.retweets(tweet__id=self.id)
def get_absolute_url(self):
return reverse('tweets:pk', kwargs={'pk': self.pk})
Serializer:
class TweetModelSerializer(serializers.ModelSerializer):
likes_count = serializers.SerializerMethodField()
already_liked = serializers.SerializerMethodField()
def get_likes_count(self, tweet):
return tweet.liked_by.all().count()
def get_already_liked(self, tweet):
user = None
request = self.context.get("request")
if request and hasattr(request, "user"):
user = request.user
if user is not None:
if user in tweet.liked_by.all():
return True
else:
return False
else:
pass
class Meta:
model = Tweet
fields = [
'id',
'author',
'commented_tweet',
'content',
'retweeted_by',
'likes_count',
'already_liked',
'created_at',
]
Settings:
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
# 3rd party
'rest_framework',
'rest_framework.authtoken',
'rest_auth',
'django.contrib.sites',
'channels',
'allauth',
'allauth.account',
'rest_auth.registration',
'reset_migrations',
'corsheaders',
]
REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.DjangoModelPermissionsOrAnonReadOnly'
],
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework.authentication.SessionAuthentication',
'rest_framework.authentication.TokenAuthentication',
),
'DEFAULT_PAGINATION_CLASS': 'twitter.paginations.StandardResultsSetPagination',
'TEST_REQUEST_DEFAULT_FORMAT': 'json',
}
Authentication itself works fine, including registration, so I don't think that it's a problem with tokens, yet I have no other idea where the bug is when even CreateAPIView doesn't work.
In your serializer you have author in the list of fields. Since it's a ModelSerializer, drf is picking up details for that field from the model, and in the model author is null = False by default, hence drf is making it a required field, since it can't be null. But since you want the author to be automatically the user who is making the request, you don't need that field to be editable. Hence make it a read only field in your serializer like this:
class TweetModelSerializer(serializers.ModelSerializer):
...
class Meta:
model = Tweet
fields = ('id', 'author', 'commented_tweet', 'content', 'retweeted_by',
'likes_count', 'already_liked', 'created_at', )
read_only_fields = ('author', ) # it's read only now, so drf go looking for it in POST data
Now you just need to override the perform_create method, no need to change the post method.
If you need even more customizations like superuser can edit authors but no one else, you can override the __init__ method your serializer and make fields read_only based on request.user which gets a bit complicated.
Also, you should consider using tuple instead of list for fields and read_only_fields to get minor performance boost.
Related
These are my version
Django==3.0.2
djangorestframework==3.11.0
and this is my setting
REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticated',
],
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination',
'PAGE_SIZE': 10
}
and this is my views:
class CostList(ListCreateAPIView):
serializer_class = CostSerializers
def get_queryset(self):
cost = Cost.objects.filter(
id='filtered with one of my id'
)
return cost
this is my serializer:
class CostSerializers(ModelSerializer):
class Meta:
model = Cost
fields = '__all__'
Everything is working fine but the only issue is pagination. I have 100+ entry in cost model and I see it is rendering all the entry together, not paginating item following my settings
class CostList(ListCreateAPIView):
serializer_class = CostSerializers
def get_queryset(self):
cost = Cost.objects.filter(id='filtered with one of my id')
return cost
def get(self, request, *args, **kwargs):
qs = self.get_queryset()
page = self.paginate_queryset(qs)
return self.get_paginated_response(page)
Try this.
I am trying to add object level permission to my django REST project using django-guardian, but I am getting
http://127.0.0.1:8000/api/v1/tasks/
HTTP 403 Forbidden
Allow: GET, POST, HEAD, OPTIONS
Content-Type: application/json
Vary: Accept
{
"detail": "You do not have permission to perform this action."
}
The user joe is logged in.
settings.py:
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'django.contrib.sites',
'guardian',
'rest_framework',
'rest_framework.authtoken',
'rest_auth',
'task.apps.TaskConfig',
]
models.py:
class Task(models.Model):
summary = models.CharField(max_length=32)
content = models.TextField()
reported_by = models.ForeignKey(User, on_delete=models.CASCADE)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
permissions = (
('view_task', 'View task'),
)
serializers.py:
class TaskSerializer(serializers.ModelSerializer):
class Meta:
model = Task
fields = '__all__'
permissions.py:
class CustomObjectPermissions(permissions.DjangoObjectPermissions):
perms_map = {
'GET': ['%(app_label)s.view_%(model_name)s'],
'OPTIONS': ['%(app_label)s.view_%(model_name)s'],
'HEAD': ['%(app_label)s.view_%(model_name)s'],
'POST': ['%(app_label)s.add_%(model_name)s'],
'PUT': ['%(app_label)s.change_%(model_name)s'],
'PATCH': ['%(app_label)s.change_%(model_name)s'],
'DELETE': ['%(app_label)s.delete_%(model_name)s'],
}
filters.py:
class DjangoObjectPermissionsFilter(BaseFilterBackend):
perm_format = '%(app_label)s.view_%(model_name)s'
shortcut_kwargs = {
'accept_global_perms': False,
}
def __init__(self):
assert 'guardian' in settings.INSTALLED_APPS, (
'Using DjangoObjectPermissionsFilter, '
'but django-guardian is not installed.')
def filter_queryset(self, request, queryset, view):
from guardian.shortcuts import get_objects_for_user
user = request.user
permission = self.perm_format % {
'app_label': queryset.model._meta.app_label,
'model_name': queryset.model._meta.model_name,
}
return get_objects_for_user(
user, permission, queryset,
**self.shortcut_kwargs)
views.py:
class TaskViewSet(viewsets.ModelViewSet):
queryset = Task.objects.all()
serializer_class = TaskSerializer
permission_classes = (CustomObjectPermissions,)
filter_backends = (DjangoObjectPermissionsFilter,)
urls.py:
router = DefaultRouter()
router.register('tasks', TaskViewSet, base_name='tasks')
urlpatterns = router.urls
But it works fine in shell
> python manage.py shell -i ipython
In [1]: from django.contrib.auth.models import User
In [2]: joe = User.objects.all().filter(username="joe")[0]
In [3]: import task.models as task_models
In [4]: task = task_models.Task.objects.all()[0]
In [5]: joe.has_perm('view_task', task)
Out[5]: True
The API first checks model-level permissions, then object-level permissions if they apply. Since the custom permissions class requires the user to have read-permissions, you need to ensure that Joe has been assigned model-level read access. If you check joe.has_perm('tasks.view_task'), I would bet that it returns False. To fix this, you either need to directly assign his user the permission, or add him to a group that has been assigned the appropriate permissions.
Also, note that Django 2.1 recently added the "view" permission, and it shouldn't be necessary to add it to your models anymore.
I've installed django-registration-redux and custom_user. I have a UserProfile model with fullname,dob and photo as extra fields.
I have connected the UserProfile with user-registration so as to save the additional fields to UserProfile table.
Here my questions are...
How do I validate fields in UserProfile model at the time of registration so that I can prevent addition of a user to the emailuser table. Say, if the dob given is invalid or not in allowed range, then stop adding a user to emailuser table.
How can I make an EditProfile system that allows authenticated users to edit their profile (/user/edit - id to be obtained from session).
Below is my settings.py file
INSTALLED_APPS = [
'django.contrib.admin',
'custom_user',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
#------------
'registration',
'userprofile',
]
AUTH_USER_MODEL = 'custom_user.EmailUser'
models.py
class UserProfile(models.Model):
user=models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE,
primary_key=True,)
fullname=models.CharField(max_length=70, blank=False)
dob=models.DateField(blank=False)
photo=models.ImageField(upload_to='profile_images', blank=False)
def __str__(self):
return self.user.email
def user_registered_callback(sender, user, request, **kwargs):
profile = UserProfile(user = user)
profile.fullname =request.POST["fullname"]
profile.dob ="%s-%s-%s" % (request.POST["dob_year"],request.POST["dob_month"],request.POST["dob_day"])
if 'photo' in request.FILES:
profile.photo = request.FILES['photo']
profile.save()
user_registered.connect(user_registered_callback)
forms.py
class UserProfileForm(RegistrationForm):
fullname=forms.CharField(required=True,label="Full name", min_length=3, max_length=75)
dob=forms.DateField(required=True,label="Date of birth",
widget=forms.SelectDateWidget(years=range(now.year-settings.AGE_UPPER,now.year-settings.AGE_LOWER)))
photo=forms.ImageField(required=False)
views.py
def profile(request):
if not request.user.is_authenticated():
return HttpResponseRedirect('/user/login')
else:
return render(request, 'userprofile/profile.html', {"user":request.user})
class EditProfile(UpdateView):
template_name = 'userprofile/edit_profile.html'
fields = ['fullname','dob','photo']
main urls.py
urlpatterns = [
url(r'^admin/', admin.site.urls),
url(r'user/register/$',
RegistrationView.as_view(form_class = UserProfileForm),
name = 'registration_register'),
url(r'^user/', include('registration.backends.default.urls')),
url(r'^user/', include('userprofile.urls')),
userprofile.urls.py
urlpatterns = [
url(r'^profile/$', views.profile, name='profile'),
url(r'^profile/edit/$', views.EditProfile.as_view(), name='edit_profile'),
]
Right now I'm, getting EditProfile is missing a QuerySet. Define EditProfile.model, EditProfile.queryset, or override EditProfile.get_queryset().
Is it the right way to proceed? Any help would be appreciated.
Thanks
I am using django-rest-swagger for api doc generation. I would like to return different user the different part of models. So I used the get_serializer_class() function to use different serializer for different users by checking the kwargs['pk'] on the url.
The urls.py is:
url(r'^api/users/$', views.UserEnum.as_view()),
url(r'^api/users/(?P<pk>[0-9]+)/$', views.UserDetail.as_view()),
The view that I am using is:
#List all the users or create a new user
class UserEnum(generics.ListCreateAPIView):
queryset = User.objects.all()
serializer_class = UserSerializer
permission_classes = (IsAuthenticated, IsAdminUser,)
class UserDetail(generics.RetrieveUpdateDestroyAPIView):
queryset = User.objects.all()
#serializer_class = UserSerializer #TODO: Problem with Swagger
permission_classes = (IsAuthenticated, IsOwnerAdminOrReadOnly,)
def get_serializer_class(self):
#swagger crash here.
user_id = int(self.kwargs['pk'])
if self.request.user.id == user_id or self.request.user.is_superuser:
serializer_class = UserSerializer
else:
serializer_class = UserProfileSerializer
return serializer_class
def get_object(self, *args, **kwargs):
user_id = int(self.kwargs['pk'])
if self.request.user.id == user_id or self.request.user.is_superuser:
return User.objects.get(id=user_id)
else:
return UserProfile.objects.get(user=user_id)
The serializer classes I am using are:
class UserProfileSerializer(serializers.ModelSerializer):
class Meta:
model = UserProfile
fields = ('photo', 'description', 'birthday', 'credit',)
class UserSerializer(serializers.ModelSerializer):
profile = UserProfileSerializer()
class Meta:
model = User
fields = ('id', 'last_login', 'username', 'email', 'date_joined', 'profile', )
...
The UserProfile is:
class UserProfile(models.Model):
user = models.OneToOneField(User, primary_key=True, related_name="profile")
photo = models.ImageField(upload_to='user_photo', null=True, blank=True)
description = models.TextField(blank=True)
birthday = models.DateTimeField(blank=True)
credit = models.PositiveIntegerField()
def __unicode__(self):
return u'%s' % (self.user.username)
The above two urls work successfully without problems. (Or anyone can tell me The right way of returning different fields for different users based on the url parameters? ) But when I type the http://127.0.0.1:8000/api/ for generate api docs, it return the below error:
Unable to read api 'users' from path http://127.0.0.1:8000/api/api-docs/api/users (server returned Internal Server Error)
So I test the http://127.0.0.1:8000/api/api-docs/api/users alone, and I get the error tips with more details:
Environment:
Request Method: GET
Request URL: http://127.0.0.1:8000/api/api-docs/api/users
Django Version: 1.6.5
Python Version: 2.7.8
Installed Applications:
('django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'south',
'rest_framework',
'rest_framework_swagger',
'django_extensions',
'***')
Installed Middleware:
('django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware')
Traceback:
File "/Users/Scofield/anaconda/lib/python2.7/site-packages/django/core/handlers/base.py" in get_response
112. response = wrapped_callback(request, *callback_args, **callback_kwargs)
File "/Users/Scofield/anaconda/lib/python2.7/site-packages/django/views/decorators/csrf.py" in wrapped_view
57. return view_func(*args, **kwargs)
File "/Users/Scofield/anaconda/lib/python2.7/site-packages/django/views/generic/base.py" in view
69. return self.dispatch(request, *args, **kwargs)
File "/Users/Scofield/anaconda/lib/python2.7/site-packages/rest_framework/views.py" in dispatch
407. response = self.handle_exception(exc)
File "/Users/Scofield/anaconda/lib/python2.7/site-packages/rest_framework/views.py" in dispatch
404. response = handler(request, *args, **kwargs)
File "/Users/Scofield/anaconda/lib/python2.7/site-packages/rest_framework_swagger/views.py" in get
119. 'apis': generator.generate(apis),
File "/Users/Scofield/anaconda/lib/python2.7/site-packages/rest_framework_swagger/docgenerator.py" in generate
32. 'operations': self.get_operations(api, apis),
File "/Users/Scofield/anaconda/lib/python2.7/site-packages/rest_framework_swagger/docgenerator.py" in get_operations
68. serializer = self._get_method_serializer(method_introspector)
File "/Users/Scofield/anaconda/lib/python2.7/site-packages/rest_framework_swagger/docgenerator.py" in _get_method_serializer
165. serializer = method_inspector.get_response_serializer_class()
File "/Users/Scofield/anaconda/lib/python2.7/site-packages/rest_framework_swagger/introspectors.py" in get_response_serializer_class
203. serializer = self.get_serializer_class()
File "/Users/Scofield/anaconda/lib/python2.7/site-packages/rest_framework_swagger/introspectors.py" in get_serializer_class
196. serializer = self.ask_for_serializer_class()
File "/Users/Scofield/anaconda/lib/python2.7/site-packages/rest_framework_swagger/introspectors.py" in ask_for_serializer_class
179. return view.get_serializer_class()
File "/Users/Scofield/Dropbox/My Projects/Angels&Demons/src/doWishSites/doWishCore/views.py" in get_serializer_class
27. user_id = int(self.kwargs['pk'])
Exception Type: KeyError at /api/api-docs/api/users
Exception Value: 'pk'
Try raping your code that accesses self.kwargs['pk']
with a try and except block
def get_serializer_class(self):
# swagger crash here.
try:
user_id = int(self.kwargs['pk'])
except Exception as e:
print(e)
if self.request.user.id == user_id or self.request.user.is_superuser:
serializer_class = UserSerializer
else:
serializer_class = UserProfileSerializer
return serializer_class
Source https://github.com/marcgibbons/django-rest-swagger/issues/194
I have a model that has a CharField and in the admin I want to add choices to the widget. The reason for this is I'm using a proxy model and there are a bunch of models that share this CharField but they each have different choices.
class MyModel(MyBaseModel):
stuff = models.CharField('Stuff', max_length=255, default=None)
class Meta:
proxy = True
class MyModelAdmin(admin.ModelAdmin):
fields = ('stuff',)
list_display = ('stuff',)
admin.site.register(MyModel, MyModelAdmin)
For this model I want to use MY_CHOICES in MyModelAdmin.
Do I override a widget? Do I need to override the whole form?
from django.contrib import admin
from django import forms
class MyModel(MyBaseModel):
stuff = models.CharField('Stuff', max_length=255, default=None)
class Meta:
proxy = True
class MyModelForm(forms.ModelForm):
MY_CHOICES = (
('A', 'Choice A'),
('B', 'Choice B'),
)
stuff = forms.ChoiceField(choices=MY_CHOICES)
class MyModelAdmin(admin.ModelAdmin):
fields = ('stuff',)
list_display = ('stuff',)
form = MyModelForm
admin.site.register(MyModel, MyModelAdmin)
See: https://docs.djangoproject.com/en/dev/ref/forms/fields/#choicefield
You don't need a custom form.
This is the minimum you need:
# models.py
from __future__ import unicode_literals
from django.db import models
class Photo(models.Model):
CHOICES = (
('hero', 'Hero'),
('story', 'Our Story'),
)
name = models.CharField(max_length=250, null=False, choices=CHOICES)
# admin.py
from django.contrib import admin
from .models import Photo
class PhotoAdmin(admin.ModelAdmin):
list_display = ('name',)
admin.site.register(Photo, PhotoAdmin)
You can override formfield_for_choice_field() that way you don't need to create a new form.
class MyModelAdmin(admin.ModelAdmin):
def formfield_for_choice_field(self, db_field, request, **kwargs):
if db_field.name == 'status':
kwargs['choices'] = (
('accepted', 'Accepted'),
('denied', 'Denied'),
)
if request.user.is_superuser:
kwargs['choices'] += (('ready', 'Ready for deployment'),)
return super().formfield_for_choice_field(db_field, request, **kwargs)
See formfield_for_choice_field
You need to override the form the ModelAdmin is going to use:
class MyForm(forms.ModelForm):
stuff = forms.CharField('Stuff', max_length=255, choices=MY_CHOICES, default=None)
class Meta:
model = MyModel
fields = ('stuff', 'other_field', 'another_field')
class MyModelAdmin(admin.ModelAdmin):
fields = ('stuff',)
list_display = ('stuff',)
form = MyForm
If you need your choices to be dynamic, maybe you could do something similar to:
class MyForm(forms.ModelForm):
stuff = forms.CharField('Stuff', max_length=255, choices=MY_CHOICES, default=None)
def __init__(self, stuff_choices=(), *args, **kwargs):
# receive a tupple/list for custom choices
super(MyForm, self).__init__(*args, **kwargs)
self.fields['stuff'].choices = stuff_choices
and in your ModelAdmin's __init__ define what MY_CHOICES is going to be and assign the form instance there instead:
Good luck! :)
in Gerard's answer, if you keep :
def __init__(self, stuff_choices=(), *args, **kwargs):
then when you will try to add new model from admin, you will always get 'This field is required.' for all required fields.
you should remove stuff_choices=() from initialization:
def __init__(self,*args, **kwargs):
You need to think of how you are going to store the data at a database level.
I suggest doing this:
Run this pip command: pip install django-multiselectfield
In your models.py file:
from multiselectfield import MultiSelectField
MY_CHOICES = (('item_key1', 'Item title 1.1'),
('item_key2', 'Item title 1.2'),
('item_key3', 'Item title 1.3'),
('item_key4', 'Item title 1.4'),
('item_key5', 'Item title 1.5'))
class MyModel(models.Model):
my_field = MultiSelectField(choices=MY_CHOICES)
In your settings.py:
INSTALLED_APPS = (
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.sites',
'django.contrib.admin',
#.....................#
'multiselectfield',
)
Watch the MAGIC happen!
Source:
https://pypi.python.org/pypi/django-multiselectfield
Below a solution that works immediately with Postgres' special ArrayField:
# models.py
class MyModel(models.Model):
class Meta:
app_label = 'appname'
name = models.CharField(max_length=1000, blank=True)
ROLE_1 = 'r1'
ROLE_2 = 'r2'
ROLE_3 = 'r3'
ROLE_CHOICES = (
(ROLE_1, 'role 1 name'),
(ROLE_2, 'role 2 name'),
(ROLE_3, 'role 3 name'),
)
roles = ArrayField(
models.CharField(choices=ROLE_CHOICES, max_length=2, blank=True),
default=list
)
# admin.py
class MyModelForm(ModelForm):
roles = MultipleChoiceField(choices=MyModel.ROLE_CHOICES, widget=CheckboxSelectMultiple)
#admin.register(MyModel)
class MyModelAdmin(admin.ModelAdmin):
form = MyModelForm
list_display = ("pk", "name", "roles")
(Django 2.2)