django-rest-framework "This field is required" on POST - django

Whenever I POST to my django-rest-framework (DRF) endpoints, I keep receiving a "HTTP 400 Bad Request" {"offeror_organization":["This field is required."]} response. But, given the curl example below, I'm clearly specifying a value.
This happens regardless of the Content-Type (application/json, application/x-www-form-urlencoded, multipart/form-data). The only time it works is when I submit using the "HTML form" (vs. the "Raw Data") tab on the DRF web interface.
There's a few similar SO posts (like this and this), but none of the solutions seem to be working for me.
Model:
class OrganizationManager(models.Manager):
def get_by_natural_key(self, offeror_organization):
return self.get(offeror_organization=offeror_organization)
class Organization(models.Model):
idorganization = models.AutoField(primary_key=True)
offeror_organization = models.CharField(max_length=250, null=False, blank=False, verbose_name='Offeror Organization')
created_at = models.DateTimeField(auto_now_add=True, null=False)
updated_at = models.DateTimeField(auto_now=True, null=False)
objects = OrganizationManager()
def natural_key(self):
return "%s" % (self.offeror_organization)
def __str__(self):
return self.offeror_organization
Serializer:
class OrganizationSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Organization
fields = ['offeror_organization']
# I've tried both with and without a create function
def create(self, validated_data):
organization_data = validated_data.pop('offeror_organization', None)
if organization_data:
organization = Organization.objects.get_or_create(**organization_data)[0]
validated_data['offeror_organization'] = organization
views/api.py:
from webapp.models import Organization
from webapp.serializers import OrganizationSerializer
from rest_framework import viewsets
class OrganizationViewSet(viewsets.ModelViewSet):
queryset = Organization.objects.all().order_by('offeror_organization')
serializer_class = OrganizationSerializer
urls.py:
from django.urls import include, path
from rest_framework import routers
from . import views
router = routers.DefaultRouter()
router.register(r'organization', views.OrganizationViewSet)
urlpatterns = [
...
path('api/', include(router.urls)),
path('api-auth/', include('rest_framework.urls', namespace='rest_framework')),
]
Curl Command:
curl -X POST -H 'Content-Type: application/json' -d '{"offeror_organization":"Test2"}' 10.101.10.228:29000/webapp/api/organization/
settings.py MIDDLEWARE:
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.auth.middleware.RemoteUserMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'csp.middleware.CSPMiddleware',
'whitenoise.middleware.WhiteNoiseMiddleware'
]
settings.py REST_FRAMEWORK
# currently have all API authentication disabled while troubleshooting this issue
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [],
'DEFAULT_PERMISSION_CLASSES': [],
}

