so i needed to had some model-translation support for my DRF API and i started using django-hvad.
It seems to work well with my django application but i am getting some issues with the DRF APi.
I am trying to create a simple POST request and i am getting a error:
Accessing a translated field requires that the instance has a translation loaded, or a valid translation in current language (en) loadable from the database
Here are my models, serializers and viewsets:
Model:
class Mission(TranslatableModel):
translations = TranslatedFields(
mission=models.CharField(max_length=255, help_text="Mission name"),
)
def __unicode__(self):
return self.lazy_translation_getter('mission', str(self.pk))
Serializer:
class MissionSerializer(serializers.ModelSerializer):
mission = serializers.CharField(source='mission')
class Meta:
model = Mission
Viewset:
class MissionViewSet(viewsets.ModelViewSet):
queryset = Mission.objects.language().all()
serializer_class = MissionSerializer
authentication_classes = (NoAuthentication,)
permission_classes = (AllowAny,)
def get_queryset(self):
# Set Language For Translations
user_language = self.request.GET.get('language')
if user_language:
translation.activate(user_language)
return Mission.objects.language().all()
Does anyone know how i can get around this?? I am also opened to other suggested apps known to work but i would really like to have this one working
I got this to work thanks to the Spectras here https://github.com/KristianOellegaard/django-hvad/issues/211
The issue, I guess is DRF tries to do some introspection on the model. I do use DRF in a project of mine, on a TranslatableModel. It needs some glue to work properly. I once suggested adding that to hvad, but we concluded that that would be overextending the feature set. Maybe another module some day, but I don't have enough time to maintain both hvad and that.
It's been some time since I implemented it, so here it is as is:
# hvad compatibility for rest_framework - JHA
class TranslatableModelSerializerOptions(serializers.ModelSerializerOptions):
def __init__(self, meta):
super(TranslatableModelSerializerOptions, self).__init__(meta)
# We need this ugly hack as ModelSerializer hardcodes a read_only_fields check
self.translated_read_only_fields = getattr(meta, 'translated_read_only_fields', ())
self.translated_write_only_fields = getattr(meta, 'translated_write_only_fields', ())
class HyperlinkedTranslatableModelSerializerOptions(serializers.HyperlinkedModelSerializerOptions):
def __init__(self, meta):
super(HyperlinkedTranslatableModelSerializerOptions, self).__init__(meta)
# We need this ugly hack as ModelSerializer hardcodes a read_only_fields check
self.translated_read_only_fields = getattr(meta, 'translated_read_only_fields', ())
self.translated_write_only_fields = getattr(meta, 'translated_write_only_fields', ())
class TranslatableModelMixin(object):
def get_default_fields(self):
fields = super(TranslatableModelMixin, self).get_default_fields()
fields.update(self._get_translated_fields())
return fields
def _get_translated_fields(self):
ret = OrderedDict()
trans_model = self.opts.model._meta.translations_model
opts = trans_model._meta
forward_rels = [field for field in opts.fields
if field.serialize and not field.name in ('id', 'master')]
for trans_field in forward_rels:
if trans_field.rel:
raise RuntimeError()
field = self.get_field(trans_field)
if field:
ret[trans_field.name] = field
for field_name in self.opts.translated_read_only_fields:
assert field_name in ret
ret[field_name].read_only = True
for field_name in self.opts.translated_write_only_fields:
assert field_name in ret
ret[field_name].write_only = True
return ret
def restore_object(self, attrs, instance=None):
new_attrs = attrs.copy()
lang = attrs['language_code']
del new_attrs['language_code']
if instance is None:
# create an empty instance, pre-translated
instance = self.opts.model()
instance.translate(lang)
else:
# check we are updating the correct translation
tcache = self.opts.model._meta.translations_cache
translation = getattr(instance, tcache, None)
if not translation or translation.language_code != lang:
# nope, get the translation we are updating, or create it if needed
try:
translation = instance.translations.get_language(lang)
except instance.translations.model.DoesNotExist:
instance.translate(lang)
else:
setattr(instance, tcache, translation)
return super(TranslatableModelMixin, self).restore_object(new_attrs, instance)
class TranslatableModelSerializer(TranslatableModelMixin, serializers.ModelSerializer):
_options_class = TranslatableModelSerializerOptions
class HyperlinkedTranslatableModelSerializer(TranslatableModelMixin,
serializers.HyperlinkedModelSerializer):
_options_class = HyperlinkedTranslatableModelSerializerOptions
From there, you just inherit your serializers from TranslatableModelSerializer or HyperlinkedTranslatableModelSerializer. When POSTing, you should simple add language_code as a normal field as part of your JSON / XML / whatever.
The main trick is in the restore_object method. Object creation needs to include translation loading.
Related
I have defined a serializer like this:
class ActivitySerializer(serializers.ModelSerializer):
activity_project = serializers.SlugRelatedField(queryset=Project.objects.all(), slug_field='project_name')
activity_tags = serializers.SlugRelatedField(queryset=Tag.objects.all(), slug_field='tag_name', many=True)
class Meta:
model = Activity
fields = ('activity_name', 'activity_description', 'activity_status', 'activity_completion_percent', 'activity_due_date', 'activity_project', 'activity_tags',)
Now if I insert an activity_tag that does not exist in the database, I get a validation error"
{
"activity_tags": [
"Object with tag_name=test does not exist."
]
}
I would like to create a validation method that adds the tag in the database if it does not exist.
I have tried using the
def validate(self, attrs):
....
method, but apparently for a slug field there is a method that is called before this one.
Can someone point me to the right method I should use? Would this method be called in the corresponding view?
I think you would need to create a nested serializer for this to work. This is totally untested and off the top of my head, but maybe something like this:
class ActivityTagFieldSerializer(serializer.ModelSerializer):
tag_name = serializers.SlugField()
class Meta:
model = Tag
fields = ('tag_name')
class ActivitySerializer(serializer.ModelSerializer):
activity_tags = ActivityTagFieldSerializer(many=True)
class Meta:
model = Activity
fields = ('activity_tags', 'activity_project', ...)
def create(self, validated_data):
tags = validated_data.pop('activity_tags')
activity = Activity.objects.create(**validated_data)
for tag in tags:
try:
tag_to_add = Tag.objects.get(**tag)
except:
tag_to_add = Tag.objects.create(**tag)
activity.activity_tags.add(tag_to_add)
return activity
Check the API guide for writable nested serializers
I managed to do this by subclassing SlugRelatedField and overriding "to_internal_value" method. In the original implementation this method tries to get an object from the queryset, and if an object doesn't exist it fails the validation. So instead of calling "get" method, I'm calling "get_or_create":
class CustomSlugRelatedField(serializers.SlugRelatedField):
def to_internal_value(self, data):
try:
obj, created = self.get_queryset().get_or_create(**{self.slug_field: data})
return obj
except (TypeError, ValueError):
self.fail('invalid')
I have a model in Django with a method that I can't seem to access in the view. Here is the model:
class Realtor(models.Model):
user = models.OneToOneField(User)
team = models.ForeignKey(RealtorTeam, blank=True, null=True, default=None)
is_showing_realtor = models.BooleanField(default=False)
is_listing_realtor = models.BooleanField(default=False)
def type(self):
if self.is_showing_realtor and self.is_listing_realtor:
return 'Showing and Listing'
elif self.is_showing_realtor:
return 'Showing'
elif self.is_listing_realtor:
return 'Listing'
else:
return ''
Now, in the view, I try to query the model, including the method, like this:
q = Realtor.objects.all()
q = q.filter(user__first_name__icontains=first)
q = q.filter(user__last_name__icontains=last)
q = q.filter(team__name__icontains=team)
q = q.filter(user__email__icontains=email)
q = q.filter(type__icontains=type)
The error I get is Cannot resolve keyword 'type' into field. Choices are.... I looked up a number of stackoverflow questions, and read through more of the Django docs, and I saw to add a field like this in the model:
type_display = (type,)
But that didn't work. Do I have to query the is_showing_realtor and is_listing_realtor? Is there no way to query the method?
More info
We are using Django_tables2 to display tables of models. We are able to access the type method with that, like this:
class RealtorTable(tables.Table):
id = tables.Column()
first_name = tables.Column(accessor='user.first_name', order_by='user.first_name')
last_name = tables.Column(accessor='user.last_name', order_by='user.last_name')
team = tables.Column()
email = tables.Column(accessor='user.email', order_by='user.email')
type = tables.Column(order_by='is_showing_realtor')
But still, can't pass it to the view.
type is the name of a built-in function python, and so also in django. I'd recommend renaming your function to something like listing_type to avoid any clashes.
In particular, this line
type_display = (type,)
probably isn't doing what you think it is.
reptilicus is correct, I'd not spotted the filter on a non-field attribute, that doesn't make sense. You could turn listing_type into a CharField with choices= set, and then some custom save() logic to set it when the model is changed - or you could use property
#def listing_type():
doc = "The listing_type property."
def fget(self):
if self.is_showing_realtor and self.is_listing_realtor:
return 'Showing and Listing'
elif self.is_showing_realtor:
return 'Showing'
elif self.is_listing_realtor:
return 'Listing'
else:
return ''
def fset(self, value):
self._listing_type = value
def fdel(self):
del self._listing_type
return locals()
listing_type = property(**listing_type())
PS I still don't like type :-)
I'm no Django expert, but I'm pretty sure that you can't query on model attributes that aren't columns in the database, which is what you are trying to do. Perhaps try to decorate the method with #property to make it like an attribute.
You could just do a list comprehension if like
results = [m for m in query_set if m.type = "Showing and listing"]
Struggling to figure out how to Over-Ride the __init__() method in my Django Form to include additional values from the database. I have a group of photographers that I am trying to list as a form option for the user. Afterwards, the user's photographer selection will be added (along with other information) to the database as an instantiation of a new model.
This is a continuation, or elaboration, of my other Current Question. #Rob Osborne has given me some great advice helping me understand how to extend BaseForm, but I still cannot get my code to execute. The linked question lists my models, form, and views, if you are interested. While I understand that using ModelForm is easier and more documented, I must use BaseForm in this instance.
Here is what I have:
class AForm(BaseForm):
def __init__(self, data=None, files=None, instance=None, auto_id='id_%s',
prefix=None, initial=None, error_class=ErrorList,
label_suffix=':', empty_permitted=False):
self.instance = instance
object_data = self.instance.fields_dict()
self.declared_fields = SortedDict()
self.base_fields = fields_for_a(self.instance)
BaseForm.__init__(self, data, files, auto_id, prefix, object_data,
error_class, label_suffix, empty_permitted)
self.fields['photographer'].queryset = Photographer.objects.all()
def save(self, commit=True):
if not commit:
raise NotImplementedError("AForm.save must commit it's changes.")
if self.errors:
raise ValueError(_(u"The Form could not be updated because the data didn't validate."))
cleaned_data = self.cleaned_data
# save fieldvalues for self.instance
fields = field_list(self.instance)
for field in fields:
if field.enable_wysiwyg:
value = unicode(strip(cleaned_data[field.name]))
else:
value = unicode(cleaned_data[field.name])
Using the above code results in a KeyError at 'photographer'.
I appreciate any ideas / comments on how to resolve this KeyError so that I can get the photographer values into my form. Thank you!
EDIT:
Trying to use super, as recommended by #supervacuo, but still getting a KeyError at photographer as before:
class AForm(BaseForm):
def __init__(self, data=None, files=None, instance=None, auto_id='id_%s',
prefix=None, initial=None, error_class=ErrorList,
label_suffix=':', empty_permitted=False):
super(AForm, self).__init__(data, files, auto_id, prefix, object_data, error_class, label_suffix, empty_permitted)
self.fields['photographer'].queryset = Photographer.objects.all()
What could I be missing that is generating the KeyError? Thanks for any advice.
EDIT 2: adding fields_dict()
from models.py
class A(models.Model):
category = models.ForeignKey(Category)
user = models.ForeignKey(User)
def fields_dict(self):
fields_dict = {}
fields_dict['title'] = self.title
for key, value in self.fields():
fields_dict[key.name] = value.value
return fields_dict
Thanks for any advice.
EDIT 3: (edited class AForm above in the initial question as well, to include more information)
def fields_for_a(instance):
fields_dict = SortedDict()
fields = field_list(instance)
for field in fields:
if field.field_type == Field.BOOLEAN_FIELD:
fields_dict[field.name] = forms.BooleanField(label=field.label, required=False, help_text=field.help_text)
elif field.field_type == Field.CHAR_FIELD:
widget = forms.TextInput
fields_dict[field.name] = forms.CharField(label=field.label, required=field.required, max_length=field.max_length, help_text=field.help_text, widget=widget)
fields_dict[field.name] = field_type(label=field.label,
required=field.required,
help_text=field.help_text,
max_length=field.max_length,
widget=widget)
return fields_dict
EDIT 4: def fields(self). from models.py:
def fields(self):
fields_list = []
fields = list(self.category.field_set.all())
fields += list(Field.objects.filter(category=None))
for field in fields:
try:
fields_list.append((field, field.fieldvalue_set.get(ad=self),))
except FieldValue.DoesNotExist:
pass # If no value is associated with that field, skip it.
return fields_list
def field(self, name):
if name == 'title':
return self.title
else:
return FieldValue.objects.get(field__name=name, ad=self).value
That GitHub link should've been the first thing in your question.
The django-classifieds application has an entire system of dynamic fields (based on the Field and FieldValue models) which is why you're having trouble. If you don't fully understand this aspect of django-classifieds, I recommend you base your project on something else instead.
Looking down the list of FIELD_CHOICES in django-classified's models.py, you can't use this database-driven field system to define relationsips — so there's no dynamic per-category ForeignKey field!
The alternative would be to add a photographer field on your A model (any particular reason you've renamed it from Ad?), as it seems you have done based on your other question. To go the rest of the distance, however, you'd need to edit the fields_dict() method like so:
def fields_dict(self):
fields_dict = {}
fields_dict['title'] = self.title
fields_dict['photographer'] = self.photographer
for key, value in self.fields():
fields_dict[key.name] = value.value
return fields_dict
Your call to BaseForm.__init__ seems wrong; you should be using super(), like so
class AForm(BaseForm):
def __init__(self, *args, **kwargs):
super(AForm, self).__init__(*args, **kwargs)
self.fields['photographer'].queryset = Photographer.objects.all()
(as actually recommended in Rob Osbourne's accepted answer to your other question).
Beyond that, I am suspicious of your fields_dict() method, which isn't part of Django and you haven't provided the definition for. Confirm with print self.fields.keys() that, for whatever mysterious reason, photographer is not there, then post the code for fields_dict().
I have such model:
class Place(models.Model):
name = models.CharField(max_length=80, db_index=True)
city = models.ForeignKey(City)
address = models.CharField(max_length=255, db_index=True)
# and so on
Since I'm importing them from many sources, and users of my website are able to add new Places, I need a way to merge them from an admin interface. Problem is, name is not very reliable since they can be spelled in many different ways, etc
I'm used to use something like this:
class Place(models.Model):
name = models.CharField(max_length=80, db_index=True) # canonical
city = models.ForeignKey(City)
address = models.CharField(max_length=255, db_index=True)
# and so on
class PlaceName(models.Model):
name = models.CharField(max_length=80, db_index=True)
place = models.ForeignKey(Place)
query like this
Place.objects.get(placename__name='St Paul\'s Cathedral', city=london)
and merge like this
class PlaceAdmin(admin.ModelAdmin):
actions = ('merge', )
def merge(self, request, queryset):
main = queryset[0]
tail = queryset[1:]
PlaceName.objects.filter(place__in=tail).update(place=main)
SomeModel1.objects.filter(place__in=tail).update(place=main)
SomeModel2.objects.filter(place__in=tail).update(place=main)
# ... etc ...
for t in tail:
t.delete()
self.message_user(request, "%s is merged with other places, now you can give it a canonical name." % main)
merge.short_description = "Merge places"
as you can see, I have to update all other models with FK to Place with new values. But it's not very good solution since I have to add every new model to this list.
How do I "cascade update" all foreign keys to some objects prior to deleting them?
Or maybe there are other solutions to do/avoid merging
If anyone intersted, here is really generic code for this:
def merge(self, request, queryset):
main = queryset[0]
tail = queryset[1:]
related = main._meta.get_all_related_objects()
valnames = dict()
for r in related:
valnames.setdefault(r.model, []).append(r.field.name)
for place in tail:
for model, field_names in valnames.iteritems():
for field_name in field_names:
model.objects.filter(**{field_name: place}).update(**{field_name: main})
place.delete()
self.message_user(request, "%s is merged with other places, now you can give it a canonical name." % main)
Two libraries now exist with up-to-date model merging functions that incorporate related models:
Django Extensions' merge_model_instances management command.
Django Super Deduper
Tested on Django 1.10. Hope it can serve.
def merge(primary_object, alias_objects, model):
"""Merge 2 or more objects from the same django model
The alias objects will be deleted and all the references
towards them will be replaced by references toward the
primary object
"""
if not isinstance(alias_objects, list):
alias_objects = [alias_objects]
if not isinstance(primary_object, model):
raise TypeError('Only %s instances can be merged' % model)
for alias_object in alias_objects:
if not isinstance(alias_object, model):
raise TypeError('Only %s instances can be merged' % model)
for alias_object in alias_objects:
# Get all the related Models and the corresponding field_name
related_models = [(o.related_model, o.field.name) for o in alias_object._meta.related_objects]
for (related_model, field_name) in related_models:
relType = related_model._meta.get_field(field_name).get_internal_type()
if relType == "ForeignKey":
qs = related_model.objects.filter(**{ field_name: alias_object })
for obj in qs:
setattr(obj, field_name, primary_object)
obj.save()
elif relType == "ManyToManyField":
qs = related_model.objects.filter(**{ field_name: alias_object })
for obj in qs:
mtmRel = getattr(obj, field_name)
mtmRel.remove(alias_object)
mtmRel.add(primary_object)
alias_object.delete()
return True
Based on the snippet provided in the comments in the accepted answer, I was able to develop the following. This code does not handle GenericForeignKeys. I don't ascribe to their use as I believe it indicates a problem with the model you are using.
I used list a lot of code to do this in this answer, but I have updated my code to use django-super-deduper mentioned here. At the time, django-super-deduper did not handle unmanaged models in a good way. I submitted an issue, and it looks like it will be corrected soon. I also use django-audit-log, and I don't want to merge those records. I kept the signature and the #transaction.atomic() decorator. This is helpful in the event of a problem.
from django.db import transaction
from django.db.models import Model, Field
from django_super_deduper.merge import MergedModelInstance
class MyMergedModelInstance(MergedModelInstance):
"""
Custom way to handle Issue #11: Ignore models with managed = False
Also, ignore auditlog models.
"""
def _handle_o2m_related_field(self, related_field: Field, alias_object: Model):
if not alias_object._meta.managed and "auditlog" not in alias_object._meta.model_name:
return super()._handle_o2m_related_field(related_field, alias_object)
def _handle_m2m_related_field(self, related_field: Field, alias_object: Model):
if not alias_object._meta.managed and "auditlog" not in alias_object._meta.model_name:
return super()._handle_m2m_related_field(related_field, alias_object)
def _handle_o2o_related_field(self, related_field: Field, alias_object: Model):
if not alias_object._meta.managed and "auditlog" not in alias_object._meta.model_name:
return super()._handle_o2o_related_field(related_field, alias_object)
#transaction.atomic()
def merge(primary_object, alias_objects):
if not isinstance(alias_objects, list):
alias_objects = [alias_objects]
MyMergedModelInstance.create(primary_object, alias_objects)
return primary_object
I was looking for a solution to merge records in Django Admin, and found a package that is doing it (https://github.com/saxix/django-adminactions).
How to use:
Install package:
pip install django-adminactions
Add adminactions to your INSTALLED_APPS:
INSTALLED_APPS = (
'adminactions',
'django.contrib.admin',
'django.contrib.messages',
)
Add actions to admin.py:
from django.contrib.admin import site
import adminactions.actions as actions
actions.add_to_site(site)
Add service url to your urls.py: url(r'^adminactions/', include('adminactions.urls')),
Tried it just now, it works for me.
I'm having trouble overriding a ModelForm save method. This is the error I'm receiving:
Exception Type: TypeError
Exception Value: save() got an unexpected keyword argument 'commit'
My intentions are to have a form submit many values for 3 fields, to then create an object for each combination of those fields, and to save each of those objects. Helpful nudge in the right direction would be ace.
File models.py
class CallResultType(models.Model):
id = models.AutoField(db_column='icontact_result_code_type_id', primary_key=True)
callResult = models.ForeignKey('CallResult', db_column='icontact_result_code_id')
campaign = models.ForeignKey('Campaign', db_column='icampaign_id')
callType = models.ForeignKey('CallType', db_column='icall_type_id')
agent = models.BooleanField(db_column='bagent', default=True)
teamLeader = models.BooleanField(db_column='bTeamLeader', default=True)
active = models.BooleanField(db_column='bactive', default=True)
File forms.py
from django.forms import ModelForm, ModelMultipleChoiceField
from callresults.models import *
class CallResultTypeForm(ModelForm):
callResult = ModelMultipleChoiceField(queryset=CallResult.objects.all())
campaign = ModelMultipleChoiceField(queryset=Campaign.objects.all())
callType = ModelMultipleChoiceField(queryset=CallType.objects.all())
def save(self, force_insert=False, force_update=False):
for cr in self.callResult:
for c in self.campain:
for ct in self.callType:
m = CallResultType(self) # this line is probably wrong
m.callResult = cr
m.campaign = c
m.calltype = ct
m.save()
class Meta:
model = CallResultType
File admin.py
class CallResultTypeAdmin(admin.ModelAdmin):
form = CallResultTypeForm
In your save you have to have the argument commit. If anything overrides your form, or wants to modify what it's saving, it will do save(commit=False), modify the output, and then save it itself.
Also, your ModelForm should return the model it's saving. Usually a ModelForm's save will look something like:
def save(self, commit=True):
m = super(CallResultTypeForm, self).save(commit=False)
# do custom stuff
if commit:
m.save()
return m
Read up on the save method.
Finally, a lot of this ModelForm won't work just because of the way you are accessing things. Instead of self.callResult, you need to use self.fields['callResult'].
UPDATE: In response to your answer:
Aside: Why not just use ManyToManyFields in the Model so you don't have to do this? Seems like you're storing redundant data and making more work for yourself (and me :P).
from django.db.models import AutoField
def copy_model_instance(obj):
"""
Create a copy of a model instance.
M2M relationships are currently not handled, i.e. they are not copied. (Fortunately, you don't have any in this case)
See also Django #4027. From http://blog.elsdoerfer.name/2008/09/09/making-a-copy-of-a-model-instance/
"""
initial = dict([(f.name, getattr(obj, f.name)) for f in obj._meta.fields if not isinstance(f, AutoField) and not f in obj._meta.parents.values()])
return obj.__class__(**initial)
class CallResultTypeForm(ModelForm):
callResult = ModelMultipleChoiceField(queryset=CallResult.objects.all())
campaign = ModelMultipleChoiceField(queryset=Campaign.objects.all())
callType = ModelMultipleChoiceField(queryset=CallType.objects.all())
def save(self, commit=True, *args, **kwargs):
m = super(CallResultTypeForm, self).save(commit=False, *args, **kwargs)
results = []
for cr in self.callResult:
for c in self.campain:
for ct in self.callType:
m_new = copy_model_instance(m)
m_new.callResult = cr
m_new.campaign = c
m_new.calltype = ct
if commit:
m_new.save()
results.append(m_new)
return results
This allows for inheritance of CallResultTypeForm, just in case that's ever necessary.