I have a base64 field that is copied from the django snippet.
https://djangosnippets.org/snippets/1669/
class Base64Field(models.TextField):
"""
https://djangosnippets.org/snippets/1669/
Example use:
class Foo(models.Model):
data = Base64Field()
foo = Foo()
foo.data = 'Hello world!'
print foo.data # will 'Hello world!'
print foo.data_base64 # will print 'SGVsbG8gd29ybGQh\n'
"""
def contribute_to_class(self, cls, name):
if not self.db_column:
self.db_column = name
self.field_name =name+ '_base64'
super(Base64Field, self).contribute_to_class(cls, self.field_name)
setattr(cls, name, property(self.get_data, self.set_data))
def get_data(self, obj):
return base64.decodestring(getattr(obj, self.field_name))
def set_data(self, obj, data):
setattr(obj, self.field_name, base64.encodestring(data))
def deconstruct(self):
ame, path, args, kwargs = super(Base64Field, self).deconstruct()
from pprint import pprint
pprint(vars(self))
return ame, path, args, kwargs
I am facing issues while migrating this field
e.g.
class EmailStatus(models.Model):
attachment = Base64Field(null=True, blank=True, db_column='attachment', name="attachment", verbose_name="attachment")
The error I am getting while migrating is
raise FieldDoesNotExist('%s has no field named %r' % (self.object_name, field_name))
django.core.exceptions.FieldDoesNotExist: EmailStatus has no field named u'attachment'
Now I can see why that is happening. But cant figure out a way around it. I think I might need to change something in the deconstruct field. I have tried multiple things for this but all of them broke.
e.g. removing the _base64. It does not work while saving and retrieving data.
I tried changing the name in the migrations file it does not work.
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(name='EmailStatus',
fields=[('attachment', gradsite.gradnotes.models.Base64Field(blank=True, null=True)),])]
I think the migrations auto-detecter is getting confused because of the change in name in contribute_to_class. I am not sure what can be a work around.
class Base64Field(models.TextField):
def contribute_to_class(self, cls, name, private_only=False):
if not self.db_column:
self.db_column = name
self.field_name = name + '_base64'
super().contribute_to_class(cls,
name)
setattr(cls, self.field_name, property(self.get_data, self.set_data))
def get_data(self, obj):
return base64.b64encode(getattr(obj, self.name).encode('utf-8'))
def set_data(self, obj, data):
setattr(obj, self.field_name, base64.b64decode(data).decode('utf-8'))
This seems to work. There was a mix-up between self.field_name and name in contribute_to_class leading to the wrong value being used (hence makemigrations not picking up the field the second time around/when using migrate).
I have made python3 specific changes, namely the super calls and the use of base64 functions. The set_data method may be wrong (I didn't look into that too much, since you may be using python2 and encoding would differ), but migrations work.
Added bonus: the private_only argument was missing from your contribute_to_class method.
Here's what I'm getting:
from test_app import models
e = models.EmailStatus()
e.attachment = "Hello world!"
e.attachment # Prints 'Hello world!'
e.attachment_base64 # Prints b'SGVsbG8gd29ybGQh'
Useful link for you.
https://code.djangoproject.com/ticket/24563
HELPFUL TIPs:
The failing migration:
https://github.com/codefisher/djangopress/blob/master/djangopress/forum/migrations/0011_auto_20150426_1821.py
The models:
https://github.com/codefisher/djangopress/blob/master/djangopress/forum/models.py
It may be helps to others.
Related
I am using django-extensions.AutoSlugField and django.db.models.ImageField.
To customize image name uploaded for django.db.models.ImageField, what I did:
from django.utils.text import slugify
# idea is to make image name the same as the automatically generated slug, however don't work
def update_image_name(instance, filename):
# this debug instance, and instance.slug is empty string
print(instance.__dict__)
# I attempt to use slugify directly and see that it's not the same as the output generated by AutoSlugField
# E.g. If I create display_name "shawn" 2nd time, AutoSlugField will return "shawn-2", but slugify(display_name) return "shawn"
print(slugify(instance.display_name))
return f"images/{instance.slug}.jpg"
class Object(models.Model):
...
display_name = models.TextField()
...
# to customize uploaded image name
image = models.ImageField(blank=True, upload_to=update_image_name)
...
# create slug automatically from display_name
slug = AutoSlugField(blank=True, populate_from=["display_name"]
Based on what I debug, when I call instance inside update_image_name, slug is empty string.
If I understand correctly slug is only created at event save, so when I call ImageField instance, slug is not yet created, therefore empty string.
I think it might have something to do with event post save. However, I am not sure if that's the real reason or how to do that.
How can I get the automatically generated slug as my customized image name?
That's a tricky one because the order the fields are getting saved matters.
The brute-force attack I'm suggesting would be to override the save method of your model and manually call the create_slug method before everything else ensuring the slug is set:
from django.utils.encoding import force_str
[...]
class Object(models.Model):
[...]
def save(self, *args, **kwargs):
self.slug = force_str(self._meta.get_field('slug').create_slug(self, False))
super(Object, self).save(*args, **kwargs)
That's what AutoSlugField does, refer to the code here. self._meta.get_field('slug') get's the slug field definition and then we just call the create_slug method.
Tested under Python 3.7.9 & Django 3.1.5 like this:
from django.core.files.uploadedfile import SimpleUploadedFile
o = Object()
o.display_name = "foo bar"
o.image = SimpleUploadedFile(name='test_image.png', content=open('/path/to/test/image.png', 'rb').read(), content_type='image/png')
o.save()
Then I see update_image_name return images/foo-bar.jpg.
I would like to control some configuration settings for my project using a database model. For example:
class JuicerBaseSettings(models.Model):
max_rpm = model.IntegerField(default=10)
min_rpm = model.IntegerField(default=0)
There should only be one instance of this model:
juicer_base = JuicerBaseSettings()
juicer_base.save()
Of course, if someone accidentally creates a new instances, it's not the end of the world. I could just do JuicerBaseSettings.objects.all().first(). However, is there a way to lock it down such that it's impossible to create more than 1 instance?
I found two related questions on SO. This answer suggests using 3rd party apps like django-singletons, which doesn't seem to be actively maintained (last update to the git repo is 5 years ago). Another answer suggests using a combination of either permissions or OneToOneField. Both answers are from 2010-2011.
Given that Django has changed a lot since then, are there any standard ways to solve this problem? Or should I just use .first() and accept that there may be duplicates?
You can override save method to control number of instances:
class JuicerBaseSettings(models.Model):
def save(self, *args, **kwargs):
if not self.pk and JuicerBaseSettings.objects.exists():
# if you'll not check for self.pk
# then error will also raised in update of exists model
raise ValidationError('There is can be only one JuicerBaseSettings instance')
return super(JuicerBaseSettings, self).save(*args, **kwargs)
Either you can override save and create a class function JuicerBaseSettings.object()
class JuicerBaseSettings(models.Model):
#classmethod
def object(cls):
return cls._default_manager.all().first() # Since only one item
def save(self, *args, **kwargs):
self.pk = self.id = 1
return super().save(*args, **kwargs)
============= OR =============
Simply, Use django_solo.
https://github.com/lazybird/django-solo
Snippet Courtsy: django-solo-documentation.
# models.py
from django.db import models
from solo.models import SingletonModel
class SiteConfiguration(SingletonModel):
site_name = models.CharField(max_length=255, default='Site Name')
maintenance_mode = models.BooleanField(default=False)
def __unicode__(self):
return u"Site Configuration"
class Meta:
verbose_name = "Site Configuration"
# admin.py
from django.contrib import admin
from solo.admin import SingletonModelAdmin
from config.models import SiteConfiguration
admin.site.register(SiteConfiguration, SingletonModelAdmin)
# There is only one item in the table, you can get it this way:
from .models import SiteConfiguration
config = SiteConfiguration.objects.get()
# get_solo will create the item if it does not already exist
config = SiteConfiguration.get_solo()
If your model is used in django-admin only, you additionally can set dynamic add permission for your model:
# some imports here
from django.contrib import admin
from myapp import models
#admin.register(models.ExampleModel)
class ExampleModelAdmin(admin.ModelAdmin):
# some code...
def has_add_permission(self, request):
# check if generally has add permission
retVal = super().has_add_permission(request)
# set add permission to False, if object already exists
if retVal and models.ExampleModel.objects.exists():
retVal = False
return retVal
i am not an expert but i guess you can overwrite the model's save() method so that it will check if there has already been a instance , if so the save() method will just return , otherwise it will call the super().save()
You could use a pre_save signal
#receiver(pre_save, sender=JuicerBaseSettings)
def check_no_conflicting_juicer(sender, instance, *args, **kwargs):
# If another JuicerBaseSettings object exists a ValidationError will be raised
if JuicerBaseSettings.objects.exclude(pk=instance.pk).exists():
raise ValidationError('A JuiceBaseSettings object already exists')
I'm a bit late to the party but if you want to ensure that only one instance of an object is created, an alternative solution to modifying a models save() function would be to always specify an ID of 1 when creating an instance - that way, if an instance already exists, an integrity error will be raised.
e.g.
JuicerBaseSettings.objects.create(id=1)
instead of:
JuicerBaseSettings.objects.create()
It's not as clean of a solution as modifying the save function but it still does the trick.
I did something like this in my admin so that I won't ever go to original add_new view at all unless there's no object already present:
def add_view(self, request, form_url='', extra_context=None):
obj = MyModel.objects.all().first()
if obj:
return self.change_view(request, object_id=str(obj.id) if obj else None)
else:
return super(type(self), self).add_view(request, form_url, extra_context)
def changelist_view(self, request, extra_context=None):
return self.add_view(request)
Works only when saving from admin
Back in the days of South migrations, if you wanted to create a custom model field that extended a Django field's functionality, you could tell South to use the introspection rules of the parent class like so:
from south.modelsinspector import add_introspection_rules
add_introspection_rules([], ["^myapp\.stuff\.fields\.SomeNewField"])
Now that migrations have been moved to Django, is there a non-South equivalent of the above? Is an equivalent even needed anymore, or is the new migration stuff smart enough to just figure it out on its own?
As Phillip mentions in the comments, deconstruct() is the official way to handle custom fields in django migrations.
To go on to complete the request for clarification... It would appear that there are already a couple of examples of code out there written to handle both. For example, this excerpt (to handle the on parameter for ExclusiveBooleanField) is taken from django-exclusivebooleanfield:
from django.db import models, transaction
from django.db.models import Q
from six import string_types
from six.moves import reduce
try:
transaction_context = transaction.atomic
except AttributeError:
transaction_context = transaction.commit_on_success
class ExclusiveBooleanField(models.BooleanField):
"""
Usage:
class MyModel(models.Model):
the_one = ExclusiveBooleanField()
class MyModel(models.Model):
field_1 = ForeignKey()
field_2 = CharField()
the_one = ExclusiveBooleanField(on=('field_1', 'field_2'))
# `on` is a bit like a unique constraint, value of field
# is only exclusive for rows with same value of the on fields
"""
def __init__(self, on=None, *args, **kwargs):
if isinstance(on, string_types):
on = (on, )
self._on_fields = on or ()
super(ExclusiveBooleanField, self).__init__(*args, **kwargs)
def contribute_to_class(self, cls, name):
super(ExclusiveBooleanField, self).contribute_to_class(cls, name)
models.signals.class_prepared.connect(self._replace_save, sender=cls)
def deconstruct(self):
"""
to support Django 1.7 migrations, see also the add_introspection_rules
section at bottom of this file for South + earlier Django versions
"""
name, path, args, kwargs = super(
ExclusiveBooleanField, self).deconstruct()
if self._on_fields:
kwargs['on'] = self._on_fields
return name, path, args, kwargs
def _replace_save(self, sender, **kwargs):
old_save = sender.save
field_name = self.name
on_fields = self._on_fields
def new_save(self, *args, **kwargs):
def reducer(left, right):
return left & Q(**{right: getattr(self, right)})
with transaction_context():
if getattr(self, field_name) is True:
f_args = reduce(reducer, on_fields, Q())
u_args = {field_name: False}
sender._default_manager.filter(f_args).update(**u_args)
old_save(self, *args, **kwargs)
new_save.alters_data = True
sender.save = new_save
try:
from south.modelsinspector import add_introspection_rules
add_introspection_rules(
rules=[
(
(ExclusiveBooleanField,),
[],
{"on": ["_on_fields", {"default": tuple()}]},
)
],
patterns=[
'exclusivebooleanfield\.fields\.ExclusiveBooleanField',
]
)
except ImportError:
pass
I'm creating the following custom field based off How to create list field in django
import re
from django.db import models
from django.forms.widgets import TextInput
class ListField(models.TextField):
__metaclass__ = models.SubfieldBase
description = "Stores a python list"
widget = TextInput
def __init__(self, *args, **kwargs):
super(ListField, self).__init__(*args, **kwargs)
def to_python(self, value):
if not value:
return []
return filter(None, re.split(r'\,|\s*', value))
def get_prep_value(self, value):
if value is None:
return value
return ', '.join(value)
def value_to_string(self, obj):
value = self._get_val_from_obj(obj)
return self.get_db_prep_value(value)
from south.modelsinspector import add_introspection_rules
add_introspection_rules([], ["^cflowportal\.utils\.modelutils\.ListField"])
Basically, what I want to achieve is a field where you write something like "1, asd, asdf fdgd", it stores it as such in the database but when retrieved it should return that string as an array and when given an array it should convert it back to a comma-seperated string.
I'm still not sure if what I've written so far works, but I'm having trouble displaying it as an input field and not a textarea even if I've set widget=TextInput.
So, how do I show it in the admin with the same input used by the standard CharField?
How can I customize it so that it displays a comma-separated string when showed on such input, but is given back as a Python List when accessed elsewhere?
Thanks
The following is a method to realize what you want
from django.db import models
class Blog(models.Model):
title = models.CharField(max_length=256)
labels = models.TextField()
def get_labels(self):
return self.content.split('\n')
def set_labels(self,value):
if isinstance(value,list) or isinstance(value,tuple) or isinstance(value,set):
content = '\n'.join(value)
else:
content = value
self.content = content
You can regard labels as a ListField, set value use obj.set_labels(list) function, and get value use obj.get_labels()
It act as a List Field, and admin site will run as a normal TextField.
This is what I did, but a better solution is excepted.
and a better way to do this is using save_model in admin.py:
class BlogAdmin(admin.ModelAdmin):
def save_model(self, request, obj, form, change):
# extra data handling, prevent data convert
obj.save()
I am trying to do a very simple custom field, but can't seem to get it to work.
Currently, I am adding this field to pretty much all models in my application. I would like to have it specified as a custom field to avoid duplicate code.
identifier = models.CharField(
max_length = 20,
unique = True, validators = [validators.validate_slug],
help_text = "Help text goes here."
)
What I have is this:
class MyIdentifierField(models.CharField):
description = "random string goes here"
__metaclass__ = models.SubfieldBase
def __init__(self, *args, **kwargs):
kwargs['max_length'] = 20
kwargs['unique'] = True
kwargs['validators'] = [validators.validate_slug]
kwargs['help_text'] = "custom help text goes here"
super(MyIdentifierField, self).__init__(*args, **kwargs)
def db_type(self, connection):
return 'char(25)'
so that I can use it like this:
identifier = MyIdentifierField()
However, when I do python manage.py schemamigration --auto <myapp>, I am getting the following error:
! Cannot freeze field 'geral.seccao.identifier'
! (this field has class geral.models.MyIdentifierField)
! South cannot introspect some fields; this is probably because they are custom
! fields. If they worked in 0.6 or below, this is because we have removed the
! models parser (it often broke things).
! To fix this, read http://south.aeracode.org/wiki/MyFieldsDontWork
I have gone through the recommended webpage, but still can't seem to find a workaround this. Any help is appreciated. Thanks.
You need to help South out a bit when describing your field. There are two methods to do this as documented at: http://south.aeracode.org/wiki/MyFieldsDontWork
My preferred method is to add a South field triple on the field class:
def south_field_triple(self):
try:
from south.modelsinspector import introspector
cls_name = '{0}.{1}'.format(
self.__class__.__module__,
self.__class__.__name__)
args, kwargs = introspector(self)
return cls_name, args, kwargs
except ImportError:
pass
Hope that helps you out.