Validating polymorphic data in JSONField using json-schemas - django

Hello Stackoverflowers!
I've searched for a solution for this for a while, but have not been able to find anything addressing this usecase specifically.
Say we have the following models:
class Machine(models.model):
machine_name = models.CharField(max_length=30)
machine_data_template = models.JSONField()
class Event(models.model):
machine_name = models.ForeignKey(Machine, on_delete=models.CASCADE
machine_data = models.JSONField()
We have a machine for which there can be multiple events, the details of which are stored as JSON in a JSONField. The JSONField in event should be validated using the json-schema defined in Machine.machine_data_template.
I've looked at various Python/Django implementations of json-schemas, however I have not been able to find a way where we can define custom validation on a per-object basis. For example, solutions such as the one presented by Corwin Cole here defines the validation logic directly in the model, which would mean each instance of event would be subject to the same validation logic regardless of the machine:
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 = JSONField(
default=dict,
validators=[JSONSchemaValidator(limit_value=MY_JSON_FIELD_SCHEMA)]
)
Does anyone have any suggestion how I can achieve the desired "fully polymorphic" behavior?
BR - K

You can override the Model.clean() method and add custom validation there. You have access to the Event and it's related Machine so can access the schema. Add an example using jsonschema similar to the linked answer
import jsonschema
class Machine(models.Model):
machine_name = models.CharField(max_length=30)
machine_data_template = models.JSONField()
class Event(models.Model):
machine_name = models.ForeignKey(Machine, on_delete=models.CASCADE)
machine_data = models.JSONField()
def clean(self):
try:
jsonschema.validate(self.machine_data, self.machine_name.machine_data_template)
except jsonschema.exceptions.ValidationError as e:
raise ValidationError({'machine_data': str(e)})

Related

Cannot use ArrayField from Djongo Models

I'm trying to make an app with Django using MongoDB as my database engine, so I'm using Djongo as ORM.
In my models, I have defined a Client class, which is intended to contain an array of Profiles (authorized people to login in name of client).
In the Djongo Documentation, it says one can use the ArrayField class in models in order to store an array to the database. The point is that I followed the example in the documentation (even I tried to copy without changing anything) and it isn't working. When running the view, I always get this error:
"Value: Profile object (None) must be an instance of <class 'list'>"
I have the following models.py:
from django import forms
from djongo import models
class Profile(models.Model):
_id=models.ObjectIdField()
fname = models.CharField(max_length=25)
lname = models.CharField(max_length=50)
email = models.EmailField()
password = models.CharField(max_length=16)
class Meta:
abstract=True
class ProfileForm(forms.ModelForm):
class Meta:
model = Profile
fields =(
'fname', 'lname', 'email', 'password',
)
class Client(models.Model): #informacion del cliente
_id = models.ObjectIdField()
name = models.CharField(max_length=256)
authorized = models.ArrayField(
model_container=Profile,
model_form_class=ProfileForm
)
objects = models.DjongoManager()
class ClientForm(forms.ModelForm):
class Meta:
model = Client
fields = (
'name', 'authorized'
)
I have a form that uses ClientForm, and the view renders properly. However, when submitting the form I get the error I said at the beginning. I'm searched the whole internet and didn't get any idea of what is causing this problem.
I have bumped into a similar error a couple of days ago, I was trying to store an object (in your case it's Profile) directly to the ArrayField, I was trying to do something like:
client = Client.object.first()
client.authorized = {
'fname': form.validated_data['first name'],
'last': form.validated_data['last name'],
'email': form.validated_data['email'],
'password': form.validated_data['Password'],
}
client.save()
the right way to do it is:
client = Client.object.first()
client.authorized = [{
'fname': form.validated_data['first name'],
'last': form.validated_data['last name'],
'email': form.validated_data['email'],
'password': form.validated_data['Password'],
}]
client.save()
you'll need to store it as a list object by wrapping it around []
that's how I was able to solve it in my case, I think yours is similar, try the above solution, and if it doesn't work, maybe you should share more details like your views.py

django rest framework dynamic related object

Problem - I have a REST server using django-rest-framework (django v1.7.7, django-rest-framework v3.1.1). In the notifications, I let a user know if they've received a new friend request, or have earned a new badge. There are other notification types as well, but this simple example can explain my problem.
In my GET response, I want to get the notification with a dynamic related object, which is determined by type. if the type is friendreq then I want the relatedObject to be a User instance, with a UserSerializer. If the type is badge, I want to have the relatedObject be a Badge instance, with a BadgeSerializer.
Note: I already have these other serializers (UserSerializer, BadgeSerializer).
Below is what I am wanting to achieve in a response:
{
"id": 1,
"title": "Some Title",
"type": "friendreq"
"relatedObject": {
// this is the User instance. For badge it would be a Badge instance
"id": 1,
"username": "foo",
"email": "foo#bar.com",
}
}
And here are what I have for models and serializer:
# models.py
class Notification(models.Model):
"""
Notifications are sent to users to let them know about something. The
notifications will be about earning a badge, receiving friend request,
or a special message from the site admins.
"""
TYPE_CHOICES = (
('badge', 'badge'),
('friendreq', 'friend request'),
('system', 'system'),
)
title = models.CharField(max_length=30)
type = models.CharField(max_length=10, choices=TYPE_CHOICES)
user = models.ForeignKey(settings.AUTH_USER_MODEL, related_name="user")
related_id = models.PositiveIntegerField(null=True, blank=True)
# serializers.py
class NotificationSerializer(serializers.ModelSerializer):
if self.type == "badge":
related_object = BadgeSerializer(
read_only=True,
queryset=Badge.objects.get(id=self.related_id)
)
elif self.type == "friendreq":
related_object = FriendRequestSerializer(
read_only=True,
queryset=FriendRequest.objects.get(id=self.related_id)
)
class Meta:
model = Notification
This code does not work but hopefully it explains what I'm trying to accomplish and the direction I'm trying to go. Maybe that direction is completely wrong and I should be trying to accomplish this by using some other method.
Another option I tried would be to use a SerializerMethodField and perform this in a method, but that seemed not as clean for this case of trying to return a Serialized object based upon another field.
I believe what you're going to want to use is the .to_representation() approach mentioned here in the DRF documentation: http://www.django-rest-framework.org/api-guide/relations/#generic-relationships

How can I relate two models (django tutorial's Poll and Choice) in a Tastypie API

I'm trying relate two resources (models) in an API using Tastypie but I'm getting an error.
I've followed the django tutorial and used:
models.py
from django.db import models
class Poll(models.Model):
question = models.CharField(max_length=200)
pub_date = models.DateTimeField('date published')
class Choice(models.Model):
poll = models.ForeignKey(Poll)
choice_text = models.CharField(max_length=200)
votes = models.IntegerField(default=0)
I tried to create a link between the Poll and Choice based on this stackoverflow answer and wrote the following code:
api.py
class ChoiceResource(ModelResource):
poll = fields.ToOneField('contact.api.PollResource', attribute='poll', related_name='choice')
class Meta:
queryset = Choice.objects.all()
resource_name = 'choice'
class PollResource(ModelResource):
choice = fields.ToOneField(ChoiceResource, 'choice', related_name='poll', full=True)
class Meta:
queryset = Poll.objects.all()
resource_name = 'poll'
When I go to: 127.0.0.1:8088/contact/api/v1/choice/?format=json
Everything works as it should. For example one of my choices links to the right poll:
{
"choice_text": "Nothing",
"id": 1,
"poll": "/contact/api/v1/poll/1/",
"resource_uri": "/contact/api/v1/choice/1/",
"votes": 6
}
When I go to: 127.0.0.1:8088/contact/api/v1/poll/?format=json
I get:
{
"error": "The model '<Poll: What's up?>' has an empty attribute 'choice' and doesn't allow a null value."
}
Do I need to use the fields.ToManyField instead or do I need to change my original model?
Tastypie recommends against creating reverse relationships (what you're trying to do here the relationship is Choice -> Poll and you want Poll -> Choice), but if you still wanted to, you can.
Excerpt from the Tastypie docs:
Unlike Django’s ORM, Tastypie does not automatically create reverse
relations. This is because there is substantial technical complexity
involved, as well as perhaps unintentionally exposing related data in
an incorrect way to the end user of the API.
However, it is still possible to create reverse relations. Instead of
handing the ToOneField or ToManyField a class, pass them a string that
represents the full path to the desired class. Implementing a reverse
relationship looks like so:
# myapp/api/resources.py
from tastypie import fields
from tastypie.resources import ModelResource
from myapp.models import Note, Comment
class NoteResource(ModelResource):
comments = fields.ToManyField('myapp.api.resources.CommentResource', 'comments')
class Meta:
queryset = Note.objects.all()
class CommentResource(ModelResource):
note = fields.ToOneField(NoteResource, 'notes')
class Meta:
queryset = Comment.objects.all()

Using computed fields in admin

I'm trying to use a runtime-computed field in my admin page. This works fine, but I'd like to allow sorting based for that field. Using Django 1.5 (dev), is this possible? I've been scouring the interweb but can't find anything indicating that it is possible.
class Guest(models.Model)
email = models.CharField(max_length=255)
class Invitation(models.Model)
guest = models.ForeignKey(Guest)
created_on = models.DateTimeField(auto_now_add=True)
class GuestAdmin(admin.ModelAdmin):
list_display = ["email", "latest_invitation_sent_on",]
def latest_invitation_sent_on(self, o):
try:
return o.invitation_set.all().order_by(
"-created_on")[0].created_on.strftime("%B %d, %Y")
except IndexError:
return "N/A"
I'd like to be able to enable sorting by latest_invitation_sent_on. Are there any methods of doing this nicely that I'm unaware of?
You should be able to annotate Guests with their latest invitation time and then order_by it (order_by uses the DB to sort and as long as you can provide a valid DB field, table or virtual it should work).
class GuestManager(models.Manager):
def get_query_set(self):
return super(GuestManager, self).get_query_set().annotate(latest_invite=Max("invitation_set__created_on"))
class Guest(models.Model)
email = models.CharField(max_length=255)
objects = GuestManager()
class Invitation(models.Model)
guest = models.ForeignKey(Guest)
created_on = models.DateTimeField(auto_now_add=True)
class GuestAdmin(admin.ModelAdmin):
list_display = ["email", "latest_invite",]
If you only need latest_invite annotation once in a while it makes sense to move it to a separate method or even manager.
class GuestManager(models.Manager):
def by_invitations(self):
return super(GuestManager, self).get_query_set().annotate(latest_invite=Max("invitation_set__created_on")).order_by('-latest_invite')
>>> Guest.objects.by_invitations()

Django json serialization problem

I am having difficulty serializing a django object. The problem is that there are foreign keys. I want the serialization to have data from the referenced object, not just the index.
For example, I would like the sponsor data field to say "sponsor.last_name, sponsor.first_name" rather than "13".
How can I fix my serialization?
json data:
{"totalCount":"2","activities":[{"pk": 1, "model": "app.activity", "fields": {"activity_date": "2010-12-20", "description": "my activity", "sponsor": 13, "location": 1, ....
model code:
class Activity(models.Model):
activity_date = models.DateField()
description = models.CharField(max_length=200)
sponsor = models.ForeignKey(Sponsor)
location = models.ForeignKey(Location)
class Sponsor(models.Model):
last_name = models.CharField(max_length=20)
first_name= models.CharField(max_length=20)
specialty = models.CharField(max_length=100)
class Location(models.Model):
location_num = models.IntegerField(primary_key=True)
location_name = models.CharField(max_length=100)
def activityJSON(request):
activities = Activity.objects.all()
total = activities.count()
activities_json = serializers.serialize("json", activities)
data = "{\"totalCount\":\"%s\",\"activities\":%s}" % (total, activities_json)
return HttpResponse(data, mimetype="application/json")
Add relations to the serializer like this:
activities_json = serializers.serialize("json", activities, relations=('sponsor',))
Then all you need is:
return HttpResponse(activities_json, mimetype="application/json")
Then make sure you also have the django library wadofstuff installed.
Hope this helps!
The docs seem to explain exactly how to do this. Read the part about serialization of natural keys.
This small lib is very handy with django : http://code.google.com/p/wadofstuff/wiki/DjangoFullSerializers
It allows more customisation than the standard encoder.
any2any also contains serializers allowing to customize completely the output format :
check the docs
repo on bitbucket
or on pypi