I have the following models.
class User(models.Model):
...
class Group(models.Model):
name = models.CharField(max_length=255)
creater = models.ForeignKey(User)
users = models.ManyToManyField(User, through='GroupUser', through_fields=('group', 'user'))
class GroupUser(models.Model):
group = models.ForeignKey(Group)
user = models.ForeignKey(User)
date_joined = models.DateField(auto_now_add=True, editable=False)
and for admin.py, I defined these.
class GroupAdminForm(forms.ModelForm):
users = forms.ModelMultipleChoiceField(
queryset=User.objects.all(),
required=False,
widget=FilteredSelectMultiple(
is_stacked=False,
),
)
class Meta:
model = Group
def __init__(self, *args, **kwargs):
super(GroupAdminForm, self).__init__(*args, **kwargs)
if self.instance.id:
self.fields['users'].initial = self.instance.users.all()
# Version 1 save()
def save(self, commit=True):
group = super(GroupAdminForm, self).save(commit=False)
if group.id:
group.users = self.cleaned_data['users']
if commit:
group.save()
group.save_m2m()
return group
# Version 2 save()
def save(self, commit=True):
group = super(GroupAdminForm, self).save(commit=False)
if group.id:
for user in self.cleaned_data['users']:
u = GroupUser(group=group, user=user)
u.save()
if commit:
group.save()
return group
class GroupAdmin(models.ModelAdmin):
...
form = GroupAdminForm
filter_horizontal = ['users']
...
The problem is no matter I use version 1 and 2 save() function, it returns an error:
Cannot set values on a ManyToManyField which specifies an intermediary model.
I found some similar questions on web but I still cannot get the ideas of how the to override the save() function. Any suggestions?
I know it may works if I do not define the intermediary model, but can do it in such way but still have the way to get the date_join field for each records?
Related
Currently, I'm having a problem when overriding a form field value on my (Django==4.0.3) django admin form.
The objective is :
I have a specific user table that I'm connecting to AWS Cognito. And when the admin creates a new user in django, the system must create a request to create a new Cognito user.
Once the Cognito user is created it generates a "sub" code, and then the sub should be saved in django
Code Follows
Model
class BuyerUser(BaseModel):
buyer = models.ForeignKey(
Buyer, on_delete=models.RESTRICT, related_name="%(class)s_buyer"
)
cognito_sub = models.CharField(max_length=50)
given_name = models.CharField(max_length=50)
family_name = models.CharField(max_length=50)
preferred_username = models.CharField(max_length=50)
email = models.EmailField(blank=False)
terms_conditions_accepted_datetime = models.DateTimeField(null=True, blank=True)
def __str__(self):
return self.preferred_username
admin
class BuyerUsers(admin.ModelAdmin):
list_display = ('id', 'buyer', 'given_name', 'family_name', 'preferred_username', 'available')
list_filter = ('buyer', 'available',)
list_display_links = ('id', 'preferred_username',)
search_fields = ('buyer__name', 'preferred_username', 'available')
list_per_page = 20
form = BuyerUserChangeForm
add_form = BuyerUserAddForm # It is not a native django field. I created this field and use it in get_form method.
def get_form(self, request, obj=None, **kwargs):
"""
Use special form during foo creation
"""
defaults = {}
if obj is None:
defaults['form'] = self.add_form
defaults.update(kwargs)
return super().get_form(request, obj, **defaults)
admin.site.register(BuyerUser, BuyerUsers)
and my forms
class BuyerUserAddForm(forms.ModelForm):
grupo = forms.CharField()
def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None,
initial=None, error_class=ErrorList, label_suffix=None,
empty_permitted=False, instance=None, use_required_attribute=None,
renderer=None):
super().__init__(data, files, auto_id, prefix, initial, error_class, label_suffix, empty_permitted, instance,
use_required_attribute, renderer)
self.cognito_sub = None
def save(self, commit=True):
grupo = self.cleaned_data.get('grupo', None)
self.given_name = self.cleaned_data.get('given_name', None)
self.family_name = self.cleaned_data.get('family_name', None)
self.preferred_username = self.cleaned_data.get('preferred_username', None)
self.email = self.cleaned_data.get('email', None)
cognito = CognitoDriver()
sub = cognito.parse_user(
cognito.create_user(self.preferred_username, self.email)["User"]
)["Sub"]
self.cognito_sub = sub
cognito.add_group(self.preferred_username, grupo)
return super(BuyerUserAddForm, self).save(commit=commit)
class Meta:
model = BuyerUser
# fields = '__all__'
exclude = ['terms_conditions_accepted_datetime']
class BuyerUserChangeForm(forms.ModelForm):
class Meta:
model = BuyerUser
fields = '__all__'elf.cognito_sub = sub
cognito.add_group(self.preferred_username, grupo)
return super(BuyerUserAddForm, self).save(commit=commit)
class Meta:
model = BuyerUser
# fields = '__all__'
exclude = ['terms_conditions_accepted_datetime']
class BuyerUserChangeForm(forms.ModelForm):
class Meta:
model = BuyerUser
fields = '__all__'
create
Change
This cognito sub field should have its value override after cognito-user is created. as it should be happening in the following code
cognito = CognitoDriver()
sub = cognito.parse_user(
cognito.create_user(self.preferred_username, self.email)["User"]
)["Sub"]
self.cognito_sub = sub
In fact, this cognito-user is being created and the sub is correct. the BIG PROBLEM is: this sub is not saved. It is getting only the value from the form.
I've tried to hide sub field using exclude = ['cognito_sub','terms_conditions_accepted_datetime']
but only happens to save a empty value.
You may ask why I use Forms instead of simply override model.Save() method
and the answer is: I need the grupo field, but this field must be persisted in DB. It only exists in Cognito.
You have to assign the value to form.instance instead of directly to the form itself.
class BuyerUserAddForm(forms.ModelForm):
grupo = forms.CharField()
# ...
def save(self, commit=True):
grupo = self.cleaned_data.get('grupo', None)
self.preferred_username = self.cleaned_data.get('preferred_username', None)
self.email = self.cleaned_data.get('email', None)
cognito = CognitoDriver()
sub = cognito.parse_user(
cognito.create_user(self.preferred_username, self.email)["User"])["Sub"]
self.instance.cognito_sub = sub
cognito.add_group(self.preferred_username, grupo)
return super(BuyerUserAddForm, self).save(commit=commit)
You might even want to disable the input field completly using the disabled attribute. https://docs.djangoproject.com/en/4.0/ref/forms/fields/#disabled
class BuyerUserAddForm(forms.ModelForm):
cognito_sub = forms.CharField(disabled=True)
# ...
I am quite new with Django and I need help.
My problem is quite similar what Mike had in his case:
UpdateView not populating form with existing data, but I have not found solution yet.
My goal is to view owner dropdown selection list only those users who are members of the organization.
models.py
# organizations.models.py
...
from accounts.models import User
from core.models import TimeStampModel
...
class Organization(TimeStampModel, models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
name = models.CharField(
verbose_name=_('Name'),
max_length=255,
unique=True
)
code = models.CharField(
verbose_name=_('Code'),
max_length=255,
null=True,
blank=True
)
owner = models.ForeignKey(
User,
on_delete=models.PROTECT,
verbose_name=_('Owner'),
related_name='owner',
help_text=_('Organization Owner and Contact Person'),
)
slug = models.SlugField(verbose_name=_('Organization key'), unique=True)
...
class Meta:
verbose_name = _('Organization')
verbose_name_plural = _('Organization')
ordering = ['name', 'code']
def __str__(self):
return f'{self.name}, {self.code}'
# Create automatically slug value from organization name field.
# In case similar is exist then add extra count digit end of slug.
def _get_unique_slug(self):
slug = slugify(self.name)
unique_slug = slug
num = 1
while Organization.objects.filter(slug=unique_slug).exists():
unique_slug = '{}-{}'.format(slug, num)
num += 1
return unique_slug
def save(self, *args, **kwargs):
if not self.slug:
self.slug = self._get_unique_slug()
self.next_update = timezone.now() + relativedelta(
months=self.update_interval)
super(Organization, self).save(*args, **kwargs)
def get_absolute_url(self):
kwargs = {
'slug': self.slug
}
return reverse('organization_main_page', kwargs=kwargs)
class OrganizationMembers(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
organization = models.ForeignKey(
Organization,
on_delete=models.CASCADE,
verbose_name=_('Organization')
)
member = models.ForeignKey(
User,
on_delete=models.CASCADE,
verbose_name=_('Member'),
null=True,
blank=True
)
organization_admin = models.BooleanField(
verbose_name=_('Organization admin'),
default=False
)
class Meta:
verbose_name = _('Organization: Member')
verbose_name_plural = _('Organization: Members')
ordering = ['organization', 'member']
unique_together = ('organization', 'member')
def __str__(self):
return f'{self.member}'
def get_absolute_url(self):
kwargs = {
'slug': self.slug
}
return reverse('organization_detail', kwargs=kwargs)
forms.py
# organizations.forms.py
....
from accounts.models import User
from .models import Organization, OrganizationMembers
...
class OrganizationUpdateForm(forms.ModelForm):
class Meta:
model = Organization
fields = '__all__'
exclude = ('date_created', 'created_by', 'created_by_id',
'last_updated', 'last_updated_by', 'last_updated_by_id',
'next_update', 'slug')
# Restrict user selection lists to view only members of the organization
def __init__(self, *args, **kwargs):
inst = kwargs.get('instance', None)
super(OrganizationUpdateForm, self).__init__(*args, **kwargs)
self.fields['owner'].queryset = OrganizationMembers.objects.\ # <--- !!!
filter(organization_id=inst.id)
In the forms.py, if I comment out self.field['owner]... line, then owner field will show saved value from database, but then I can see all users in the dropdown list. When queryset is enabled then selection list show correct users, but saved value is not visible.
views.py
# organizations.views.py
from .forms import OrganizationUpdateForm
from accounts.models import User
from .models import Organization, OrganizationMembers
class OrganizationUpdateView(LoginRequiredMixin, UpdateView):
model = Organization
form_class = OrganizationUpdateForm
template_name = 'organizations/organization_update.html'
success_url = reverse_lazy('organizations')
# Save data and set current user to last updated by fields
def form_valid(self, form):
object = form.save(commit=False)
object.last_updated_by = self.request.user.get_full_name()
object.last_updated_by_id = self.request.user
return super(OrganizationUpdateView, self).form_valid(form)
def get_queryset(self):
criteria1 = Q(owner=self.request.user)
criteria2 = Q(organizationmembers__member=self.request.user)
criteria3 = Q(organizationmembers__organization_admin=1)
org_list = Organization.objects.\
filter(criteria1 | (criteria2 & criteria3)).distinct()
if org_list.count() != 0:
return org_list
else:
raise Http404('You don\'t have permissions!')
In Mikes case Chiheb has commented that "With UpdateView it's a little bit tricky. So, in order to initialize your form's data, you need to do it in the view itself not in the form."
What is the reason that cannot add filter to UpdateView?
Please can someone help me to solve my problem. Thanks.
UPDATE
Not filtered. Value from database is visible
Not filtered. Dropdown list show all users in the system
Filter enabled. Value is not visible
Filter enabled. Dropdown list show correct valeus
The problem is that owner in your models is a FK to User model, but you are filtering queryset in form by OrganizationMembers. Make it the same and the problem should be gone.
Edit: Rewrote Description for clarity
I have a "CustomUser" class that I got from an all-auth tutorial (Django All-Auth Tutorial) and I have user as a foreign key in each model, which works as intended, only showing records pertaining to the current logged in user to that specific user.
For example:
EDUCATION MODEL (Works Correctly Here)
class Education(models.Model):
user = models.ForeignKey(CustomUser, on_delete=models.CASCADE)
EducationInstitutionName = models.CharField(verbose_name=_('Institution Name'), max_length=100, default=None)
EducationLevel = models.CharField(verbose_name=_('Education Level'), choices=EDUCATIONLEVEL, max_length=100, default=None)
EducationStartDate = models.DateField(verbose_name=_('Education Start Date'), default=None)
EducationEndDate = models.DateField(verbose_name=_('Education End Date'), default=None)
EducationCaoCode = models.CharField(choices=CAO_CODE, max_length=100, default=None)
EducationDesc = models.CharField(verbose_name=_('Education Description'), max_length=250, default=None)
def __str__(self):
return self.EducationInstitutionName
This works perfectly and I am achieving what is needed.
The issue arises when I have a table comprised of Foreign Keys which is the focal point of my application which takes the constituent parts of a CV and allows you to combine them to make a CV of interchangable sections.
CV MODEL (Only model I have which is composed of Foreign Keys)
class Cv(models.Model):
user = models.ForeignKey(
CustomUser,
on_delete=models.CASCADE,
)
CvName = models.CharField(verbose_name=_('CvName'), max_length=100, default=None)
CvEducation = models.ForeignKey(Education, on_delete=models.CASCADE)
CvSkills = models.ForeignKey(Skills, on_delete=models.CASCADE)
CvWorkExperience = models.ForeignKey(WorkExperience, on_delete=models.CASCADE)
def __str__(self):
return self.CvName
CV FORM
This is where I'm hitting the issue - I want to filter the fields to only show records of Education, Skills and Work Experience for the current user - at the moment the dropdown shows every record in existence for the Education, Skills and Work Experience models rather than those ONLY pertaining to the current logged in user.
class CvForm(forms.ModelForm):
class Meta:
model = Cv
fields = ('CvName', 'CvEducation', 'CvSkills', 'CvWorkExperience')
# TRYING TO FILTER BY CURRENT USER
def __init__(self, user, *args, **kwargs):
super(CvForm, self).__init__(*args, **kwargs)
print(user)
self.fields['CvEducation'].queryset = Education.objects.filter(user=user)
self.fields['CvSkills'].queryset = Education.objects.filter(user=user)
self.fields['CvWorkExperience'].queryset = Education.objects.filter(user=user)
def save(self, commit=True):
cv = super(CvForm, self).save(commit=False)
cv.user = self.request.user
cv.CvName = self.cleaned_data['CvName']
cv.CvEducation = self.cleaned_data['CvEducation']
cv.CvSkills = self.cleaned_data['CvSkills']
cv.CvWorkExperience = self.cleaned_data['CvWorkExperience']
cv.save()
return cv
CV LIST VIEW (This - correctly - only shows completed CV records associated with the current logged in user)
class CvList(ListView):
model = Cv
fields = ['CvName', 'CvEducation', 'CvSkills', 'CvWorkExperience']
success_url = reverse_lazy('Cv_list')
def get_queryset(self):
user_ids = CustomUser.objects.filter(username=self.request.user).values_list('id', flat=True)
if user_ids:
for uid in user_ids:
return Cv.objects.filter(user__id=uid)
else:
return Cv.objects.all()
CV CREATE VIEW
class CvCreate(CreateView):
model = Cv
fields = ['CvName', 'CvEducation', 'CvSkills', 'CvWorkExperience']
success_url = reverse_lazy('Cv_list')
def form_valid(self, form):
form.instance.user = self.request.user
return super(CvCreate, self).form_valid(form)
def get_queryset(self):
user_ids = CustomUser.objects.filter(username=self.request.user).values_list('id', flat=True)
if user_ids:
for uid in user_ids:
return Cv.objects.filter(user__id=uid)
else:
return Cv.objects.all()
This gives me...
Dropdown showing the whole Education table rather than filtering by Current User
In this image as I'm logged in as John Doe I should not be able to see the record - "James Edu" as that is associated with a different user James.
This occurs in all three of the fields I have with Foreign Keys - CvEducation, CvSkills and CvWorkExperience
Fixed eventually (in a very roundabout way):
FORMS INIT METHOD
def __init__(self, *args, **kwargs):
initial_arguments = kwargs.get('initial', None)
initial_arguments_list = list(initial_arguments.values())
user_id = initial_arguments_list[0]
super(CvForm, self).__init__(*args, **kwargs)
self.fields['CvEducation'].queryset = Education.objects.filter(user__id=user_id)
self.fields['CvSkills'].queryset = Skills.objects.filter(user__id=user_id)
self.fields['CvWorkExperience'].queryset = WorkExperience.objects.filter(user__id=user_id)
CREATE VIEW
class CvCreate(CreateView):
model = Cv
success_url = reverse_lazy('Cv_list')
form_class = CvForm
exclude = ['user']
def get_initial(self):
self.initial.update({ 'created_by': self.request.user.id })
return self.initial
Is there a way to update a unique field in update view?
I have a model that has a name and age field but when I try to update the age without even changing the value of the name, it returns an error that the name already exists in the database
models.py
class MyModel(models.Model)
name = models.CharField(max_length=200, unique=True)
age = models.IntegerField()
views.py
class MyModelUpdateView(UpdateView):
def get(self):
self.object = self.get_object()
my_model = self.object
form = MyModelForm(instance=my_model)
return self.render_to_response(
self.get_context_data(pk=my_model.pk, form=form)
)
def post(self, request, *args, **kwargs):
self.object = self.get_object()
my_model = self.object
form = MyModelForm(data=request.POST, instance=my_model)
if form.is_valid():
form.save()
return some_url
return self.render_to_response(
self.get_context_data(pk=my_model.pk, form=form)
)
forms.py
class MyModelForm(forms.ModelForm):
class Meta:
model = MyModel
fields = (
'name',
'age',
)
def clean(self):
cleaned_data = super().clean()
if MyModel.objects.filter(
active=True, name=cleaned_data.get('name')
).exists():
raise forms.ValidationError('MyModel already exists.')
return cleaned_data
What am I missing here? Thank you.
Since you update a model, and you do not change the name, of course a record with that name already exists: that specific record. You thus should alter the checking code, to:
class MyModelForm(forms.ModelForm):
def clean(self, *args, **kwargs):
cleaned_data = super().clean(*args, **kwargs)
if MyModel.objects.exclude(pk=self.instance.pk).filter(
active=True, name=cleaned_data.get('name')
).exists():
raise forms.ValidationError('MyModel already exists.')
return cleaned_data
class Meta:
model = MyModel
fields = ('name', 'age')
Please do not alter the boilerplate logic of the UpdateView, you can easily implement this with:
class MyModelUpdateView(UpdateView):
form_class = MyModelForm
success_url = 'some url'
That being said, since if you already have set the field to unique=True, then there is no need to implement the check yourself. It seems here, that you already have a unique=True constraint:
class MyModel(models.Model)
name = models.CharField(max_length=200, unique=True)
age = models.IntegerField()
In that case, you can simply let the ModelForm do the work, so then your form looks like:
class MyModelForm(forms.ModelForm):
class Meta:
model = MyModel
fields = ('name', 'age')
It is only if you want a more sophisticated uniqness (like with active=True?), and you can not represent it (easily) you should do your own validation.
I have a linked model:
class Children(models.Model):
person = models.ForeignKey(Person, on_delete=models.CASCADE)
child_name = models.CharField(max_length=150, null=True, blank=True)
slug = AutoSlugField(populate_from='child_name')
blood_group = models.CharField(max_length=5, blank=True)
class Meta:
unique_together = ('slug', 'person')
def get_absolute_url(self):
return self.person.get_absolute_url()
def get_delete_url(self):
return reverse(
'member:children-delete',
kwargs={
'person_slug': self.person.slug,
'children_slug': self.slug})
def get_update_url(self):
return reverse(
'member:children-update',
kwargs={
'person_slug': self.person.slug,
'children_slug': self.slug})
my forms.py:
class ChildrenForm( SlugCleanMixin, forms.ModelForm):
class Meta:
model = Children
exclude = ('person',)
def clean(self):
cleaned_data = super().clean()
slug = cleaned_data.get('slug')
person_obj = self.data.get('person')
exists = (
Children.objects.filter(
slug__iexact=slug,
person=person_obj,
).exists())
if exists:
raise ValidationError(
"Children with this Slug "
"and Person already exists.")
else:
return cleaned_data
def save(self, **kwargs):
instance = super().save(commit=False)
instance.person = (
self.data.get('person'))
instance.save()
self.save_m2m()
return instance
views.py:
class ChildrenCreate( ChildrenFormMixin, ChildrenGetObjectMixin,
PersonContextMixin,CreateView):
template_name = 'member/children_form.html'
model = Children
form_class = ChildrenForm
class ChildrenUpdate(ChildrenFormMixin, ChildrenGetObjectMixin,
PersonContextMixin,UpdateView):
template_name = 'member/children_form.html'
model = Children
form_class = ChildrenForm
slug_url_kwarg = 'children_slug'
class ChildrenDelete(ChildrenFormMixin,ChildrenGetObjectMixin,
PersonContextMixin,DeleteView):
model = Children
slug_url_kwarg = 'children_slug'
def get_success_url(self):
return (self.object.person
.get_absolute_url())
my utils.py:
class ChildrenFormMixin():
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
if self.request.method in ('POST', 'PUT'):
self.person = get_object_or_404(
Person,
slug__iexact=self.kwargs.get(
self.person_slug_url_kwarg))
data = kwargs['data'].copy()
data.update({'person': self.person})
kwargs['data'] = data
return kwargs
class ChildrenGetObjectMixin():
def get_object(self, queryset=None):
person_slug = self.kwargs.get(
self.person_slug_url_kwarg)
children_slug = self.kwargs.get(
self.slug_url_kwarg)
return get_object_or_404(
Children,
slug__iexact=children_slug,
person__slug__iexact=person_slug)
class PersonContextMixin():
person_slug_url_kwarg = 'person_slug'
person_context_object_name = 'person'
def get_context_data(self, **kwargs):
person_slug = self.kwargs.get(
self.person_slug_url_kwarg)
person = get_object_or_404(
Person, slug__iexact=person_slug)
context = {
self.person_context_object_name:
person,
}
context.update(kwargs)
return super().get_context_data(**context)
The children created more than one for same name of same parents. When I tried to edit children it gives "get() returned more than one Children -- it returned 2!" error. In traceback, it said, 'person__slug__iexact=person_slug' is the direct causes of this traceback.
In the form, I added clean method to catch the error and maintain uniqueness of children name of same parents but it not worked. Could I get suggestions where I do wrong?
Edit:
my Person model:
class Person(models.Model):
name = models.CharField(max_length=250)
slug = AutoSlugField(populate_from='name')
birth_date = models.DateField(null=True, blank=True)
blood_group = models.CharField(max_length=5)
present_address = models.CharField(max_length=250, blank=True)
permanent_address = models.CharField(max_length=250, blank=True)
user = models.OneToOneField(
settings.AUTH_USER_MODEL,
related_name='member_persons')
class Meta:
ordering = ['name']
unique_together = ['name', 'birth_date']
I believe you are using AutoSlugField from django-autoslug, and you trying to get by non-unique field. AutoSlugField won't make your field unique by default, from docs:
AutoSlugField can also perform the following tasks on save:
populate itself from another field (using populate_from),
use custom slugify function (using slugify or Settings), and
preserve uniqueness of the value (using unique or unique_with).
None of the tasks is mandatory, i.e. you can have auto-populated non-unique fields, manually entered unique ones (absolutely unique or within a given date) or both.
So quick fix would be slug = AutoSlugField(populate_from='child_name', unique=True)
UPDATE(Since you posted your Person model)
The problem is the same and solution is the same.
Explanation:
For example you have two Person objects:
id name slug birth_date
1 alex alex 10.10.2016
2 alex alex 10.10.2015
This won't violate unique_together = ['name', 'birth_date']
And you got two Children objects:
id name slug person_id
1 john john 1
2 john john 2
And that won't violate unique_together = ('slug', 'person') neither
Then you are making query
get_object_or_404(
Children,
slug__iexact='john',
person__slug__iexact='alex')
Which would match two objects. So you got problem. Quick fix would be to make slug unique=True.