Very Simple Django Custom Field - django

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.

Related

django-simple-history save history only when a certain condition is met

I'm building a project using Django. I want to use the django-simple-history package to save the history of a model for every create/update/delete.
I want to save the history only when the superuser or a specific type of user (e.g. supervisor) makes a create/update/delete action, but I don't know how can I implement this condition.
I've tried this to see if the current user is one of its arguments to use it, but this method seems that it doesn't work properly. Below a portion of my code:
class WFEmployee(models.Model):
code = models.IntegerField(unique=True)
name_en = models.CharField(max_length=55)
...
history = HistoricalRecords()
def save_without_historical_record(self, *args, **kwargs):
print(kwargs, 'from save without ')
try:
ret = self.save(*args, **kwargs)
finally:
pass
return ret
I get nothing in the console when I use .save_without_historical_record() it saves the instance but doesn't print anything.
Maybe override the save method?
def save(self, *args, **kwargs):
if user== supervisor: ##Your condition here
self.skip_history_when_saving = True
super().save(*args, **kwargs)
Is this what you looking for?

How to migrate django custom base64 field. Field does not exist

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.

How to make Django forms return only one error message instead of a list of error messages for one field?

I'm working with Django forms and Django REST Framework to create a project.
Now I'm wondering: how can I make Django return only ONE error message per form field instead of a list of error messages per form field?
I'll explain myself somehow further with a code example.
I defined a form in Django:
class EditProfileForm(forms.Form):
name = forms.CharField(required=True, max_length=50)
description = forms.CharField(required=False, max_length=200)
url = forms.URLField(required=False, max_length=100)
location = forms.CharField(required=False, max_length=100)
And now I run the validation on this form in Django context (python manage.py shell):
>>> f=EditProfileForm({})
>>> f.is_valid()
False
>>> f._errors
{'name': [u'This field is required.']}
As you can see, the form errors returns a dictionary and there's a list of error messages present for the form field / key "name". How can I make sure that Django only returns ONE string as error messages instead of a list of error messages for this form field?
Thanks in advance for any help!
Kind regards,
K.
As I'm looking in the Django code, I think I need to modify/override the ErorrList or ErrorDict classes with something else, but I'm not sure if this is a smart idea... I'm using Django 1.7.1 by the way.
Update!
And the solution is: override the ErrorList class.
from django import forms
from django.forms.utils import ErrorList
class CustomErrorList(ErrorList):
def as_text(self):
return u'%s' % self[0]
class CustomForm(forms.Form):
def __init__(self, *args, **kwargs):
super(CustomForm, self).__init__(*args, **kwargs)
self.error_class = CustomErrorList
Now when you call the as_text() method, one and only one error message in the list (for the same key) is returned. Django is great, by the way! ;-)

Adding links to full change forms for inline items in django admin?

