django-parler - M2M Relationship Issue with Admin M2M Field (Circular Dependency?) - python

we're facing an issue with the use of M2M relationships - It seems we have a circular dependency issue?
We need to make a M2M relationship to a TranslatableModel field (which is M2M under the hood), exposed in the Admin (we have a multi-select widget that utilises the M2M field).
Lots of research has only revealed this specific debug info (thanks to #egasimus):
models.py
# Here's a pretty basic model with a translatable M2M field.
class ContentItem(TranslatableModel):
translations = TranslatedFields(
title = models.CharField(max_length=200),
content = models.TextField(blank=True),
primary_media = models.ForeignKey(
'media.MediaAsset', related_name='cards_where_primary',
blank=True, null=True)
extra_media = models.ManyToManyField(
'media.MediaAsset', related_name='cards_where_extra',
blank=True, null=True))
author = models.ForeignKey(settings.AUTH_USER_MODEL)
created = models.DateTimeField(auto_now_add=True)
modified = models.DateTimeField(auto_now=True)
admin.py
class ContentItemAdmin(TranslatableAdmin):
fields = ('title', 'content', 'primary_media', 'extra_media',
'author', 'created', 'modified')
# The above code results in the following error:
# FieldError at /admin/cms/contentitem/1/
# Unknown field(s) (extra_media) specified for ContentItem. Check fields/fieldsets/exclude attributes of class CardAdmin.
class ContentItemAdmin(TranslatableAdmin): pass
# And, if I don't explicitly define fields, the `extra_media` field doesn't show up at all.
# This is confirmed if I run `manage.py shell` and I retrieve a ContentItem instance:
# it simply does not have an `extra_media` attribute. However, if I do manually retrieve
# a translation instance, the `extra_media` M2M field is there - it just doesn't end up
# getting added to the shared object.
models.py
# Adding a call to `get_m2m_with_model()`, though, makes any translated M2M fields
# appear on the shared object, and in the admin.
class TranslatedFieldsModel(models.Model):
def _get_field_values(self):
return [getattr(self, field.get_attname()) for field, _
in self._meta.get_fields_with_model()
+ tuple(self._meta.get_m2m_with_model())]
#classmethod
def get_translated_fields(cls):
return [f.name for f, _
in cls._meta.get_fields_with_model()
+ tuple(cls._meta.get_m2m_with_model())
if f.name not in ('language_code', 'master', 'id')]
# However, when I try to save a new ContentItem via the admin,
# I get the aforementioned error:
#
# "<ContentItemTranslation: #None, bg, master: #None>" needs to have a value for field
# "contentitemtranslation" before this many-to-many relationship can be used.
#
# I assume that this has to do with the order in which the shared model, the translation model,
# and the M2M intermediate model interact, and the order in which they are saved.
Has anyone faced such an issue? Any pointers to how we can get around this problem?
Thank you for any help you can provide.

Related

Django Rest Framework: Get field name from model definition

Within the Django Rest framework documentation it is suggested to declare the "field" list explicitly to avoid providing the data of new columns just by adding them to the model which may contain sensitive information.
The field list is an array of strings, containing the field ids. To avoid declaring field ids, which actually do not exist in the model (e.g. typos or changed models) I tried to declare the list using object references - but always end up with "DeferredAttribute: object has no attribute ".
I have read something that meta information is not available in objects and that you could solve that by defininig your own Meta class using Object._meta.get_fields() and store it in the class, but I thought there might be a simpler/more elegant way (and I do now know, how, in detail ;-)).
Example:
class Samples(models.Model):
# Meta data, primarily used in AdminSite.
class Meta:
verbose_name = _('Samples')
verbose_name_plural = _('Samples')
samples_boolfield = models.BooleanField
samples_textfield = models.CharField(max_length=2000, blank=True)
views.py:
class SamplesView(viewsets.ModelViewSet):
serializer_class = SamplesSerializer
queryset = Samples.objects.all()
serializers.py:
Version 1, which does not show any errors in pyCharm or makemigrations, but calling the API reults in "TypeError at /api/samples/: argument of type 'DeferredAttribute' is not iterable":
class SamplesSerializer(serializers.ModelSerializer):
class Meta:
model = Samples
fields = (
'id',
Samples.samples_boolfield,
Samples.samples_textfield,
)
Version 2, which does not show any errors in pyCharm, but makemigrations fails with "DeferredAttribute: object has no attribute name":
class SamplesSerializer(serializers.ModelSerializer):
class Meta:
model = Samples
fields = (
'id',
Samples.samples_boolfield.__name__,
Samples.samples_textfield.__name__,
)
Version 3, which does not show any errors in pyCharm, but makemigrations fails with "DeferredAttribute: object has no attribute get_attname":
class SamplesSerializer(serializers.ModelSerializer):
class Meta:
model = Samples
fields = (
'id',
Samples.samples_boolfield.get_attname(),
Samples.samples_textfield.get_attname(),
)
Is there a way to declare the field list using object references (so that it fails e.g. in pyCharm/during compilation)?
Thank you for your feedback.
Regards,
HerrB92

