I have a custom ModelForm class and ModelAdmin class and for some reason my save_model method will not execute.
Ill show both classes i currently have in admin.py. all functions (search, filter, hiding fields, delete) currently work correctly except for saving a new entry.
Here is my form:
class SeasonalityOriginalsForm(forms.ModelForm):
# defining the input fields that should be hidden
class Meta:
model = SeasonalitiesCalculated
fields = '__all__'
widgets = {
'has_been_reviewed': forms.HiddenInput(),
'user': forms.HiddenInput(),
'source_importance_0least_to_10most': forms.HiddenInput(),
'internal_id': forms.HiddenInput(),
}
### this defines a dropdown selector field for object_id
### instead of copy-pasting the UUID from another adminmodel page, which could bring errors
### admins can now select the country or admin_zone_1 with the right granularity by selecting location name
### an issue is that this is a choicefield, not a model choicefield.
### this issue is solved in the save_model method in the ModelAdmin class.
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
logger.debug(f'self model info: {type(self.fields)}')
countries = Countries.objects.filter(level_of_seasonal_area_granularity='country')
admin_zones1 = AdminZones1.objects.filter(country__level_of_seasonal_area_granularity='admin_1')
choices = [(obj.public_id, str(obj)) for obj in countries] + [(obj.public_id, str(obj)) for obj in admin_zones1]
self.fields['object_id'] = forms.ChoiceField(choices=[])
self.fields['object_id'].choices = choices
logger.debug(f'self model info2: {self.fields}')
### here are the visible input fields of the
object_id = forms.ChoiceField(choices=[])
content_type = forms.ModelChoiceField(
queryset=ContentType.objects.filter(model__in=['countries', 'adminzones1']),
widget=forms.Select(attrs={'class': 'form-control'})
)
seasonality = forms.DecimalField(min_value=Decimal('0'), max_value=Decimal('1'), decimal_places=1)
Here is my ModelAdmin class:
### define SeasonalityOriginalsAdmin for ContributorAdmin
class SeasonalityOriginalsAdmin(admin.ModelAdmin):
form = SeasonalityOriginalsForm
### modifying the search method for the searchfield
def get_search_results(self, request, queryset, search_term):
#... skipping this for now
### defining that existing fields are readonly
def get_readonly_fields(self, request, obj=None):
if obj: # editing an existing object
return [field.name for field in self.model._meta.fields]
return []
### not allowing delete permissions
def has_delete_permission(self, request, obj=None):
return False
### save method modification
def save_model(self, request, obj, form, commit=True):
logger.debug(f'tried several logs and print statements here but none show')
location_name = self.cleaned_data['object_id']
location = Countries.objects.filter(country_name=location_name).first() or AdminZones1.objects.filter(admin_zone_name=location_name).first()
self.object_id = location.pk
max_internal_id = self.model.objects.filter(internal_id__isnull=False).aggregate(Max('internal_id'))['internal_id__max']
obj.internal_id = max_internal_id + 1 if max_internal_id is not None else 1
if request.user.is_contributor:
obj.source_importance_0least_to_10most = 10
else:
obj.source_importance_0least_to_10most = 3
obj.user = request.user
return super().save(commit)
### defining the displayed rows, fields and search field
list_per_page = 20
list_display = ('ingredient_original', 'content_object','seasonality')
search_fields = ('seasonality_originals__public_id', 'ingredient_original__ingredient_original_name_en',)
ordering = ('ingredient_original',)
something must be super wrong with my save method because it wont even execute any logs that I place in the codeblock.
My model itself does not have any custom save method.
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)
# ...
this is my views.py :
i want save type in device field in model
class GetDeviceMixin( object):
def setup(self, request, *args, **kwargs):
super().setup( request, *args, **kwargs)
type= request.META['HTTP_USER_AGENT']
print(type)
return type
class RegisterView(GetDeviceMixin , generic.CreateView):
form_class = CustomUserCreationForm
success_url = reverse_lazy("register")
template_name = "man/man.html"
and this is my models.py
class account(AbstractBaseUser):
first_name= models.CharField(max_length=20,verbose_name="first name")
device = models.CharField(verbose_name="device" , max_length=100)
this is my forms.py:
class GetReq(forms.ModelForm):
class Meta:
model = account
fields = ['device',]
First, pop Classy CBVs onto your browser bookmark list ...
I'm assuming that you want to use a form, either to get other information from the user, or to allow the user to override the automatically determined value of device. In this case, you want to pass it as the initial value for device to the form
Now, look at CreateView to work out what to subclass. get_initial() looks hopeful, so
def get_initial(self):
initial = super().get_initial()
initial['device'] = self.device_type # as per the comment!
return initial
You should now see a form with the automatically determined value as the default value
If the intent was to get other fields of the model from the user and to always forcibly insert the automatically determined device_type, you would instead subclass form_valid
def form_valid(form):
obj = form.save( commit=False)
obj.device = self.device_type
obj.save()
I have a custom User class with a property that return a queryset
And I have an Admin class that use a custom ModelForm with 2 ModelChoiceField and fone BooleanFields.
I want to filter queryset of one ModelChoiceField using user property
but my problem is that I do not have access to request or user in my ModelForm.
I try to use method get_form_kwargs I use for 'normal CBV' but it doen't work as this method do not exist in ModelAdmin
admin.py
class User_TableAdmin(SimpleHistoryAdmin):
def __init__(self, model, admin_site):
super(User_TableAdmin,self).__init__(model,admin_site)
self.form.admin_site = admin_site # capture the admin_site
form = User_TableAdminForm **# How to request object to my form?**
list_display = ('id','user','table','can_download')
search_fields = ('user','table','can_download')
forms.py
class User_TableAdminForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(User_TableAdminForm, self).__init__(*args, **kwargs)
# add the 'green +' button to create a new user (green + button suppress when overidding field with ModelChoiceField)
self.fields['user'].widget = RelatedFieldWidgetWrapper(
self.fields['user'].widget,
self.instance._meta.get_field('user').remote_field,
admin_site)
class Meta:
model = User_Table
fields = '__all__'
# display only tables of study database that user workin: User property
# tables = self.user.can_download
tables = Table.objects.all() **#<- I would like to use something like request.user.can_download**
user = forms.ModelChoiceField(queryset = User.objects.all(), label = "User", widget = forms.Select())
table = forms.ModelChoiceField(queryset = tables, label = "Table", widget = forms.Select())
can_download = forms.BooleanField(
widget = forms.CheckboxInput(),
required = False,
)
models.py
class User(AbstractUser):
# site = models.ForeignKey(Site, on_delete = models.CASCADE, related_name="database")
birth_date = models.DateField(null=True, blank=True)
#property
def can_download(self):
""" Return the related list of tables use can download. """
return Table.objects.filter(
Q(database__study__in = [uss.study.id for uss in User_Site_Study.objects.filter(user = self.id)]) &
Q(database__study__is_opened = True) &
Q(database__is_opened = True)
)
can_download.fget.short_description = 'List of tables user allowed to download'
For that you can override ModelAdmin.get_form() which returns the ModelForm class that will be used in add or change admin page and decorate it to inject request upon creation of form instance.
class ModelFormWithRequest(forms.ModelForm):
def __init__(self, *args, **kwargs):
self.request = kwargs.pop('request')
super().__init__(*args, **kwargs)
#classmethod
def inject_request(cls, request):
def __new__(_, *args, **kwargs):
kwargs.setdefault('request', request)
return cls(*args, **kwargs)
return type(
f'{cls.__name__}Decorator',
(cls,),
{
'__module__': cls.__module__,
'__doc__': cls.__doc__,
'__new__': __new__
}
)
class User_TableAdminForm(ModelFormWithRequest):
def __init__(self, *args, **kwargs):
super(ModelFormWithRequest, self).__init__(*args, **kwargs)
self.fields['table'].queryset = # set new queryset filtered with self.request.user data
# add the 'green +' button to create a new user (green + button suppress when overidding field with ModelChoiceField)
self.fields['user'].widget = RelatedFieldWidgetWrapper(
self.fields['user'].widget,
self.instance._meta.get_field('user').remote_field,
admin_site)
class User_TableAdmin(SimpleHistoryAdmin):
form = User_TableAdminForm
list_display = ('id','user','table','can_download')
search_fields = ('user','table','can_download')
def __init__(self, model, admin_site):
super(User_TableAdmin,self).__init__(model,admin_site)
self.form.admin_site = admin_site # capture the admin_site
# this is how you pass request to form
def get_form(self, request, obj=None, change=False, **kwargs):
ModelForm = super().get_form(request, obj=obj, change=change, **kwargs))
# pass request only to change form, it assumes this is a subclass of auth.UserAdmin
if change:
return ModelForm.inject_request(request)
return ModelForm
just to extend answer, if one needs to pass request to InlineModelAdmin's forms, which is handled by formsets, need to override get_formset() instead of get_form()
class CustomInlineModelAdmin(admin.InlineModelAdmin):
def get_formset(self, request, obj=None, **kwargs):
formset_class = super().get_formset(request, obj=obj, **kwargs)
formset_class.form = formset_class.form.inject_request(request)
return formset_class
There is a shorter solution, just set ModelForm.request = request in ModelAdmin.get_form(). In this case ModelFormWithRequest is not needed, but I prefer to pass dynamic dependencies though constructor instead of appending it to class objects which are global.
I'm using Django 2.1 and PostgreSQL.
My problem is that I'm trying to create a form to edit two different models at the same time. This models are related with a FK, and every example that I see is with the user and profile models, but with that I can't replicate what I really need.
My models simplified to show the related information about them are:
# base model for Campaigns.
class CampaignBase(models.Model):
....
project = models.ForeignKey(Project, on_delete=models.CASCADE)
creation_date = models.DateTimeField(auto_now_add=True)
start_date = models.DateTimeField(null=True, blank=True)
end_date = models.DateTimeField(null=True, blank=True)
....
# define investment campaign made on a project.
class InvestmentCampaign(models.Model):
....
campaign = models.ForeignKey(CampaignBase, on_delete=models.CASCADE, null=True, blank=True)
description = models.CharField(
blank=True,
max_length=25000,
)
....
And the form that I want to create is one that includes the end_date of the FK CampaignBase, and the Description from the InvestmentCampaign.
Now I have this UpdateView to edit the InvestmentCampaign, and I need to adapt to my actual needs, that are also update the CampaignBase model:
class ProjectEditInvestmentCampaignView(LoginRequiredMixin, SuccessMessageMixin, generic.UpdateView):
template_name = 'webplatform/project_edit_investment_campaign.html'
model = InvestmentCampaign
form_class = CreateInvestmentCampaignForm
success_message = 'Investment campaign updated!'
def get_success_url(self):
return reverse_lazy('project-update-investment-campaign', args=(self.kwargs['project'], self.kwargs['pk']))
# Make the view only available for the users with current fields
def dispatch(self, request, *args, **kwargs):
self.object = self.get_object()
# here you can make your custom validation for any particular user
if request.user != self.object.campaign.project.user:
raise PermissionDenied()
return super().dispatch(request, *args, **kwargs)
# Set field as current user
def form_valid(self, form):
campaign = InvestmentCampaign.objects.get(pk=self.kwargs['campaign'])
form.instance.campaign = campaign
form.instance.history_change_reason = 'Investment campaign updated'
return super(ProjectEditInvestmentCampaignView, self).form_valid(form)
def get_context_data(self, **kwargs):
project = Project.objects.get(pk=self.kwargs['project'])
context = super(ProjectEditInvestmentCampaignView, self).get_context_data(**kwargs)
context['project'] = project
return context
My forms are:
class CreateCampaignBaseForm(forms.ModelForm):
class Meta:
model = CampaignBase
fields = ('end_date',)
widgets = {
'end_date': DateTimePickerInput(),
}
def __init__(self, *args, **kwargs):
# first call parent's constructor
super(CreateCampaignBaseForm, self).__init__(*args, **kwargs)
# evade all labels and help text to appear when using "as_crispy_tag"
self.helper = FormHelper(self)
self.helper.form_show_labels = False
self.helper._help_text_inline = True
class CreateInvestmentCampaignForm(forms.ModelForm):
class Meta:
model = InvestmentCampaign
fields = ('description')
widgets = {
'description': SummernoteWidget(attrs={'summernote': {
'placeholder': 'Add some details of the Investment Campaign here...'}}),
}
def __init__(self, *args, **kwargs):
# first call parent's constructor
super(CreateInvestmentCampaignForm, self).__init__(*args, **kwargs)
# evade all labels and help text to appear when using "as_crispy_tag"
self.helper = FormHelper(self)
self.helper.form_show_labels = False
self.helper._help_text_inline = True
I've read everywhere that the best way of doing this is using function based views, and call each of the forms that I have and then do the validation. the thing is that I don't know how can I populate the fields with the right object in both forms, and also, I don't know how to do the equivalent of the get_context_data nor getting the self arguments to do the equivalent of the get_success_url (because with function based views I only have the request attr so I can't access the kwargs).
I've seen some people using the django-betterforms, but again, the only examples are with the auth and profile models and I don't see the way to replicate that with my own models.
Thank you very much.
If the only thing you want to change is one field end_date on BaseCampaign, then you should use just one form. Just add end_date as an additional field (e.g. forms.DateTimeField()) on your CreateInvestmentCampaignForm and in your form.valid() method, after saving the form, set the value on the associated campaign:
def form_valid(self, form):
inv_campaign = form.save(commit=False)
inv_campaign.campaign.end_date = form.cleaned_data['end_date']
inv_campaign.campaign.save()
inv_campaign.history_change_reason = ...
return super().form_valid(form)
Here's how to add end_date to your form and initialize it correctly:
class CreateInvestmentCampaignForm(ModelForm):
end_date = forms.DateTimeField(blank=True)
class Meta:
model = InvestmentCampaign
fields = ('description')
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if self.instance.campaign:
self.fields['end_date'].initial = self.instance.campaign.end_date
Based on the conversation on the answer of #dirkgroten, I've developed what worked for me and what I'm actually using, but I market his answer as correct because his code is also functional.
So, meanwhile he is initiating the values on the form, I'm using the view to do that by adding a def get_initial(self): and also adding the validation on the def form_valid(self, form)::
On the view:
...
def get_initial(self):
"""
Returns the initial data to use for forms on this view.
"""
initial = super(ProjectEditInvestmentCampaignView, self).get_initial()
initial['end_date'] = self.object.campaign.end_date
return initial
...
# Set field as current user
def form_valid(self, form):
form.instance.history_change_reason = 'Investment campaign updated'
is_valid = super(ProjectEditInvestmentCampaignView, self).form_valid(form)
if is_valid:
# the base campaign fields
campaign = form.instance.campaign
campaign.end_date = form.cleaned_data.get("end_date")
campaign.save()
return is_valid
And on the form I just added the end_date field:
class CreateInvestmentCampaignForm(forms.ModelForm):
end_date = forms.DateTimeField()
class Meta:
model = InvestmentCampaign
fields = ('description',)
widgets = {
'description': SummernoteWidget(attrs={'summernote': {
'placeholder': 'Add some details of the Investment Campaign here...'}}),
'end_date': DateTimePickerInput(), # format='%d/%m/%Y %H:%M')
}
def __init__(self, *args, **kwargs):
# first call parent's constructor
super(CreateInvestmentCampaignForm, self).__init__(*args, **kwargs)
# evade all labels and help text to appear when using "as_crispy_tag"
self.helper = FormHelper(self)
self.helper.form_show_labels = False
self.helper._help_text_inline = True
note : This is closely related to the answer in this question :
django admin - add custom form fields that are not part of the model
In Django it is possible to create custom ModelForms that have "rouge" fields that don't pertain to a specific database field in any model.
In the following code example there is a custom field that called 'extra_field'. It appears in the admin page for it's model instance and it can be accessed in the save method but there does not appear to be a 'load' method.
How do I load the 'extra_field' with data before the admin page loads?
# admin.py
class YourModelForm(forms.ModelForm):
extra_field = forms.CharField()
def load(..., obj):
# This method doesn't exist.
# extra_field = obj.id * random()
def save(self, commit=True):
extra_field = self.cleaned_data.get('extra_field', None)
return super(YourModelForm, self).save(commit=commit)
class Meta:
model = YourModel
class YourModelAdmin(admin.ModelAdmin):
form = YourModelForm
fieldsets = (
(None, {
'fields': ('name', 'description', 'extra_field',),
}),
)
source code by #vishnu
Override the form's __init__ method and set the initial property of the field:
class YourModelForm(forms.ModelForm):
extra_field = forms.CharField()
def __init__(self, *args, **kwargs):
super(YourModelForm, self).__init__(*args, **kwargs)
initial = '%s*rnd' % self.instance.pk if self.instance.pk else 'new'
self.fields['extra_field'].initial = initial