Django - receiving multiple varities of json objects in the same post method - json

from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from .models import QandA
from .serializers import QandASerializer
import json
import random
from itertools import count
class QandAlist(APIView):
_ids = count(0)
def __init__(self):
self.id = next(self._ids)
def get(self, request):
questions = QandA.objects.all()
serializer = QandASerializer(questions, many=True)
return Response(serializer.data)
def post(self, request):
serializer = QandASerializer(data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
else:
try:
n = json.loads(request.body)
return Response(n)
except:
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
else:
questions = QandA.objects.order_by('?')[:n]
serializer = QandASerializer(questions, many=True)
return Response(serializer.data)
here i am trying to accept 2 kinds of json. one at a time
one which updates the database with a QandA object
other which looks like
{ "number" : 3 }
this number must be extracted and 3 random QandA objects must be returned
everything except for the "number" thing works.
the try block always fails and i get exception saying i have missed all fields of QandA object
Serializer.py file is
from rest_framework import serializers
from .models import QandA
class QandASerializer(serializers.ModelSerializer):
class Meta:
model = QandA
fields = ('question', 'answer', 'option_a', 'option_b', 'option_c')

So, a couple of things.
First, your QandASerializer does not have a number field, which is the one you are trying to POST. It also has a bunch of fields required for the serializer to be valid.
You could just not use a serializer if you receive POST data with a 'number' in it.
def post(self, request):
number = request.data.get('number')
if number:
# grab those random QandA objects and return them
questions = QandA.objects.order_by('?')[:int(number)]
serializer = QandASerializer(questions, many=True)
return Response(serializer.data)
else:
serializer = QandASerializer(data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
else:
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
You may want to add additional checks to ensure that number is actually a number.

Related

Is it possible to send a request to django rest api to run a script?

I installed Django and Django Rest Api. I want to send some data to rest api. Rest api will take the data and run a script with this data and get a result. Then send this result back to me.
There won't be database usage.
Like this, request : http://testerapi.com:8000/search?q=title:xfaster564CertVal9body:A%22&fl=id
Response : {validation : true}
Is it possible?
Yes it is possible ! But i will try to respond with api function based view.
Let's suppose that our worker function to call when call the API (GET or POST) is in the utilities.py file, the models.py, serializers.py and views.py.
utilities.py
def my_worker(a, b=0, c=0):
# do something with a, b, c
return a + b + c > 10
models.py
from datetime import datetime
class User(object):
def __init__(self, email, name, created = None):
self.email = email
self.name = name
self.created = created or datetime.now()
serializers.py
I use simple Serializer but ModelSerializer is better i think
from rest_framework import serializers
class UserSerializer(serializers.Serializer):
# initialize fields
email = serializers.EmailField()
name = serializers.CharField(max_length = 200)
created = serializers.DateTimeField()
views.py
from django.http import JsonResponse
from django.views.decorators.csrf import csrf_exempt # Allow request without csrf_token set
from rest_framework.decorators import api_view
from .models import User
from .serializers import UserSerializer
# Import my_worker from .utilities
from .utilities import my_worker
#csrf_exempt
#api_view('GET') # Only get request is allowed
def user_worker(request, a, b, c):
"""
Do something with
"""
if request.method == 'GET':
# Do some stuff
users = User.objects.all()
serializer = UserSerializer(users, many=True)
# Call the utilities script here
result = my_worker(a, b, c)
if result: # a+b+c > 10
return JsonResponse({"validation": "true"}, safe=False)
else:
return JsonResponse({"validation": "false"}, safe=False)
Note that i dont use the UserSerializer but show it at example.
You can then execute a more complex function (here the my_worker).
Adapt it according to your needs.

json field in django-haystack with elasticsearch engine

How can I store json in elasticsearch by django-haystack fields?
At the moment I am using following trick:
# search_indexes.py
class BlogPostIndex(indexes.SearchIndex, indexes.Indexable):
author = indexes.CharField()
#staticmethod
def prepare_author(obj):
return json.dumps(serializers.UserMinimalSerializer(obj.author).data)
# serializers.py
class BlogPostSearchSerializer(HaystackSerializer):
author = serializers.SerializerMethodField()
#staticmethod
def get_author(obj):
return json.loads(obj.author)

ID collision with Graphene-SQLAlchemy Interface class plus Node Interface

I've written Graphene models for polymorphic entities represented in my database w/SQLalchemy.
The problem is simple:
I want to create an interface that reflects my SQLAlchemy models for Graphene but also either a) implements Node or b) does not conflict with Node and allows me to retrieve the model's ID without needing to add ... on Node {id} to the query string.
have to exclude the ID field from my ORM-based interface or field conflicts with the Node interface, by doing so in order to get the ID then you need to add ...on Node { id }, which is ugly.
I created an SQLAlchemyInterface object that extends graphene.Interface. Many (but not all) of my models used this as well as Node as interfaces. The first problem was that this contains an ID field and it conflicted with the Node interface.
I excluded the id field to not interfere with Node, but then found I could not directly query ID on my models anymore, and had to add ... on Node {id} to the query string.
I then decided to have this SQLAlchemyInterface extend Node. I don't love this approach because I need to use another (named) Node interface for all of my models that don't necessarily need to implement SQLAlchemyInterface
class SQLAlchemyInterface(Node):
#classmethod
def __init_subclass_with_meta__(
cls,
model=None,
registry=None,
only_fields=(),
exclude_fields=(),
connection_field_factory=default_connection_field_factory,
_meta=None,
**options
):
_meta = SQLAlchemyInterfaceOptions(cls)
_meta.name = f'{cls.__name__}Node'
autoexclude_columns = exclude_autogenerated_sqla_columns(model=model)
exclude_fields += autoexclude_columns
assert is_mapped_class(model), (
"You need to pass a valid SQLAlchemy Model in " '{}.Meta, received "{}".'
).format(cls.__name__, model)
if not registry:
registry = get_global_registry()
assert isinstance(registry, Registry), (
"The attribute registry in {} needs to be an instance of "
'Registry, received "{}".'
).format(cls.__name__, registry)
sqla_fields = yank_fields_from_attrs(
construct_fields(
model=model,
registry=registry,
only_fields=only_fields,
exclude_fields=exclude_fields,
connection_field_factory=connection_field_factory
),
_as=Field
)
if not _meta:
_meta = SQLAlchemyInterfaceOptions(cls)
_meta.model = model
_meta.registry = registry
connection = Connection.create_type(
"{}Connection".format(cls.__name__), node=cls)
assert issubclass(connection, Connection), (
"The connection must be a Connection. Received {}"
).format(connection.__name__)
_meta.connection = connection
if _meta.fields:
_meta.fields.update(sqla_fields)
else:
_meta.fields = sqla_fields
super(SQLAlchemyInterface, cls).__init_subclass_with_meta__(_meta=_meta, **options)
#classmethod
def Field(cls, *args, **kwargs): # noqa: N802
return NodeField(cls, *args, **kwargs)
#classmethod
def node_resolver(cls, only_type, root, info, id):
return cls.get_node_from_global_id(info, id, only_type=only_type)
#classmethod
def get_node_from_global_id(cls, info, global_id, only_type=None):
try:
node: DeclarativeMeta = one_or_none(session=info.context.get('session'), model=cls._meta.model, id=global_id)
return node
except Exception:
return None
#staticmethod
def from_global_id(global_id):
return global_id
#staticmethod
def to_global_id(type, id):
return id
Interface impls, Models + Query code examples:
class CustomNode(Node):
class Meta:
name = 'UuidNode'
#staticmethod
def to_global_id(type, id):
return '{}:{}'.format(type, id)
#staticmethod
def get_node_from_global_id(info, global_id, only_type=None):
type, id = global_id.split(':')
if only_type:
# We assure that the node type that we want to retrieve
# is the same that was indicated in the field type
assert type == only_type._meta.name, 'Received not compatible node.'
if type == 'User':
return one_or_none(session=info.context.get('session'), model=User, id=global_id)
elif type == 'Well':
return one_or_none(session=info.context.get('session'), model=Well, id=global_id)
class ControlledVocabulary(SQLAlchemyInterface):
class Meta:
name = 'ControlledVocabularyNode'
model = BaseControlledVocabulary
class TrackedEntity(SQLAlchemyInterface):
class Meta:
name = 'TrackedEntityNode'
model = TrackedEntityModel
class Request(SQLAlchemyObjectType):
"""Request node."""
class Meta:
model = RequestModel
interfaces = (TrackedEntity,)
class User(SQLAlchemyObjectType):
"""User Node"""
class Meta:
model = UserModel
interfaces = (CustomNode,)
class CvFormFieldValueType(SQLAlchemyObjectType):
class Meta:
model = CvFormFieldValueTypeModel
interfaces = (ControlledVocabulary,)
common_field_kwargs = {'id': graphene.UUID(required=False), 'label': graphene.String(required=False)}
class Query(graphene.ObjectType):
"""Query objects for GraphQL API."""
node = CustomNode.Field()
te_node = TrackedEntity.Field()
cv_node = ControlledVocabulary.Field()
# Non-Tracked Entities:
users: List[User] = SQLAlchemyConnectionField(User)
# Generic Query for any Tracked Entity:
tracked_entities: List[TrackedEntity] = FilteredConnectionField(TrackedEntity, sort=None, filter=graphene.Argument(TrackedEntityInput))
# Generic Query for any Controlled Vocabulary:
cv: ControlledVocabulary = graphene.Field(ControlledVocabulary, controlled_vocabulary_type_id=graphene.UUID(required=False),
base_entry_key=graphene.String(required=False),
**common_field_kwargs)
cvs: List[ControlledVocabulary] = FilteredConnectionField(ControlledVocabulary, sort=None, filter=graphene.Argument(CvInput))
#staticmethod
def resolve_with_filters(info: ResolveInfo, model: Type[SQLAlchemyObjectType], **kwargs):
query = model.get_query(info)
log.debug(kwargs)
for filter_name, filter_value in kwargs.items():
model_filter_column = getattr(model._meta.model, filter_name, None)
log.debug(type(filter_value))
if not model_filter_column:
continue
if isinstance(filter_value, SQLAlchemyInputObjectType):
log.debug(True)
filter_model = filter_value.sqla_model
q = FilteredConnectionField.get_query(filter_model, info, sort=None, **kwargs)
# noinspection PyArgumentList
query = query.filter(model_filter_column == q.filter_by(**filter_value))
log.info(query)
else:
query = query.filter(model_filter_column == filter_value)
return query
def resolve_tracked_entity(self, info: ResolveInfo, **kwargs):
entity: TrackedEntity = Query.resolve_with_filters(info=info, model=BaseTrackedEntity, **kwargs).one()
return entity
def resolve_tracked_entities(self, info, **kwargs):
query = Query.resolve_with_filters(info=info, model=BaseTrackedEntity, **kwargs)
tes: List[BaseTrackedEntity] = query.all()
return tes
def resolve_cv(self, info, **kwargs):
cv: List[BaseControlledVocabulary] = Query.resolve_with_filters(info=info, model=BaseControlledVocabulary, **kwargs).one()
log.info(cv)
return cv
def resolve_cvs(self, info, **kwargs):
cv: List[BaseControlledVocabulary] = Query.resolve_with_filters(info=info, model=BaseControlledVocabulary, **kwargs).all()
return cv
schema:
schema = Schema(query=Query, types=[*tracked_members, *cv_members])
I would like to be able to not extend Node with SQLAlchemyInterface and rather add Node back to the list of interfaces for TrackedEntity and ControlledVocabulary but be able to perform a query like this:
query queryTracked {
trackedEntities{
id
(other fields)
... on Request {
(request specific fields)
}
}

django postgresql json field schema validation

I have a django model with a JSONField (django.contrib.postgres.fields.JSONField)
Is there any way that I can validate model data against a json schema file?
(pre-save)
Something like my_field = JSONField(schema_file=my_schema_file)
I wrote a custom validator using jsonschema in order to do this.
project/validators.py
import django
from django.core.validators import BaseValidator
import jsonschema
class JSONSchemaValidator(BaseValidator):
def compare(self, value, schema):
try:
jsonschema.validate(value, schema)
except jsonschema.exceptions.ValidationError:
raise django.core.exceptions.ValidationError(
'%(value)s failed JSON schema check', params={'value': value}
)
project/app/models.py
from django.db import models
from project.validators import JSONSchemaValidator
MY_JSON_FIELD_SCHEMA = {
'schema': 'http://json-schema.org/draft-07/schema#',
'type': 'object',
'properties': {
'my_key': {
'type': 'string'
}
},
'required': ['my_key']
}
class MyModel(models.Model):
my_json_field = models.JSONField(
default=dict,
validators=[JSONSchemaValidator(limit_value=MY_JSON_FIELD_SCHEMA)]
)
That's what the Model.clean() method is for (see docs). Example:
class MyData(models.Model):
some_json = JSONField()
...
def clean(self):
if not is_my_schema(self.some_json):
raise ValidationError('Invalid schema.')
you could use cerberus to validate your data against a schema
from cerberus import Validator
schema = {'name': {'type': 'string'}}
v = Validator(schema)
data = {'name': 'john doe'}
v.validate(data) # returns "True" (if passed)
v.errors # this would return the error dict (or on empty dict in case of no errors)
it's pretty straightforward to use (also due to it's good documentation -> validation rules: http://docs.python-cerberus.org/en/stable/validation-rules.html)
I wrote a custom JSONField that extends models.JSONField and validates attribute's value by using jsonschema (Django 3.1, Python 3.7).
I didn't use the validators parameter for one reason: I want to let users define the schema dynamically.So I use a schema parameter, that should be:
None (by default): the field will behave like its parent class (no JSON schema validation support).
A dict object. This option is suitable for a small schema definition (for example: {"type": "string"});
A str object that describes a path to the file where the schema code is contained. This option is suitable for a big schema definition (to preserve the beauty of the model class definition code). For searching I use all enabled finders: django.contrib.staticfiles.finders.find().
A function that takes a model instance as an argument and returns a schema as dict object. So you can build a schema based on the state of the given model instance. The function will be called every time when the validate() is called.
myapp/models/fields.py
import json
from jsonschema import validators as json_validators
from jsonschema import exceptions as json_exceptions
from django.contrib.staticfiles import finders
from django.core import checks, exceptions
from django.db import models
from django.utils.functional import cached_property
class SchemaMode:
STATIC = 'static'
DYNAMIC = 'dynamic'
class JSONField(models.JSONField):
"""
A models.JSONField subclass that supports the JSON schema validation.
"""
def __init__(self, *args, schema=None, **kwargs):
if schema is not None:
if not(isinstance(schema, (bool, dict, str)) or callable(schema)):
raise ValueError('The "schema" parameter must be bool, dict, str, or callable object.')
self.validate = self._validate
else:
self.__dict__['schema_mode'] = False
self.schema = schema
super().__init__(*args, **kwargs)
def check(self, **kwargs):
errors = super().check(**kwargs)
if self.schema_mode == SchemaMode.STATIC:
errors.extend(self._check_static_schema(**kwargs))
return errors
def _check_static_schema(self, **kwargs):
try:
schema = self.get_schema()
except (TypeError, OSError):
return [
checks.Error(
f"The file '{self.schema}' cannot be found.",
hint="Make sure that 'STATICFILES_DIRS' and 'STATICFILES_FINDERS' settings "
"are configured correctly.",
obj=self,
id='myapp.E001',
)
]
except json.JSONDecodeError:
return [
checks.Error(
f"The file '{self.schema}' contains an invalid JSON data.",
obj=self,
id='myapp.E002'
)
]
validator_cls = json_validators.validator_for(schema)
try:
validator_cls.check_schema(schema)
except json_exceptions.SchemaError:
return [
checks.Error(
f"{schema} must be a valid JSON Schema.",
obj=self,
id='myapp.E003'
)
]
else:
return []
def deconstruct(self):
name, path, args, kwargs = super().deconstruct()
if self.schema is not None:
kwargs['schema'] = self.schema
return name, path, args, kwargs
#cached_property
def schema_mode(self):
if callable(self.schema):
return SchemaMode.DYNAMIC
return SchemaMode.STATIC
#cached_property
def _get_schema(self):
if callable(self.schema):
return self.schema
elif isinstance(self.schema, str):
with open(finders.find(self.schema)) as fp:
schema = json.load(fp)
else:
schema = self.schema
return lambda obj: schema
def get_schema(self, obj=None):
"""
Return schema data for this field.
"""
return self._get_schema(obj)
def _validate(self, value, model_instance):
super(models.JSONField, self).validate(value, model_instance)
schema = self.get_schema(model_instance)
try:
json_validators.validate(value, schema)
except json_exceptions.ValidationError as e:
raise exceptions.ValidationError(e.message, code='invalid')
Usage:
myapp/models/__init__.py
def schema(instance):
schema = {}
# Here is your code that uses the other
# instance's fields to create a schema.
return schema
class JSONSchemaModel(models.Model):
dynamic = JSONField(schema=schema, default=dict)
from_dict = JSONField(schema={'type': 'object'}, default=dict)
# A static file: myapp/static/myapp/schema.json
from_file = JSONField(schema='myapp/schema.json', default=dict)
Another solution using jsonschema for simple cases.
class JSONValidatedField(models.JSONField):
def __init__(self, *args, **kwargs):
self.props = kwargs.pop('props')
self.required_props = kwargs.pop('required_props', [])
super().__init__(*args, **kwargs)
def validate(self, value, model_instance):
try:
jsonschema.validate(
value, {
'schema': 'http://json-schema.org/draft-07/schema#',
'type': 'object',
'properties': self.props,
'required': self.required_props
}
)
except jsonschema.exceptions.ValidationError:
raise ValidationError(
f'Value "{value}" failed schema validation.')
class SomeModel(models.Model):
my_json_field = JSONValidatedField(
props={
'foo': {'type': 'string'},
'bar': {'type': 'integer'}
},
required_props=['foo'])

Creating UTF-8 JsonResponse in Django

Is there any simple way to override DjangoJSONEncoder.ensure_ascii and set it to False or output non-ascii text in django.http.JsonResponse in any other way?
As of Django 1.9 you can configure JSONResponse to disable the ensure_ascii switch, by passing in a value for the json_dumps_params argument:
return JsonResponse(response_data, safe=False, json_dumps_params={'ensure_ascii': False})
With ensure_ascii=False, json.dumps() outputs UTF-8 data for non-ASCII codepoints.
You could subclass JsonResponse to make it the default unless set differently:
from django.http.response import JsonResponse
class UTF8JsonResponse(JsonResponse):
def __init__(self, *args, json_dumps_params=None, **kwargs):
json_dumps_params = {"ensure_ascii": False, **(json_dumps_params or {})}
super().__init__(*args, json_dumps_params=json_dumps_params, **kwargs)
then use that throughout instead of JsonResponse.
At the extreme end, you could monkey-patch the class to set ensure_ascii to False by default; put the following in a suitable module of your Django app (say, in a file named patches.py):
import logging
from functools import wraps
from django.http.response import JsonResponse
logger = logging.getLogger(__name__)
def patch_jsonresponse_disable_ensure_ascii():
if getattr(JsonResponse, '_utf8_patched', False):
# Already patched. Add warning in logs with stack to see what location
# is trying to patch this a second time.
logger.warning("JSONResponse UTF8 patch already applied", stack_info=True)
return
logger.debug("Patching JSONResponse to disable ensure_ascii")
orig_init = JsonResponse.__init__
#wraps(orig_init)
def utf8_init(self, *args, json_dumps_params=None, **kwargs):
json_dumps_params = {"ensure_ascii": False, **(json_dumps_params or {})}
orig_init(self, *args, json_dumps_params=json_dumps_params, **kwargs)
JsonResponse.__init__ = utf8_init
JsonResponse._utf8_patched = True # to prevent accidental re-patching
then import patch_jsonresponse_disable_ensure_ascii into your Django settings file and call it based on your desired config:
from yourapp.patches import patch_jsonresponse_disable_ensure_ascii
JSON_RESPONSES_UTF8 = True
if JSON_RESPONSES_UTF8:
patch_jsonresponse_disable_ensure_ascii()
EDIT:
Or if you tend to the utf-8 format, use instead of Django's JsonResponse():
return HttpResponse(json.dumps(response_data, ensure_ascii=False),
content_type="application/json")
or
return JsonResponse(json.dumps(response_data, ensure_ascii=False), safe=False)
more about the safe=False HERE
OLD:
You don't have to whatever alter.
Although Django creates JSON data in ASCII (from UTF-8), Javascript will automatically decode it back to UTF-8.
from django.core.serializers.json import DjangoJSONEncoder
from django.http import JsonResponse
class MyJsonResponse(JsonResponse):
def __init__(self, data, encoder=DjangoJSONEncoder, safe=True, **kwargs):
json_dumps_params = dict(ensure_ascii=False)
super().__init__(data, encoder, safe, json_dumps_params, **kwargs)
I didn't find any better way yet than to utilize an already installed REST Framework:
from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import IsAuthenticatedOrReadOnly
from rest_framework.response import Response
from .models import INITIATOR_TYPES
#api_view(['GET'])
#permission_classes((IsAuthenticatedOrReadOnly, ))
def initiator_types(request):
data = {t[0]: str(t[1]) for t in INITIATOR_TYPES}
return Response(data)
But I don't really like it. It's much more complicated than JsonResponse: https://stackoverflow.com/a/24411716/854477