Django admin interface: using horizontal_filter with ManyToMany field with intermediate table

I am trying to enhance the django admin interface similar to what has been done in the accepted answer of this SO post. I have a many-to-many relationship between a User table and a Project table. In the django admin, I would like to be able to assign users to a project as in the image below:
It works fine with a simple ManyToManyField but the problem is that my model uses the through parameter of the ManyToManyField to use an intermediary table. I cannot use the save_m2m() and set() function and I am clueless on how to adapt the code below to make it work.
The model:
class UserProfile(models.Model):
user = models.OneToOneField(User, unique=True)
projects = models.ManyToManyField(Project, through='Membership')
class Project(models.Model):
name = models.CharField(max_length=100, unique=True)
application_identifier = models.CharField(max_length=100)
type = models.IntegerField(choices=ProjectType)
...
class Membership(models.Model):
project = models.ForeignKey(Project,on_delete=models.CASCADE)
user = models.ForeignKey(UserProfile,on_delete=models.CASCADE)
# extra fields
rating = models.IntegerField(choices=ProjectType)
...
The code used for the widget in admin.py:
from django.contrib.admin.widgets import FilteredSelectMultiple
class ProjectAdminForm(forms.ModelForm):
class Meta:
model = Project
fields = "__all__" # not in original SO post
userprofiles = forms.ModelMultipleChoiceField(
queryset=UserProfile.objects.all(),
required=False,
widget=FilteredSelectMultiple(
verbose_name='User Profiles',
is_stacked=False
)
)
def __init__(self, *args, **kwargs):
super(ProjectAdminForm, self).__init__(*args, **kwargs)
if self.instance.pk:
self.fields['userprofiles'].initial = self.instance.userprofile_set.all()
def save(self, commit=True):
project = super(ProjectAdminForm, self).save(commit=False)
if commit:
project.save()
if project.pk:
project.userprofile_set = self.cleaned_data['userprofiles']
self.save_m2m()
return project
class ProjectAdmin(admin.ModelAdmin):
form = ProjectAdminForm
...
Note: all the extra fields from the intermediary model do not need to be changed in the Project Admin view (they are automatically computed) and they all have a default value.
Thanks for your help!
I could find a way of solving this issue. The idea is:
Create new entries in the Membership table if and only if they are new (otherwise it would erase the existing data for the other fields in the Membership table)
Remove entries that were deselected from the Membership table
To do this, I replaced:
if project.pk:
project.userprofile_set = self.cleaned_data['userprofiles']
self.save_m2m()
By:
if project.pk:
# Get the existing relationships
current_project_selections = Membership.objects.filter(project=project)
current_selections = [o.userprofile for o in current_project_selections]
# Get the submitted relationships
submitted_selections = self.cleaned_data['userprofiles']
# Create new relation in Membership table if they do not exist
for userprofile in submitted_selections :
if userprofile not in current_selections:
Membership(project=project,userprofile=userprofile).save()
# Remove the relations that were deselected from the Membership table
for project_userprofile in current_project_selections:
if project_userprofile.userprofile not in submitted_selections :
project_userprofile.delete()

