I'd like to have a Django model with a reserved field, so that no one can set it directly but its value it's generated at saving time. This is useful for example to generate user tokens and I want to prevent developers to directly set a value for the token key. At the same time I would like to be able to treat that field as I do with others, so using __ for fields lookup in queries, or be able to retrieve tokens as:
token = Token.objects.get(key='c331054c00494f6a22f0ebde7a32bf9d4619b988')
So in my mind doing something like:
Token.key = 'my-token-key'
should fail, and even instantiation should fail:
token = Token(key='my-token-key')
So far I came up with this solution, but I'm a bit concerned my changes could break some Django workflow since I'm not sure what my changes will affect:
import binascii
import datetime
import os
from django.contrib.auth import get_user_model
from django.db import models
class Token(models.Model):
"""
An access token that is associated with a user.
"""
id = models.AutoField(primary_key=True)
# By default `get_attname` returns the field `name`,
# but in my case the attribute name is different
_key = models.CharField(max_length=40, unique=True, name='key', db_column='key')
_key.get_attname = lambda: '_key'
name = models.CharField(max_length=255)
user = models.ForeignKey(get_user_model(), related_name='tokens')
created = models.DateTimeField(auto_now_add=True)
last_access_time = models.DateTimeField(null=True, blank=True)
expires = models.DateField(
null=True,
blank=True,
help_text="Leave empty for non-expiring tokens. "
"Once the token has expired you can not extend its validity.",
)
#property
def key(self):
return self._key
#key.setter
def key(self, value):
raise ValueError("Can not set key directly. It is automatically generated when saving the model.")
def save(self, *args, **kwargs):
if not self._key:
self._key = self._generate_key()
super(Token, self).save(*args, **kwargs)
#staticmethod
def _generate_key():
return binascii.hexlify(os.urandom(20)).decode()
#property
def expired(self):
return bool(self.expires and self.expires < datetime.date.today())
def __str__(self):
return '{} - {}'.format(self.user, self.name)
class Meta:
verbose_name = "User Token"
verbose_name_plural = "User Tokens"
unique_together = (('name', 'user'),)
As you can see I tried overriding the get_attname method of the key field (needed because the field name and the property are the same and it would lead to errors loading forms). This seems to work just fine, but I would like to know if this could lead to problems running queries.
Maybe there is a simpler way to do this but I couldn't find anything better.
P.S.: I'm using python2 with Django 1.11
Thanks a lot to everyone!
Related
I've been scratching my head about this problem for a couple of hours now. Basically, I have two models: User and Project:
class User(AbstractUser):
username = None
email = models.EmailField("Email Address", unique=True)
avatar = models.ImageField(upload_to="avatars", default="avatars/no_avatar.png")
first_name = models.CharField("First name", max_length=50)
last_name = models.CharField("Last name", max_length=50)
objects = UserManager()
USERNAME_FIELD = "email"
class Project(models.Model):
name = models.CharField("Name", max_length=8, unique=True)
status = models.CharField(
"Status",
max_length=1,
choices=[("O", "Open"), ("C", "Closed")],
default="O",
)
description = models.CharField("Description", max_length=3000, default="")
owner = models.ForeignKey(
User, on_delete=models.SET_NULL, null=True, related_name="project_owner"
)
participants = models.ManyToManyField(User, related_name="project_participants", blank=True)
created_at = models.DateTimeField(auto_now_add=True)
I use standard ModelViewSets for both of them, nothing changed. Then there's my Project serializer:
class ProjectSerializer(serializers.ModelSerializer):
class Meta:
model = Project
fields = "__all__"
status = serializers.CharField(source="get_status_display", required=False)
owner = UserSerializer()
participants = UserSerializer(many=True)
I use UserSerializers here, because having them achieved first of my two goals:
I wanted to get the user data when getting the project from the API -> owner is a serialized User with all the fields, same for participants, but it's a list of users
I want to be able to partially update the Project, for example add a participant
So I searched through the docs and SO and I always found answers that answer one of those questions, but never both of them.
The thing with my second goal is: when I do the partial update (via PATCH, of course), I get the response that: "Invalid data. Expected a dictionary, but got int." when I pass a list of ints (user ids) for the participants. I thought: okay, maybe I have to pass the whole user data to change it. But then I realised: when I remove the UserSerializer from ProjectSerializer - passing just the list of ints in Postman works just fine. And that is a life saver, cuz who wants to create a request with a whole bunch of data, when I can just pass user ids.
But then of course when I remove the UserSerializer, when I call get project, I get participants: [1,2,3,4,...], not participants: [{"id": 1, "name": "John", ...}, ...}]. And I really want this behavior, because I don't want to make additional API calls just to get the users' data by their IDs.
So summing up my question is: Is there a way to leave those serializers in place but still be able to partially update my model without having to pass whole serialized data to the API (dicts instead of IDs)? Frankly, I don't care about the serializers, so maybe the question is this: Can I somehow make it possible to partially update my Products' related fields like owner or participants just by passing the related entities IDs while still maintaining an ability to get my projects with those fields expanded (serialized entities - dicts, instead of just IDs)?
#Edit:
My view:
from rest_framework import viewsets, permissions
from projects.models import Project
from projects.api.serializers import ProjectSerializer
class ProjectViewSet(viewsets.ModelViewSet):
queryset = Project.objects.all()
serializer_class = ProjectSerializer
permission_classes = [permissions.IsAuthenticated]
lookup_field = "name"
def get_queryset(self):
if self.request.user.is_superuser:
return Project.objects.all()
else:
return Project.objects.filter(owner=self.request.user.id)
def perform_create(self, serializer):
serializer.save(owner=self.request.user, participants=[self.request.user])
Answer:
To anyone reading this, I've solved this problem and I actually created a base class for all my viewsets that I want this behavior to be in:
from rest_framework.response import Response
class ReadWriteViewset:
write_serializer_class = None
read_serializer_class = None
def update(self, request, *args, **kwargs):
partial = kwargs.pop("partial", False)
instance = self.get_object()
write_serializer = self.write_serializer_class(
instance=instance,
data=request.data,
partial=partial,
)
write_serializer.is_valid(raise_exception=True)
self.perform_update(write_serializer)
read_serializer = self.read_serializer_class(instance)
if getattr(instance, "_prefetched_objects_cache", None):
# If 'prefetch_related' has been applied to a queryset, we need to
# forcibly invalidate the prefetch cache on the instance.
instance._prefetched_objects_cache = {}
return Response(read_serializer.data)
Then you use it kinda like in here
I'm assuming that you are using a ModelViewSet. You could use different serializers for different methods.
class ProjectViewSet(viewsets.ModelViewSet):
def get_serializer_class(self):
if self.action in ['create', 'update']:
return WriteProjectSerializer # your serializer not using `UserSerializer` that works for updating
return ProjectSerializer # your default serializer with all data
Edit for using different serializers in same method:
# you can override `update` and use a different serializer in the response. The rest of the code is basically the default behavior
def update(self, request, *args, **kwargs):
partial = kwargs.pop('partial', False)
instance = self.get_object()
write_serializer = WriteProjectSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
instance = self.perform_update(serializer)
read_serializer = ProjectSerializer(instance)
if getattr(instance, '_prefetched_objects_cache', None):
# If 'prefetch_related' has been applied to a queryset, we need to
# forcibly invalidate the prefetch cache on the instance.
instance._prefetched_objects_cache = {}
return Response(read_serializer.data)
A good way to see the default code for all these methods is using Classy DRF. You can see all methods that come with using ModelViewSet and use that code with some changes. Here I'm using the default code for update but changing for a new serializer for the response.
I want to update my model upon login (to check the authorizations of a person from an external system).
The code of my model looks as follow:
import json
from django.contrib.auth.models import AbstractUser
from django.contrib.auth.signals import user_logged_in
from django.db import models
class Person(AbstractUser):
is_dean = models.BooleanField(null=False, blank=False, default=False)
is_institute_manager = models.BooleanField(null=False, blank=False, default=False)
managed_institutes = models.TextField(blank=True, null=True, default=None)
def get_managed_institutes(self):
return json.loads(self.managed_institutes)
def set_managed_institutes(self, value):
self.managed_institutes = json.dumps(value)
# Signals processing
def check_authorizations(sender, user, request, **kwargs):
...
# check if the user is dean
is_dean = False
# logic to check if the user is dean...
user.is_dean = is_dean
# Check if the user manages institutes
is_institute_manager = False
managed_institutes = list()
# Logic to check if the user is managing institutes ...
user.is_institute_manager = is_institute_manager
user.set_managed_institutes = managed_institutes
user.save()
user_logged_in.connect(check_authorizations)
Surprisingly, the boolean flags get set correctly, but the method set_managed_institute never gets called...
I am quite convinced this a trivial mistake from my end, but I can't figure it out.
That is not how you call methods in Python. You need to do so explicitly:
user.set_managed_institutes(managed_institutes)
Or did you mean to define a property?
#property
def managed_institutes(self):
return json.loads(self._managed_institutes)
#managed_institutes.setter
def managed_institutes(self, value):
self._managed_institutes = json.dumps(value)
But also note, you probably want to use a JsonField anyway. If you're using PostgreSQL, there is one defined in Django directly; otherwise there are several third-party libraries that take care of serializing and deserializing your data on load/save.
I am writing an API in Django Rest Framework- when using the POST method to create a new object, if the 'done' field is True and the 'done_date' is Null, I would like the 'done_date' to automatically be set to current time. Here's what I tried to do, overriding the save method in my models:
In models.py
from django.db import models
from django.utils import timezone
class Task(models.Model):
title = models.CharField(max_length=100)
done = models.BooleanField(default=False)
author_ip = models.GenericIPAddressField()
created_date = models.DateTimeField(default=timezone.now)
done_date = models.DateTimeField(null=True, blank=True)
class Meta:
ordering = ('id',)
def __str__(self):
return '{} - {}'.format(self.pk, self.title)
def save(self, *args, **kwargs):
if self.done and not self.done_date:
self.done_date = timezone.now
super(Task, self).save(*args, **kwargs)
However, this throws a "TypeError when calling Task.objects.create(). This may be because you have a writable field on the serializer class that is not a valid argument to Task.objects.create(). You may need to make the field read-only, or override the TaskSerializer.create() method to handle this correctly."
Now, I am fairly certain that it's related to timezone.now in the save method, could someone advise me on how to proceed? I apologise if it is a basic question, thanks!
All you have to do is to call the timezone.now() function (with the parenthesis)
alternatively, you could use: auto_now=True and set editable=True
I am trying to post to my API with foreign key relationships. It's throwing me back an error saying it's expecting a dictionary as opposed to int for character, character_opponent and stage. This is because the way my models are set up. They have foreign key relationships. The model in question looks like this:
import uuid
from django.db import models
from django.utils import timezone
from analysis.models import Analysis
from characters.models import Character
from stages.models import Stage
class Match(models.Model):
analysis = models.ForeignKey(Analysis, on_delete=models.CASCADE)
character = models.ForeignKey(Character, on_delete=models.CASCADE, related_name='character')
character_won = models.BooleanField()
character_opponent = models.ForeignKey(Character, on_delete=models.CASCADE, related_name='character_opponent')
character_opponent_won = models.BooleanField()
created_at = models.DateTimeField(editable=False)
id = models.UUIDField(primary_key=True, default=uuid.uuid4)
updated_at = models.DateTimeField(editable=False)
stage = models.ForeignKey(Stage, on_delete=models.CASCADE)
def __str__(self):
return '%s vs. %s on %s' % (self.character, self.character_opponent, self.stage)
def save(self, *args, **kwargs):
''' On save, update timestamps '''
if not self.created_at:
self.created_at = timezone.now()
self.updated_at = timezone.now()
return super(Match, self).save(*args, **kwargs)
class Meta:
db_table = "matches"
And here is my serializer:
from rest_framework import serializers
from matches.models import Match
from characters.serializers import CharacterSerializer
from stages.serializers import StageSerializer
class MatchSerializer(serializers.ModelSerializer):
character = CharacterSerializer()
character_opponent = CharacterSerializer()
stage = StageSerializer()
class Meta:
model = Match
fields = ('id', 'analysis', 'character', 'character_won', 'character_opponent', 'character_opponent_won', 'stage')
Is there some option I am missing here to be able to post properly? Clearly I shouldn't have to pass the entire character object each time I want to post something, right? I should just be able to pass the primary key.
From your few comments I understood that you need nested serializer in GET method. What I suggest is, use two[or more] serializers for your API class.
Assuming you are using ModelViewSet API class is using,then you could override get_serializer_class() method as below,
from rest_framework.viewsets import ModelViewSet
class MatchAPI(ModelViewSet):
queryset = Match.objects.all()
def get_serializer_class(self):
if self.action == 'create':
return MatchCreateSerializer
return MatchSerializer
And your MatchCreateSerializer will be like this,
class MatchCreateSerializer(serializers.ModelSerializer):
class Meta:
fields = '__all__'
model = Match
Thus, you only need to provide the PKs of analysis,character etc while creation of Match instance
It will come down to your CharacterSerializer and StageSerializer. If you want to input 1 format (using serialisers.PrimaryKeyRelatedField()), but output another (CharacterSerializer, StageSerializer), you might be best served using 2 serialisers and switching in your view.
In your view you can override get_serializer_class and check your request method, or in the case of a viewset you can check the method being invoked.
When you declare a serializer related field using another serializer, like this
character = CharacterSerializer()
you are telling django-rest-framework that you want a nested serializer. What you want is something like this instead
character = serializers.PrimaryKeyRelatedField()
or you can actually just leave the explicit field declaration out of the serializer (since this is the default), see the doc on serializer relations.
What's the difference between pinax.apps.accounts and the idios profiles app that were installed with the profiles base project?
As I understand it, the contrib.auth should be just for authentication purpose (i.e. username and password), and the existence of User.names and User.email in the auth model is historical and those fields shouldn't be used; but the distinction between accounts and profiles are lost to me. Why is there pinax.apps.account and idios?
The Pinax account is just a wrapper for that holds the user, timezone and language. user is a foreign key relation to the standard django.auth User model.
class Account(models.Model):
user = models.ForeignKey(User, unique=True, verbose_name=_('user'))
timezone = TimeZoneField(_('timezone'))
language = models.CharField(_('language'), max_length=10, choices=settings.LANGUAGES, default=settings.LANGUAGE_CODE)
def __unicode__(self):
return self.user.username
The idios Profile model basically does the same thing but has some custom methods:
class ProfileBase(models.Model):
# ### could be unique=True if subclasses don't inherit a concrete base class
# ### need to look at this more
user = models.ForeignKey(User, verbose_name=_("user"))
class Meta:
verbose_name = _("profile")
verbose_name_plural = _("profiles")
abstract = True
def __unicode__(self):
return self.user.username
def get_absolute_url(self):
if idios.settings.MULTIPLE_PROFILES:
# ### using PK here is kind of ugly. the alternative is to
# generate a unique slug for each profile, which is tricky
kwargs = {
"profile_slug": self.profile_slug,
"pk": self.pk
}
else:
if idios.settings.USE_USERNAME:
kwargs = {"username": self.user.username}
else:
kwargs = {"pk": self.pk}
return reverse("profile_detail", kwargs=kwargs)
#classmethod
def get_form(cls):
return get_profile_form(cls)
def _default_profile_slug(cls):
return cls._meta.module_name
profile_slug = ClassProperty(classmethod(_default_profile_slug))
Neither of them replicates the authentication functionality of django.auth.User if that is what you are asking. It doesn't look like either one has a dependency on the other either. So if you can't see a good use for both of them, just go with the one that makes sense.
Profiles are meant to be used for public data, or data you'd share with other people and is also more descriptive in nature.
Account data are more like settings for you account that drive certain behavior (language or timezone settings) that are private to you and that control how various aspects of the site (or other apps) function.