In my case, fixing this issue required "maneuvering" around a few different implementation constraints.
nginx + uWSGI Socket + Django REMOTE_USER Authentication:
As mentioned in this post's comments/chat, I've got both an nginx proxy and a uWSGI application server fronting my Django application. Since I'm relying upon REMOTE_USER Authentication, my uwsgi/nginx configuration must use uWSGI sockets (vs. http) so that I may pass the REMOTE_USER from nginx to Django as an environment variable. When using http (coupled w/ nginx proxy_pass), although proxy_pass can set headers or cookies, those seemingly cannot translate over to Django (which requires the environment variable).
I think there's a few issues at play when trying to POST to a Django/DRF application served using uWSGI sockets. Per the uWSGI Things to know (best practices), "TL/DR: if you plan to expose uWSGI directly to the public, use --http, if you want to proxy it behind a webserver speaking http with backends, use --http-socket". In my case, having both a web application and a DRF-based API (that I want other services and systems to talk to), I need both! As a (hopefully temporary) workaround, I'm currently spawning two uWSGI processes - one using --socket, and one using --http (for API POST calls). If you POST while using ---socket, you'll likely get an Empty Response error from DRF.
As an aside, I initially saw some "promise" in utilizing uwsgi_curl (from uwsgi_tools) to POST over the uWSGI socket (which resulted in the "field is required" error (vs. the Empty Response error), but that's when I started to run into my second issue...
POST nested application/json w/ simultaneous file upload: The "Organization" model referenced in the post was mostly proof-of-concept, as it's the least complex model in my Django application. In reality, I need to post to a more complex model with nested serialization, as the model contains Foreign Key's to other models. But that's totally do-able with DRF. Except in my case, where one of my model attributes is a FileUpload field. As noted in other SO Questions (like this one), there's also a few issues in trying to POST nested (i.e. not "flat") application/json with a file upload in a single request. While I was never able to fully understand the issue at play (at least using drf_writable_nested.serializers.WritableNestedModelSerializer in my case), I simplified the problem at-hand by writing my own custom Serializer (serializers.Serializer), this way I could avoid nested JSON objects (like { "offeror_organization": {"offeror_organization: "Test"}} in my POST requests. This fixed my issue.
With the custom serializer in place to mitigate the nested JSON + file upload issue, I bet the uwsgi_curl POST would work. Although then external client systems/services are limited to using that Python package. Anyways, I'll update my answer once I try it out. Thanks to #Michael for his comments and helping to lead me down the right "road".

I have the same setup (nginx + gunicorn + django + rest-framework + drf-writeable-nested) but I could figure out a valid format for the POST request containing multipart/form-data:
It needs to be like this:
json:
{
'name': 'test-name',
'files': [
{
'file': 'test-file-1'
},
{
'file': 'test-file-2'
},
...
],
...
}
must be formatted to:
FormData:
name: test-name
files[0]file: test-file-1
files[1]file: test-file-2
...
Some libraries would use a dot after the brackets for nested lists, which would lead to the This field is required error. Or even another bracket after the list bracket would lead to the same error.
This is wrong:
files[0].file
This is also wrong:
files[0][file]
My example assumes the following Django-classes:
# views.py
from rest_framework.viewsets import ModelViewSet
from rest_framework.parsers import MultiPartParser, FormParser
from .serializers import YourCustomModelSerializer
class YourCustomModelViewSet(ModelViewSet):
queryset = YourCustomModel.objects.all()
parser_classes = [FormParser, MultiPartParser]
permission_classes = []
serializer_class = YourCustomModelSerializer
# serializers.py
from rest_framework.serializers import ModelSerializer
from drf_writable_nested.serializers import WritableNestedModelSerializer
from .models import YourCustomModel, File
class FileSerializer(ModelSerializer):
class Meta:
model = File
fields = ['file']
class YourCustomModelSerializer(WritableNestedModelSerializer):
# Reverse foreign key
files = FileSerializer(many=True)
class Meta:
model = YourCustomModel
read_only_fields = ('id', )
fields = [
'id',
'name',
'files'
]
# models.py
from django.db import models
class File(models.Model):
file = models.FileField()
class YourCustomModel(models.Model):
name = models.CharField(max_length=200)
I used the following javascript/typescript frontend code to pack my json data into a FormData request:
const requestBody = {
name: 'test-name',
files: [
{ file: file1 }, // File object
{ file: file2 }, // File object
{ file: file3 } // File object
]
}
// # use your own code to serialize the above javascript dict/json/object
// into form-data object (I used the library https://www.npmjs.com/package/object-to-formdata but I had to write a patch for the exact reason described above: files[0].file is not correctly parsed by Django and files[0][file] also won't work, therefore I slightly changed the library code so that it will format my object to files[0]file for every nested item:
// 2. prepare a serializer-library to convert the dict into a special form which can be parsed by django.
const options = {
indices: true,
allowEmptyArrays: true,
useDotOrBracketsOrNothingForNestedObjects: 2 // Option 0: DOT-Notation, 1: Brackets, 2: Nothing (this option is from my custom patch)
}
// use npx patch: https://stackoverflow.com/a/62567504/3433137
// (I patched this serialize library and the patch is somewhere stored as a file in this project)
const formData = serialize(
requestBody,
options
)
// 3. upload the data
api.post(url, formData)

Related

Django url path converter not working in production

I'm using path converter in my django app like so:
# urls.py
from . import views
from django.urls import path
urlpatterns = [
path('articles/<str:collection>', views.ArticleView),
]
# views.py
#login_required
def ArticleView(request, collection):
print(collection)
if collection == "None":
articles_query = ArticleModel.objects.all()
...
This works fine in development for a url suck as : http://localhost:8000/articles/My Collection which gets encoded to http://localhost:8000/articles/My%20Collection, and is decoded properly in the ArticleView. However, in development, I have to edit the view like so to get it to work:
# views.py
import urllib.parse
#login_required
def ArticleView(request, collection):
collection = urllib.parse.unquote(collection)
print(collection)
if collection == "None":
articles_query = ArticleModel.objects.all()
...
Otherwise, the print(collection) shows My%20Collection and the whole logic in the rest of the view fails.
requirements.txt
asgiref==3.2.10
Django==3.1.1
django-crispy-forms==1.9.2
django-floppyforms==1.9.0
django-widget-tweaks==1.4.8
lxml==4.5.2
Pillow==7.2.0
python-pptx==0.6.18
pytz==2020.1
sqlparse==0.3.1
XlsxWriter==1.3.3
pymysql
What am I doing wrong here?
Thanks in advance!
The URL is being urlencoded which encodes spaces as %20. There are a number of other encodings. As you've discovered you need to decode that parameter in order to compare it to what you'd expect. As you've likely realized, if you have a value that actually wants The%20News and not The News, you have no recourse. To handle this people will create a slug field. Django has a model field for this in the framework.
This is typically a URL-friendly, unique value for the record.
Assuming you add a slug = models.SlugField() to ArticleModel, your urls and view can change into:
urlpatterns = [
# Define a path without a slug to identify the show all code path and avoid the None sentinel value.
path('articles', views.ArticleView, name='article-list'),
path('articles/<slug:slug>' views.ArticleView, name='article-slug-list'),
]
#login_required
def ArticleView(request, slug=None):
articles_query = ArticleModel.objects.all()
if slug:
articles_query = articles_query.filter(slug=slug)

Absolute paths on images uploaded by django-ckeditor

I am using django-rest-framework in conjuntion with django-ckeditor. I'm serving some images with absolute url-s without any problem. But images and files uploaded by ckeditor are served as relative paths, and they can't be displayed client side since it is in a different domain.
Here is an example of what I'm getting:
{
image: "http://example.com/media/myimage.png",
body: "<p>download my file</p>"
}
And this is what I woul like to get:
{
image: "http://example.com/media/myimage.png",
body: "<p>download my file</p>"
}
Edit:
This would be the model of my example:
from django.db import models
from ckeditor_uploader.fields import RichTextUploadingField
image: models.ImageField()
body: RichTextUploadingField(blank=True,null=True)
I would use a custom serializer to fix that:
from rest_framework import serializers
def relative_to_absolute(url):
return 'http://127.0.0.1:8000' + url
class FileFieldSerializer(serializers.Field):
def to_representation(self, value):
url = value.url
if url and url.startswith('/'):
url = relative_to_absolute(url)
return url
When filefield.url contains a relative url, relative_to_absolute() is called to prepend the domain.
Here I just used a constant string; you can either save it in your settings, or, if Django Site framework is installed, retrieve it as follows:
from django.contrib.sites.models import Site
domain=Site.objects.get_current().domain
Sample usage of the custom serializer:
class Picture(BaseModel):
...
image = models.ImageField(_('Image'), null=True, blank=True)
...
class PictureSerializer(serializers.ModelSerializer):
image = FileFieldSerializer()
class Meta:
model = Picture
fields = '__all__'
Variation for RichTextUploadingField
If, on the other hand, you're using RichTextUploadingField by CKEditor, your field is, basically, a TextField where an HTML fragment is saved upon images
upload.
In this HTML fragment, CKEditor will reference the uploaded images with a relative path, for very good reasons:
your site will still work if the domain is changed
the development instance will work in localhost
after all, we're using Django, not WordPress ;)
So, I wouldn't touch it, and fix the path at runtime in a custom serializer instead:
SEARCH_PATTERN = 'href=\\"/media/ckeditor/'
SITE_DOMAIN = "http://127.0.0.1:8000"
REPLACE_WITH = 'href=\\"%s/media/ckeditor/' % SITE_DOMAIN
class FixAbsolutePathSerializer(serializers.Field):
def to_representation(self, value):
text = value.replace(SEARCH_PATTERN, REPLACE_WITH)
return text
Alternatively, domain can be saved in settings:
from django.conf import settings
REPLACE_WITH = 'href=\\"%s/media/ckeditor/' % settings.SITE_DOMAIN
or retrieved from Django Site framework as follows:
from django.contrib.sites.models import Site
REPLACE_WITH = 'href=\\"{scheme}{domain}/media/ckeditor/'.format(
scheme="http://",
domain=Site.objects.get_current().domain
)
You might need to adjust SEARCH_PATTERN according to your CKEditor configuration; the more specific, the better.
Sample usage:
class Picture(BaseModel):
...
body = RichTextUploadingField(blank=True,null=True)
...
class PictureSerializer(serializers.ModelSerializer):
body = FixAbsolutePathSerializer()
class Meta:
model = Picture
fields = '__all__'

Testing Django Rest Framework: how to test hyperlink relations?

I'm trying to create a true unit test for a customized DjangoRestFramework Hyperlinked related field. But I cannot seem to get around this error:
django.core.exceptions.ImproperlyConfigured: Could not resolve URL for hyperlinked relationship using view name "relatedtestmodel-detail". You may have failed to include the related model in your API, or incorrectly configured the `lookup_field` attribute on this field.
And here is the unit test, stripped down to simplify the example:
from django.conf.urls import url
from django.test import TestCase, override_settings
from api_tests.models import APITestModel, RelatedTestModel
from api_tests.serializers import APITestModelSerializer
def dummy_view(request, pk):
pass
urlpatterns = [
url(r'/path/is/irrelevant/', dummy_view, name='relatedtestmodel-detail')
]
#override_settings(ROOT_URLCONF='tests.test_relations')
class HyperlinkedRelatedFieldTestCase(TestCase):
def setUp(self):
self.parent = APITestModel.objects.create()
self.child = RelatedTestModel.objects.create(parent=self.parent)
assert self.child.parent.id == self.parent.id
def test_to_internal_value_correct_error_message(self):
queryset = APITestModel.objects.all()
serializer = APITestModelSerializer(queryset, many=True, context={'request': None})
expected = [{'foo': 'bar'}]
self.assertEqual(serializer.data, expected)
I more or less lifted the test from https://github.com/encode/django-rest-framework/blob/master/tests/test_relations_hyperlink.py, because I figured who knows best how to unit test DRF than the makers of DRF? But as it stands, my test refuses to run. The error is raised during the test, when I attempt to access serializer.data for the assert.
Notice in particular that I override the settings with a custom urlpatterns (which is this same file, hence the urlpatterns at the top). So I don't understand why DRF thinks that url name doesn't exist - I can clearly see that my url conf in fact has ONLY that view name! I've even gone so far as to edit my actual URL conf and replace it with the single, named, dummy url pattern shown here, and removedthe settings override, just to be sure that it wasn't that the override_settings simply wasn't working, but even then I get the same error.
To my eye, the dummy url pattern is exactly the same as how DRF did it in their tests. Anyone have any ideas what is going on?
A bit more requested context:
api_tests.models:
from django.db import models
class APITestModel(models.Model):
pass
class RelatedTestModel(models.Model):
parent = models.ForeignKey(
APITestModel,
related_name='children',
related_query_name='child'
)
I do not have access to the traceback at this time, but I can confirm it did not pass through any of my code - it was all isolated to the DjangoRestFramework code, basically exclusively relations.py
Preamble
A few things this question is lacking
No definition of APITestModelSerializer
RelatedTestModel is not used in the test and therefore irrelevant for the example
No error stacktrace
No "useful" expected dict for the asserts
APITestModel has no fields so it can't be serialized (your test shouldn't even have run)
Minor things but still relevant
You are creating specific instances of APITestModel and RelatedTestModel in the setUp but in the test you serialize all instances of APITestModel
The line assert self.child.parent.id == self.parent.id should not be in the setUp. It should be in a separate test
My changes
I deleted all irrelevant information for this question mentioned above
I added an integer field to APITestModel
I changed the urlpatterns element from url(...) to path(...)
I added a regex to the relative path
The serializer is a subclass of HyperlinkedModelSerializer and includes fields "url" and "year"
My project and app urls.py files are the "stock" ones (not shown here) to emphasize that this test resolves the path in isolation.
Changed #override_settings(ROOT_URLCONF='tests.test_relations') to #override_settings(ROOT_URLCONF=__name__)
Code
models.py
from django.db import models
class APITestModel(models.Model):
year = models.IntegerField(null=False)
serializers.py
from rest_framework import serializers
from api.models import APITestModel
class APITestModelSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = APITestModel
fields = ["url", "year"]
test_serializers.py
from django.test import TestCase, override_settings
from django.urls import path
from api.models import APITestModel
from api.serializers import APITestModelSerializer
urlpatterns = [
path('whateveryouwant/<int:pk>/', lambda request: None, name='apitestmodel-detail'),
]
#override_settings(ROOT_URLCONF=__name__)
class HyperlinkedRelatedFieldTestCase(TestCase):
def setUp(self):
# Populate db with APITestModel instances
_ = APITestModel.objects.create(year=1960)
_ = APITestModel.objects.create(year=1961)
_ = APITestModel.objects.create(year=1962)
def test_to_internal_value_correct_error_message(self):
queryset = APITestModel.objects.all()
serializer = APITestModelSerializer(queryset, many=True, context={'request': None})
expected = [
{'url': '/whateveryouwant/1/', 'year': 1960},
{'url': '/whateveryouwant/2/', 'year': 1961},
{'url': '/whateveryouwant/3/', 'year': 1962},
]
self.assertEqual(serializer.data, expected)
The other files in the project are the default ones created automatically by django + djangorestframework.
For future readers, I created a github project with this working code and can be found here: https://github.com/Alechan/drf_test_hyperlink_relations

Django rest framework: How to reuse an app using different settings?

I have an application that has it's own urls and uses specific settings to access another api.
I would like to use this same app again within the same project, but with different urls and using a seperate endpoint.
So just setup new urls, and point to the same views from the original app but inject different settings.
For example one of my views is:
class SummaryVMsList(ListAPIView):
'''
VM Summary
'''
def list(self, request, *args, **kwargs):
'''
Return a list of processed vm's
'''
v_token = settings.VTOKEN
base_url = settings.VURL
v_password = settings.VPASSWORD
v_username = settings.VUSERNAME
session = Session()
session.headers.update({
'v_token': v_token
})
client = VClient(
url=base_url,
v_username=v_username,
v_password=v_password,
session=session
)
try:
repos = client.get_summary_vms()
return Response(data=repos, status=status.HTTP_200_OK)
except VError as err:
return Response(
data={'error': str(err)},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
# log the error
finally:
client.logout()
How would I be able to change the setting values: settings.VTOKEN, settings.VURL, settings.VPASSWORD and settings.VUSERNAME
Based on whick url is used:
In urls-site1.py
app_name = 'v_site1'
urlpatterns = [
path('vm-summary', views.SummaryVMsList.as_view(), name='vms_list'),
]
In urls-site2.py:
app_name = 'v_site2'
urlpatterns = [
path('vm-summary', views.SummaryVMsList.as_view(), name='vms_list'),
]
In this case, a better idea would be to store such params in constants instead of settings. The settings in Django used for different environments, in the end, it can be hard to work with it.
Also, take note that there are two exactly the same URL vm-summary and it could cause a name collision.

mocking a method on django model using post_save signal

So here's something I'm trying to figure out. I've got a method that is triggered by post_save
for this "Story" model. Works fine. What I need to do is figure out how to mock out the test, so I can fake the call and make assertions on my returns. I think I need to patch it somehow, but I've tried a couple different ways without much success. Best i can get is a object instance, but it ignores values I pass in.
I've commented in my test where my confusion lies. Any help would be welcome.
Here's my test:
from django.test import TestCase
from django.test.client import Client
from marketing.blog.models import Post, Tag
from unittest.mock import patch, Mock
class BlogTestCase(TestCase):
fixtures = [
'auth-test.json',
'blog-test.json',
]
def setUp(self):
self.client = Client()
def test_list(self):
# verify that we can load the list page
r = self.client.get('/blog/')
self.assertEqual(r.status_code, 200)
self.assertContains(r, "<h1>The Latest from Our Blog</h1>")
self.assertContains(r, 'Simple JavaScript Date Formatting')
self.assertContains(r, 'Page 1 of 2')
# loading a page out of range should redirect to last page
r = self.client.get('/blog/5/', follow=True)
self.assertEqual(r.redirect_chain, [
('http://testserver/blog/2/', 302)
])
self.assertContains(r, 'Page 2 of 2')
# verify that unpublished posts are not displayed
with patch('requests') as mock_requests:
# my futile attempt at mocking.
# creates <MagicMock> object but not able to call return_values
mock_requests.post.return_value = mock_response = Mock()
# this doesn't get to the magic mock object. Why?
mock_response.status_code = 201
p = Post.objects.get(id=5)
p.published = False
# post_save signal runs here and requests is called.
# Needs to be mocked.
p.save()
r = self.client.get('/blog/')
self.assertNotContains(r, 'Simple JavaScript Date Formatting')
Here's the model:
from django.db import models
from django.conf import settings
from django.db.models import signals
import requests
def update_console(sender, instance, raw, created, **kwargs):
# ignoring raw so that test fixture data can load without
# hitting this method.
if not raw:
update = instance
json_obj = {
'author': {
'alias': 'the_dude',
'token': 'the_dude'
},
'text': update.description,
}
headers = {'content-type': 'application/json'}
path = 'http://testserver.com:80/content/add/'
request = requests(path, 'POST',
json_obj, headers=headers,
)
if request.status_code < 299:
story_id = request.json().get('id')
if story_id:
# disconnect and reconnect signal so
# we don't enter recursion-land
signals.post_save.disconnect(
update_console,
sender = Story, )
update.story_id = story_id
update.save()
signals.post_save.connect(
update_console,
sender = Story, )
else:
raise AttributeError('Error Saving to console, '+ request.text)
class Story(models.Model):
"""Lets tell a story"""
story_id = models.CharField(
blank=True,
max_length=10,
help_text="This maps to the id of the post"
)
slug = models.SlugField(
unique=True,
help_text="This is used in URL and in code references.",
)
description = models.TextField(
help_text='2-3 short paragraphs about the story.',
)
def __str__(self):
return self.short_headline
# add/update this record as a custom update in console
signals.post_save.connect(update_console, sender = Story)
You need to patch requests in the module where it is actually used, i.e.
with patch('path.to.your.models.requests') as mock_requests:
mock_requests.return_value.status_code = 200
mock_requests.return_value.json.return_value = {'id': story_id'}
...
The documentation offers more detailed explanations on where to patch:
patch works by (temporarily) changing the object that a name points to with another one. There can be many names pointing to any individual object, so for patching to work you must ensure that you patch the name used by the system under test.
The basic principle is that you patch where an object is looked up, which is not necessarily the same place as where it is defined.
Here, you need to patch the name requests inside the models module, hence the need to provide its full path.