Passing additional attributes all the way thru to nested serializers

I've been having issues passing additional attributes thru using Django Rest Framework with nested serializers.
I've created a Document model that has a ForeignKey owner/creator relationship, and several other ForeignKey related models. Some of those other model have an owner/creator ForeignKey associated as well.
class Document(models.Model):
owner = models.ForeignKey('auth.User',related_name='document')
candidate = models.ForeignKey(
Candidate,
on_delete=models.CASCADE,
blank=True,
null=True,
)
class Candidate(models.Model):
owner = models.ForeignKey('auth.User', related_name='candidates')
first_name = models.CharField(max_length=30, blank=True, null=True)
When saving down the Document model with a nested serializer and a custom create() method, I can pass all fields down, however, the nested models don't seem to be able to pick up the Owner field, regardless of how I pass it in. Creating a Candidate alone is fine.
class CandidateSerializer(serializers.ModelSerializer):
owner = serializers.ReadOnlyField(source='owner.username')
class Meta:
model = Candidate
fields = (
'pk',
'first_name',
'owner',
)
class DocumentSerializer(serializers.ModelSerializer):
owner = serializers.ReadOnlyField(source='owner.username')
candidate = CandidateSerializer(required=True)
class Meta:
model = Document
fields = (
'owner',
'candidate',
)
def create(self, validated_data):
candidate_data = validated_data.pop('candidate')
document = Document.objects.create(**validated_data)
Candidate.objects.create(**candidate_data)
With DocumentSerializer set up like this, I get errors like this while trying to do a POST to Document with nested fields.
IntegrityError: NOT NULL constraint failed: dossier_candidate.owner_id
When I modify the DocumentSerializer.create() method to try to pick up the owner, it seems that owner = serializers.ReadOnlyField(source='owner.username') is now out of scope, even though it should be under the class.
i.e.,
When I try to create the Candidate object with
Candidate.objects.create(owner, **candidate_data)
I get this error :
NameError at /rest/documents/
global name 'owner' is not defined
When I try this
Candidate.objects.create(self.owner, **candidate_data)
I get this error:
AttributeError: 'DocumentSerializer' object has no attribute 'owner'
What's the proper method of making sure the nested Candidate object is able to be created successfully, picking up the owner field?
First thing first, you won't have the owner to create/update since it's read only.
If you want to get the current user for that, use CurrentUserDefault. It'll be added to the validated_data

Can't disable ForeignKey referential integrity check in Django 1.9

I have a model with two entities, Person and Code. Person is referenced by Code twice, a Person can be either the user of the code or the approver.
What I want to achieve is the following:
if the user provides an existing Person.cusman, no further action is needed.
if the user provides an unknown Person.cusman, a helper code looks up other attributes of the Person (from an external database), and creates a new Person entity.
I have implemented a function triggered by pre_save signal, which creates the missing Person on the fly. It works fine as long as I use python manage.py shell to create a Code with nonexistent Person.
However, when I try to add a new Code using the admin form or a CreateView descendant I always get the following validation error on the HTML form:
Select a valid choice. That choice is not one of the available choices.
Obviously there's a validation happening between clicking on the Save button and the Code.save() method, but I can't figure out which is it. Can you help me which method should I override to accept invalid foreign keys until pre_save creates the referenced entity?
models.py
class Person(models.Model):
cusman = models.CharField(
max_length=10,
primary_key=True)
name = models.CharField(max_length=30)
email = models.EmailField()
def __unicode__(self):
return u'{0} ({1})'.format(self.name, self.cusman)
class Code(models.Model):
user = models.ForeignKey(
Person,
on_delete=models.PROTECT,
db_constraint=False)
approver = models.ForeignKey(
Person,
on_delete=models.PROTECT,
related_name='approves',
db_constraint=False)
signals.py
#receiver(pre_save, sender=Code)
def create_referenced_person(sender, instance, **kwargs):
def create_person_if_doesnt_exist(cusman):
try:
Person = Person.objects.get(pk=cusman)
except Person.DoesNotExist:
Person = Person()
cr = CusmanResolver()
Person_details = cr.get_person_details(cusman)
Person.cusman = Person_details['cusman']
Person.name = Person_details['name']
Person.email = Person_details['email']
Person.save()
create_Person_if_doesnt_exist(instance.user_id)
create_Person_if_doesnt_exist(instance.approver_id)
views.py
class CodeAddForm(ModelForm):
class Meta:
model = Code
fields = [
'user',
'approver',
]
widgets = {
'user': TextInput,
'approver': TextInput
}
class CodeAddView(generic.CreateView):
template_name = 'teladm/code_add.html'
form_class = CodeAddForm
You misunderstood one thing: You shouldn't use TextField to populate ForeignKey, because django foreign keys are populated using dropdown/radio button to refer to the id of the object in another model. The error you got means you provided wrong information that doesn't match any id in another model(Person in your case).
What you can do is: not using ModelForm but Form. You might have some extra work to do after you call form.is_valid(), but at least you could code up your logic however you want.

