I have a Django class
class Chat(models.Model):
primary_node = models.ForeignKey('nodes.Node', blank=True, null=True, related_name='chats_at_this_pri_node', on_delete=models.SET_NULL)
secondary_node = models.ForeignKey('nodes.Node', blank=True, null=True, related_name='chats_at_this_sec_node', on_delete=models.SET_NULL)
I want to forbid direct assigning of fields, such as
chat.primary_node = some_node
and instead create a method chat.assign_node(primary, secondary) that updates nodes via Django Chat.update() model method.
The reason is that I want to log all changes to these nodes (count changes and update other model fields with the new count), but dont want myself and other developers to forget that we cannot assign fields directly, as this wouldn't trigger custom assign_node logic.
How сan I do that?
You could try do prevent assignment to these fields with __setattr__, but I recommend you don't do that for two reasons:
this will probably introduce all kinds of unexpected side-effects
you are performing a check at runtime, to prevent something introduced at the time the code was written. That's like running a (tiny) unit-test on every request.
I would simply rename the fields to _primary_node and _secondary_node to indicate that these are private fields and not intended to be used directly.
In addition, you could write a hook for your version control system that checks for assignments to these fields. This could be a simple grep for _primary_node = or something fancier like a plugin for a linter like flake8.
This simply disable setting of 'primary_node'
class Chat(models.Model)
def __setattr__(self, attrname, val):
if attrname == 'primary_node': #sets only the attribute if it's not primary_node
print('[!] You cannot assign primary_node like that! Use assign_node() method please.')
else:
super(Chat, self).__setattr__(attrname, val)
EDIT :
You should probably use the #property decorator:
Using #property versus getters and setters
Related
Sometimes a ForeignKey field needs a default. For example:
class ReleaseManager(BaseManager):
def default(self):
return self.filter(default=True).order_by('-modified').first()
class Release(BaseModel):
default = models.BooleanField(default=False)
...
class Server(models.Model):
...
release = models.ForeignKey(Release, null=True, default=Release.objects.default)
All is well and good with the above code until the time comes for db migration whereupon the functional default causes big problems because the default function cannot be serialized. Manual migration can work around this but on a large project where migrations are perhaps squashed periodically this leaves a time bomb for the unwary.
A common workaround is to move the default from the field to the save method of the model but this causes confusion if the model is used by things like the rest framework or in creating forms where the default is expected on the field.
My current favourite workaround works with migrations and with the rest framework and other form generation. It assumes the object manager supplies a default method and uses a specialized ForeignKey field to get at it:
class ForeignKeyWithObjectManagerDefault(models.ForeignKey):
def __init__(self, to, **kwargs):
super().__init__(to, **kwargs)
self.to = to
def get_default(self):
return self.to.objects.default().pk
class Project(SOSAdminObject):
primary = ForeignKeyWithObjectManagerDefault(Primary, related_name='projects')
...
Now migrations work as expected and we can use any functionality we like to supply a default object to a foreign key field.
I need to define a Django model field with the name in, which is a Python language keyword. This is a syntax error:
class MyModel(models.Model):
in = jsonfield.JSONField()
How can I make this work?
The reason I need this name is when I use django-rest-framework's ModelSerializer class, field name is used as the key for serialization output, and I thought it might be easier to manipulate django's Model class instead of ModelSerializer class to get the output I want.
Generally speaking, you don't. Avoid the use of keywords in your identifiers. The general Python convention is to add an underscore to such names; here that'd be in_:
class MyModel(models.Model):
in_ = jsonfield.JSONField()
However, Django prohibits names ending in an underscore because the underscore clashes with their filter naming conventions, so you have to come up with a different name still; pick one that still describes your case; I picked contained in rather than in, as a guess to what you want to do here:
class MyModel(models.Model):
contained_in = jsonfield.JSONField()
If you are trying to match an existing database schema, use the db_column attribute:
class MyModel(models.Model):
contained_in = jsonfield.JSONField(db_column='in')
If you want to be stubborn, in normal classes you could use setattr() after creating the class to use a string instead of an identifier:
class Foo:
pass
setattr(Foo, 'in', 'some value')
but you'll have to use setattr(), getattr(), delattr() and/or vars() everywhere in your code to be able to access this.
In Django you'll have the added complication that a models.Model subclass uses a metaclass to parse out your class members into others structures, and adding an extra field with setattr() doesn't work without (a lot of) extra work to re-do what the metaclass does. You could instead use the field.contribute_to() method, calling it after the class has been prepared by Django (technique taken from this blog post):
from django.db.models.signals import class_prepared
def add_field(sender, **kwargs):
if sender.__name__ == "MyModel":
field = jsonfield.JSONField('in')
field.contribute_to_class(sender, 'in')
class_prepared.connect(add_field)
but you have to make sure this hook is registered before you create your model class.
There is no way to make it work, and it's a bad idea anyway. Choose a different name.
If, for some reason, you want to have column name that matches some reserved keyword, use db_column argument for that field.
in_something = models.CharField(db_column='in', max_length=100)
You mentioned the use of django rest framework. Here's how to make it work on the serializer layer. The keyword used is from. to is just an example of a non-keyword if you want it mapped to a different name.
from django.db import models
from rest_framework import serializers
SP_FIELD_MAP = {
'from': 'sender'
}
# would be in models.py
class Transaction(models.Model):
recipient = models.CharField(max_length=16)
sender = models.CharField(max_length=64)
# would be in serializers.py
class TransactionSerializer(serializers.ModelSerializer):
to = serializers.CharField(source='recipient')
class Meta:
model = Transaction
fields = ('id', 'to', 'from')
# `from` is a python keyword hence this
extra_kwargs = {'from': {'source': 'sender'}}
def build_field(self, field_name, info, model_class, nested_depth):
# Catches python keywords like `from` and maps to its proper field
field_name = SP_FIELD_MAP.get(field_name, field_name)
return super(TransactionSerializer, self).build_field(
field_name, info, model_class, nested_depth)
Tested on CharField using POST and GET methods only but I don't see how it won't work on other methods. You might need special stuff for other field types though. I suggest going into the source. There's tons of fun stuff going on in DRF's source.
You should be giving all your variables descriptive names that clearly state what they are to be used for, and where possible it should be easy to assertain what type of variable it is.
in, to me, would appear at first glance to be a boolean so in order to use this variable in my own extension to the code I'd need to find other usages of it before I knew how I could use it.
Therefore, simply don't try to hack something together just so you can get this terrible variable name into your model, it offers no value to you to do so, its not really any quicker to type since intellisense is available in most places. Figure out what "in" relates to and then formulate a proper name that is descriptive.
I'm setting up a data model in django using multiple-table inheritance (MTI) like this:
class Metric(models.Model):
account = models.ForeignKey(Account)
date = models.DateField()
value = models.FloatField()
calculation_in_progress = models.BooleanField()
type = models.CharField( max_length=20, choices= METRIC_TYPES ) # Appropriate?
def calculate(self):
# default calculation...
class WebMetric(Metric):
url = models.URLField()
def calculate(self):
# web-specific calculation...
class TextMetric(Metric):
text = models.TextField()
def calculate(self):
# text-specific calculation...
My instinct is to put a 'type' field in the base class as shown here, so I can tell which sub-class any Metric object belongs to. It would be a bit of a hassle to keep this up to date all the time, but possible. But do I need to do this? Is there some way that django handles this automatically?
When I call Metric.objects.all() every objects returned is an instance of Metric never the subclasses. So if I call .calculate() I never get the sub-class's behavior.
I could write a function on the base class that tests to see if I can cast it to any of the sub-types like:
def determine_subtype(self):
try:
self.webmetric
return WebMetric
except WebMetric.DoesNotExist:
pass
# Repeat for every sub-class
but this seems like a bunch of repetitious code. And it's also not something that can be included in a SELECT filter -- only works in python-space.
What's the best way to handle this?
While it might offend some people's sensibilities, the only practical way to solve this problem is to put either a field or a method in the base class which says what kind of object each record really is. The problem with the method you describe is that it requires a separate database query for every type of subclass, for each object you're dealing with. This could get extremely slow when working with large querysets. A better way is to use a ForeignKey to the django Content Type class.
#Carl Meyer wrote a good solution here: How do I access the child classes of an object in django without knowing the name of the child class?
Single Table Inheritance could help alleviate this issue, depending on how it gets implemented. But for now Django does not support it: Single Table Inheritance in Django so it's not a helpful suggestion.
But do I need to do this?
Never. Never. Never.
Is there some way that django handles this automatically?
Yes. It's called "polymorphism".
You never need to know the subclass. Never.
"What about my WebMetric.url and my TextMetric.text attributes?"
What will you do with these attributes? Define a method function that does something. Implement different versions in WebMetric (that uses url) and TextMetric (that uses text).
That's proper polymorphism.
Please read this: http://docs.djangoproject.com/en/1.2/topics/db/models/#abstract-base-classes
Please make your superclass abstract.
Do NOT do this: http://docs.djangoproject.com/en/1.2/topics/db/models/#multi-table-inheritance
You want "single-table inheritance".
I'm using Satchmo and Django and am trying to extend Satchmo's Product model. I'd like to make one of the fields in Satchmo's Product model have a default value in the admin without changing Satchmo's source code. Here is an abbreviated version of Satchmo's Product model:
class Product(models.Model):
site = models.ForeignKey(Site, verbose_name='Site')
This is what I attempted to do to extend it...
class MyProduct(Product):
Product.site = models.ForeignKey(Site, verbose_name='Site', editable=False, default=1)
This does not work, any ideas on why?
For two reasons, firstly the way you are trying to override a class variable just isn't how it works in Python. You just define it in the class as normal, the same way that def __init__(self): is overriding the super-class initializer. But, Django model inheritance simply doesn't support this. If you want to add constraints, you could do so in the save() method.
You could probably monkeypatch it if you really wanted to:
site_field = Product._meta.get_field('site')
site_field.editable = False
site_field.default = 1
But this is a nasty habit and could cause problems; arguably less maintainable than just patching Satchmo's source directly.
You can't change the superclass from a subclass.
You have the source. Use subversion. Make the change. When Satchmo is updated merge the updates around your change.
I was wondering if it was possible (and, if so, how) to chain together multiple managers to produce a query set that is affected by both of the individual managers. I'll explain the specific example that I'm working on:
I have multiple abstract model classes that I use to provide small, specific functionality to other models. Two of these models are a DeleteMixin and a GlobalMixin.
The DeleteMixin is defined as such:
class DeleteMixin(models.Model):
deleted = models.BooleanField(default=False)
objects = DeleteManager()
class Meta:
abstract = True
def delete(self):
self.deleted = True
self.save()
Basically it provides a pseudo-delete (the deleted flag) instead of actually deleting the object.
The GlobalMixin is defined as such:
class GlobalMixin(models.Model):
is_global = models.BooleanField(default=True)
objects = GlobalManager()
class Meta:
abstract = True
It allows any object to be defined as either a global object or a private object (such as a public/private blog post).
Both of these have their own managers that affect the queryset that is returned. My DeleteManager filters the queryset to only return results that have the deleted flag set to False, while the GlobalManager filters the queryset to only return results that are marked as global. Here is the declaration for both:
class DeleteManager(models.Manager):
def get_query_set(self):
return super(DeleteManager, self).get_query_set().filter(deleted=False)
class GlobalManager(models.Manager):
def globals(self):
return self.get_query_set().filter(is_global=1)
The desired functionality would be to have a model extend both of these abstract models and grant the ability to only return the results that are both non-deleted and global. I ran a test case on a model with 4 instances: one was global and non-deleted, one was global and deleted, one was non-global and non-deleted, and one was non-global and deleted. If I try to get result sets as such: SomeModel.objects.all(), I get instance 1 and 3 (the two non-deleted ones - great!). If I try SomeModel.objects.globals(), I get an error that DeleteManager doesn't have a globals (this is assuming my model declaration is as such: SomeModel(DeleteMixin, GlobalMixin). If I reverse the order, I don't get the error, but it doesn't filter out the deleted ones). If I change GlobalMixin to attach GlobalManager to globals instead of objects (so the new command would be SomeModel.globals.globals()), I get instances 1 and 2 (the two globals), while my intended result would be to only get instance 1 (the global, non-deleted one).
I wasn't sure if anyone had run into any situation similar to this and had come to a result. Either a way to make it work in my current thinking or a re-work that provides the functionality I'm after would be very much appreciated. I know this post has been a little long-winded. If any more explanation is needed, I would be glad to provide it.
Edit:
I have posted the eventual solution I used to this specific problem below. It is based on the link to Simon's custom QuerySetManager.
See this snippet on Djangosnippets: http://djangosnippets.org/snippets/734/
Instead of putting your custom methods in a manager, you subclass the queryset itself. It's very easy and works perfectly. The only issue I've had is with model inheritance, you always have to define the manager in model subclasses (just: "objects = QuerySetManager()" in the subclass), even though they will inherit the queryset. This will make more sense once you are using QuerySetManager.
Here is the specific solution to my problem using the custom QuerySetManager by Simon that Scott linked to.
from django.db import models
from django.contrib import admin
from django.db.models.query import QuerySet
from django.core.exceptions import FieldError
class MixinManager(models.Manager):
def get_query_set(self):
try:
return self.model.MixinQuerySet(self.model).filter(deleted=False)
except FieldError:
return self.model.MixinQuerySet(self.model)
class BaseMixin(models.Model):
admin = models.Manager()
objects = MixinManager()
class MixinQuerySet(QuerySet):
def globals(self):
try:
return self.filter(is_global=True)
except FieldError:
return self.all()
class Meta:
abstract = True
class DeleteMixin(BaseMixin):
deleted = models.BooleanField(default=False)
class Meta:
abstract = True
def delete(self):
self.deleted = True
self.save()
class GlobalMixin(BaseMixin):
is_global = models.BooleanField(default=True)
class Meta:
abstract = True
Any mixin in the future that wants to add extra functionality to the query set simply needs to extend BaseMixin (or have it somewhere in its heirarchy). Any time I try to filter the query set down, I wrapped it in a try-catch in case that field doesn't actually exist (ie, it doesn't extend that mixin). The global filter is invoked using globals(), while the delete filter is automatically invoked (if something is deleted, I never want it to show). Using this system allows for the following types of commands:
TemporaryModel.objects.all() # If extending DeleteMixin, no deleted instances are returned
TemporaryModel.objects.all().globals() # Filter out the private instances (non-global)
TemporaryModel.objects.filter(...) # Ditto about excluding deleteds
One thing to note is that the delete filter won't affect admin interfaces, because the default Manager is declared first (making it the default). I don't remember when they changed the admin to use Model._default_manager instead of Model.objects, but any deleted instances will still appear in the admin (in case you need to un-delete them).
I spent a while trying to come up with a way to build a nice factory to do this, but I'm running into a lot of problems with that.
The best I can suggest to you is to chain your inheritance. It's not very generic, so I'm not sure how useful it is, but all you would have to do is:
class GlobalMixin(DeleteMixin):
is_global = models.BooleanField(default=True)
objects = GlobalManager()
class Meta:
abstract = True
class GlobalManager(DeleteManager):
def globals(self):
return self.get_query_set().filter(is_global=1)
If you want something more generic, the best I can come up with is to define a base Mixin and Manager that redefines get_query_set() (I'm assuming you only want to do this once; things get pretty complicated otherwise) and then pass a list of fields you'd want added via Mixins.
It would look something like this (not tested at all):
class DeleteMixin(models.Model):
deleted = models.BooleanField(default=False)
class Meta:
abstract = True
def create_mixin(base_mixin, **kwargs):
class wrapper(base_mixin):
class Meta:
abstract = True
for k in kwargs.keys():
setattr(wrapper, k, kwargs[k])
return wrapper
class DeleteManager(models.Manager):
def get_query_set(self):
return super(DeleteManager, self).get_query_set().filter(deleted=False)
def create_manager(base_manager, **kwargs):
class wrapper(base_manager):
pass
for k in kwargs.keys():
setattr(wrapper, k, kwargs[k])
return wrapper
Ok, so this is ugly, but what does it get you? Essentially, it's the same solution, but much more dynamic, and a little more DRY, though more complex to read.
First you create your manager dynamically:
def globals(inst):
return inst.get_query_set().filter(is_global=1)
GlobalDeleteManager = create_manager(DeleteManager, globals=globals)
This creates a new manager which is a subclass of DeleteManager and has a method called globals.
Next, you create your mixin model:
GlobalDeleteMixin = create_mixin(DeleteMixin,
is_global=models.BooleanField(default=False),
objects = GlobalDeleteManager())
Like I said, it's ugly. But it means you don't have to redefine globals(). If you want a different type of manager to have globals(), you just call create_manager again with a different base. And you can add as many new methods as you like. Same for the manager, you just keep adding new functions that will return different querysets.
So, is this really practical? Maybe not. This answer is more an exercise in (ab)using Python's flexibility. I haven't tried using this, though I do use some of the underlying principals of dynamically extending classes to make things easier to access.
Let me know if anything is unclear and I'll update the answer.
Another option worth considering is the PassThroughManager:
https://django-model-utils.readthedocs.org/en/latest/managers.html#passthroughmanager
You should use QuerySet instead of Manager.
See Documentation here.