I have a standard admin change form for an object, with the usual StackedInline forms for a ForeignKey relationship. I would like to be able to link each inline item to its corresponding full-sized change form, as the inline item has inlined items of its own, and I can't nest them.
I've tried everything from custom widgets to custom templates, and can't make anything work. So far, the "solutions" I've seen in the form of snippets just plain don't seem to work for inlines. I'm getting ready to try some DOM hacking with jQuery just to get it working and move on.
I hope I must be missing something very simple, as this seems like such a simple task!
Using Django 1.2.
There is a property called show_change_link since Django 1.8.
I did something like the following in my admin.py:
from django.utils.html import format_html
from django.core.urlresolvers import reverse
class MyModelInline(admin.TabularInline):
model = MyModel
def admin_link(self, instance):
url = reverse('admin:%s_%s_change' % (instance._meta.app_label,
instance._meta.module_name),
args=(instance.id,))
return format_html(u'Edit', url)
# … or if you want to include other fields:
return format_html(u'Edit: {}', url, instance.title)
readonly_fields = ('admin_link',)
The currently accepted solution here is good work, but it's out of date.
Since Django 1.3, there is a built-in property called show_change_link = True that addresses this issue.
This can be added to any StackedInline or TabularInline object. For example:
class ContactListInline(admin.TabularInline):
model = ContactList
fields = ('name', 'description', 'total_contacts',)
readonly_fields = ('name', 'description', 'total_contacts',)
show_change_link = True
The result will be something line this:
I had similar problem and I came up with custom widget plus some tweaks to model form. Here is the widget:
from django.utils.safestring import mark_safe
class ModelLinkWidget(forms.Widget):
def __init__(self, obj, attrs=None):
self.object = obj
super(ModelLinkWidget, self).__init__(attrs)
def render(self, name, value, attrs=None):
if self.object.pk:
return mark_safe(
u'<a target="_blank" href="../../../%s/%s/%s/">%s</a>' %\
(
self.object._meta.app_label,
self.object._meta.object_name.lower(),
self.object.pk, self.object
)
)
else:
return mark_safe(u'')
Now since widget for each inline need to get different object in constructor you can't just set it in standard way, but in Form's init method:
class TheForm(forms.ModelForm):
...
# required=False is essential cause we don't
# render input tag so there will be no value submitted.
link = forms.CharField(label='link', required=False)
def __init__(self, *args, **kwargs):
super(TheForm, self).__init__(*args, **kwargs)
# instance is always available, it just does or doesn't have pk.
self.fields['link'].widget = ModelLinkWidget(self.instance)
Quentin's answer above works, but you also need to specify fields = ('admin_link',)
There is a module for this purpose. Check out:
django-relatives
I think: args=[instance.id] should be args=[instance.pk]. It worked for me!

Django Admin: Detect if a subset of an object fields has changed and which of them

I need to detect when some of the fields of certain model have changed in the admin, to later send notifications depending on which fields changed and previous/current values of those fields.
I tried using a ModelForm and overriding the save() method, but the form's self.cleaned_data and seld.instance already have the new values of the fields.
Modifying the answer above... taking the brilliant function from Dominik Szopa and changing it will solve your relationship change detection: Use this:
def get_changes_between_models(model1, model2, excludes = []):
changes = {}
for field in model1._meta.fields:
if not (field.name in excludes):
if field.value_from_object(model1) != field.value_from_object(model2):
changes[field.verbose_name] = (field.value_from_object(model1),
field.value_from_object(model2))
return changes
Then in your code you can say (avoid try/except for performance reasons):
if (self.id):
old = MyModel.Objects.get(pk=self.id)
changes = get_changes_between_models(self, old)
if (changes):
# Process based on what is changed.
If you are doing this at the "model" level, there is no way to save the extra query. The data has already been changed by the time you reach the "Save" point. My first post, so forgive me if I sound like an idiot.
To avoid extra DB lookup, I modified constructor to remember initial value and use this in save method later:
class Package(models.Model):
feedback = models.IntegerField(default = 0, choices = FEEDBACK_CHOICES)
feedback_time = models.DateTimeField(null = True)
def __init__(self, *args, **kw):
super(Package, self).__init__(*args, **kw)
self._old_feedback = self.feedback
def save(self, force_insert=False, force_update=False, *args, **kwargs):
if not force_insert and self.feedback != self._old_feedback:
self.feedback_time = datetime.utcnow()
return super(Package, self).save(force_insert, force_update, *args, **kwargs)
In order to get differences of two model instances, you can also use this function. It compare to model instances and returns dictionary of changes.
What you'll need to do is get an extra copy of the object you're working on from the database inside the save method before fully saving it. Example:
class MyModel(models.Model):
field1 = models.CharField(max_length=50)
def save(self):
if self.id:
try:
old = MyModel.objects.get(pk=self.id)
if old.field1 != self.field1:
# Process somehow
except MyModel.DoesNotExist:
pass
super(MyModel, self).save()