Save a many-to-many model in Django/REST?

I'm writing a REST API for my Django app, and can't get POST requests to work on one model.
Here's the model in question:
class ProjectNode(models.Model):
name = models.CharField(max_length=60)
place = models.CharField(max_length=150)
time_spent = models.BigIntegerField()
parent_project = models.ForeignKey(Project, related_name='tasks')
workers = models.ManyToManyField(User, related_name='tasks_can_do')
def __str__(self):
return self.name
The User model just holds a name field at the moment.
Here's my serializer for ProjectNode:
class ProjectNodeSerializer(serializers.ModelSerializer):
class Meta:
model = ProjectNode
fields = ('id', 'name', 'place', 'time_spent', 'workers',)
And here's the API view (from views.py):
class WebProjectNodeListView(generics.ListCreateAPIView):
queryset = ProjectNode.objects.all()
serializer_class = ProjectNodeSerializer
def pre_save(self, obj):
obj.parent_project = Project.objects.get(pk=self.request.DATA['parent_project'])
for worker_pk in self.request.DATA['workers']:
obj.workers.add(User.objects.get(pk=worker_pk))
obj.final_worker = User.objects.get(pk=self.request.DATA['final_workers'])
I tried a simpler version yesterday at first, which only had the Project ForeignKey relationship, and it seemed to work, so I thought that using add would work too, but I get an error when testing out the API with httpie (I already added some users and projects, and am sure I get their id's correctly).
Here's the request:
http POST :8000/api/tasks/ name="newtask" place="home" time_spent:=50 parent_project:=1 workers:=[1]
And I get this error:
"<ProjectNode: newtask>" needs to have a value for field "projectnode" before this many-to-many relationship can be used.
And the traceback also points to this line of code:
obj.workers.add(User.objects.get(id=worker_pk))
Now, I get the feeling that this is because I'm trying to update the relationship on the User object before a ProjectNode object is created in the database, but I'm not sure how to resolve this?
DRF doesn't works create models which are nested serializers objects or Many to Many fields.
So is necessary to override Serializer create method and create/get M2M models before create ProjectNode.
Try to override create(self, validated_data) in your serializer and work with your data inside this method..
Example:
My model Project has M2M relation with ProjectImages. In ProjectSerializer I override create method like this.
def create(self, validated_data):
try:
# Remove nested and M2m relationships from validated_data
images = validated_data.pop('projectimage_set') if 'projectimage_set' in validated_data else []
# Create project model
instance = Project(**validated_data)
if status:
instance.set_status(status)
project = instance.save()
# Create relations
for image in images:
ProjectImage.objects.create(project=project, **image)
except exceptions.ValidationError as e:
errors_messages = e.error_dict if hasattr(e, 'error_dict') else e.error_list
raise serializers.ValidationError(errors_messages)
return project
Hope this help!

Categories

Resources