Django - Multiple custom models on the same form - python

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

Related

Django ModelAdmin save method not executing

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.

Custom django inlineformset validation based on user permissions

The objective is to have a simple workflow where an order and associated orderlines (created in a previous step) needs to be approved by the relevant budget holder. The approval form shows all order lines but disables those lines that the current user is not associated with (they should be able to see the overall order but only be able to edit lines that they are permitted to). They should be able to add new lines if necessary. The user needs to decide whether to approve or not (approval radio cannot be blank)
The initial form presents correctly and is able to save inputs correctly when all values are inputted correctly - however, if it fails validation then the incorrect fields get highlighted and their values are cleared.
models.py
class Order(models.Model):
department = models.ForeignKey(user_models.Department, on_delete=models.CASCADE)
location = models.ForeignKey(location_models.Location, on_delete=models.CASCADE, null=True)
description = models.CharField(max_length=30)
project = models.ForeignKey(project_models.Project, on_delete=models.CASCADE)
product = models.ManyToManyField(catalogue_models.Product, through='OrderLine', related_name='orderlines')
total = models.DecimalField(max_digits=20, decimal_places=2, null=True, blank=True)
def __str__(self):
return self.description
class OrderLine(models.Model):
order = models.ForeignKey(Order, on_delete=models.CASCADE)
project_line = models.ForeignKey(project_models.ProjectLine, on_delete=models.SET_NULL, null=True, blank=False)
product = models.ForeignKey(catalogue_models.Product, on_delete=models.CASCADE)
quantity = models.PositiveIntegerField()
price = models.DecimalField(max_digits=20, decimal_places=4)
total = models.DecimalField(max_digits=20, decimal_places=2)
budgetholder_approved = models.BooleanField(null=True)
def get_line_total(self):
total = self.quantity * self.price
return total
def save(self, *args, **kwargs):
self.total = self.get_line_total()
super(OrderLine, self).save(*args, **kwargs)
def __str__(self):
return self.product.name
views.py
class BudgetApprovalView(FlowMixin, generic.UpdateView):
form_class = forms.BudgetHolderApproval
def get_object(self):
return self.activation.process.order
def get_context_data(self, **kwargs):
data = super(BudgetApprovalView, self).get_context_data(**kwargs)
if self.request.POST:
data['formset'] = forms.OrderLineFormet(self.request.POST, instance=self.object)
else:
data['formset'] = forms.OrderLineFormet(instance=self.activation.process.order, form_kwargs={'user': self.request.user})
return data
def post(self, request, *args, **kwargs):
self.object = None
form_class = self.get_form_class()
form = self.get_form(form_class)
form = forms.BudgetHolderApproval(self.request.POST, instance=self.activation.process.order)
formset = forms.OrderLineFormet(self.request.POST, instance=self.activation.process.order)
if form.is_valid() and formset.is_valid():
return self.is_valid(form, formset)
else:
return self.is_invalid(form, formset)
def is_valid(self, form, formset):
self.object = form.save(commit=False)
self.object.created_by = self.request.user
self.activation.process.order = self.object
with transaction.atomic():
self.object.save()
self.activation.done()
formset.save()
return HttpResponseRedirect(self.get_success_url())
def is_invalid(self, form, formset):
return self.render_to_response(self.get_context_data(form=form, formset=formset))
I have tried a couple of things to figure this out - without success:
to override the clean() method of the ModelForm - however, I cannot figure out how to determine if the submitted form is disabled or not.
forms.py
class OrderForm(forms.ModelForm):
class Meta:
model = models.Order
fields = ['description', 'project', 'location']
def __init__(self, *args, **kwargs):
super(OrderForm, self).__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.form_tag = False
class OrderLine(forms.ModelForm):
class Meta:
model = models.OrderLine
exclude = ['viewflow']
def __init__(self, *args, **kwargs):
YES_OR_NO = (
(True, 'Yes'),
(False, 'No')
)
self.user = kwargs.pop('user', None)
super(OrderLine, self).__init__(*args, **kwargs)
self.fields['project_line'].queryset = project_models.ProjectLine.objects.none()
self.fields['budgetholder_approved'].widget = forms.RadioSelect(choices=YES_OR_NO)
if self.instance.pk:
self.fields['budgetholder_approved'].required = True
self.fields['order'].disabled = True
self.fields['project_line'].disabled = True
self.fields['product'].disabled = True
self.fields['quantity'].disabled = True
self.fields['price'].disabled = True
self.fields['total'].disabled = True
self.fields['budgetholder_approved'].disabled = True
if 'project' in self.data:
try:
project_id = int(self.data.get('project'))
self.fields['project_line'].queryset = project_models.ProjectLine.objects.filter(project_id=project_id)
except (ValueError, TypeError):
pass
elif self.instance.pk:
self.fields['project_line'].queryset = self.instance.order.project.projectline_set
project_line_id = int(self.instance.project_line.budget_holder.id)
user_id = int(self.user.id)
if project_line_id == user_id:
self.fields['budgetholder_approved'].disabled = False
self.helper = FormHelper()
self.helper.template = 'crispy_forms/templates/bootstrap4/table_inline_formset.html'
self.helper.form_tag = False
def clean(self):
super(OrderLine, self).clean()
pprint(vars(self.instance))
//This just returns a list of fields without any attributes to apply the validation logic
OrderLineFormet = forms.inlineformset_factory(
parent_model=models.Order,
model=models.OrderLine,
form=OrderLine,
extra=2,
min_num=1
)
to override the clean() method of the BaseInlineFormSet - however, I cannot disable the fields in the init or any of the validation rules (it silently fails validation and presents a blank inlineformset on failure - it never gets to clean() method.
forms.py
class OrderForm(forms.ModelForm):
class Meta:
model = models.Order
fields = ['description', 'project', 'location']
def __init__(self, *args, **kwargs):
super(TestOrderForm, self).__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.form_tag = False
class BaseTestOrderLine(forms.BaseInlineFormSet):
def __init__(self, user, *args, **kwargs):
self.user = user
super(BaseTestOrderLine, self).__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.template = 'crispy_forms/templates/bootstrap4/table_inline_formset.html'
self.helper.form_tag = False
// Never gets to the clean method as is_valid fails silently
def clean(self):
super(BaseTestOrderLine, self).clean()
if any(self.errors):
pprint(vars(self.errors))
return
OrderLineFormet = forms.inlineformset_factory(
parent_model=models.Order,
model=models.OrderLine,
formset=BaseTestOrderLine,
exclude=['order'],
extra=2,
min_num=1
)
Edit - reflecting progress based on Dao's suggestion (the form reloads correctly with the validation errors showing correctly)
The only remaining problem is that when the form reloads - the field (budgetholder_approved) that should still be enabled is disabled. One of the two approval checkbox lines lines should be editable
Looks like because you have different formset context data on submit invalid
if self.request.POST:
data['formset'] = forms.OrderLineFormet(self.request.POST, instance=self.activation.process.order, form_kwargs={'user': self.request.user})
else:
data['formset'] = forms.OrderLineFormet(instance=self.activation.process.order, form_kwargs={'user': self.request.user})
return data
Edit for updated Q:
Didn't test this out because of time constraint but because you already initiated the field and overwrite the widget, so if you need to update disabled attr of widget instead of field.
self.fields['budgetholder_approved'].widget = forms.RadioSelect(choices=YES_OR_NO)
self.fields['budgetholder_approved'].widget.attrs['disabled'] = False

Django rest framework, update object after creation

I have a DRF API that takes in the following model:
class Points(models.Model):
mission_name = models.CharField(name='MissionName',
unique=True,
max_length=255,
blank=False,
help_text="Enter the mission's name"
)
# Some irrlevant feid
url = models.URLField(help_text='Leave Empty!', default=" ")
date_added = models.DateTimeField(default=timezone.now)
class Meta:
get_latest_by = 'date_added'
And it's serializer:
from rest_framework.serializers import HyperlinkedModelSerializer
from .models import Points
class PointsSerializer(HyperlinkedModelSerializer):
class Meta:
model = Points
fields = (
'id', 'MissionName', 'GDT1Latitude', 'GDT1Longitude',
'UavLatitude', 'UavLongitude', 'UavElevation', 'Area',
'url', 'date_added'
)
And the view:
class PointsViewSet(ModelViewSet):
# Return all order by id, reversed.
queryset = Points.objects.all().order_by('-id')
serializer_class = PointsSerializer
data = queryset[0]
serialized_data = PointsSerializer(data, many=False)
points = list(serialized_data.data.values())
def retrieve(self, request, *args, **kwargs):
print(self.data)
mission_name = self.points[1]
assign_gdt = GeoPoint(lat=self.points[2], long=self.points[3])
gdt1 = [assign_gdt.get_lat(), assign_gdt.get_long()]
assign_uav = GeoPoint(lat=self.points[4], long=self.points[5], elevation=self.points[6])
uav = [assign_uav.get_lat(), assign_uav.get_long(), assign_uav.get_elevation()]
area_name = f"'{self.points[-2]}'"
main = MainApp.run(gdt1=gdt1, uav=uav, mission_name=mission_name, area=area_name)
print('file created')
return render(request, main)
I want to update the URL field of the file to contain a constant pattern and format in the end the mission_name field.
object.url = f'127.0.0.1/twosecondgdt/{mission_name}'
How can that be achieved and where should I store such code, the views.py or serializers.py?
There are several ways this could be achieved based on your requirements.
If you want to set the url upon creation even if it is not through the api, you can do it in the save method of the model itself:
class Points(models.Model):
# fields here
def save(self, **args, **kwargs):
if not self.url.strip():
# You may want to store the value of `127...` in an environment variable
self.url = f"127.0.0.1/twosecondgdt/{self.mission_name}"
super().save(*args, **kwargs)
If you want to set it through the view/serializer, you can set it in the create method of your serializer:
class PointsSerializer(HyperlinkedModelSerializer):
def create(self, validated_data):
mission_name = validated_data["mission_name"]
validated_data["url"] = f"127.0.0.1/twosecondgdt/{mission_name}"
return super().create(validated_data)
You can also override some methods in your viewset like perform_create or create

Get latest saved object values in Django admin panel

Many toys have same information(for example description and price) how can i customise my admin.py in order to get last saved object values, everytime when i press add new toy in admin panel and display them in my input fields.
models.py
class Toy(models.Model):
name = models.CharField(max_length=255)
description = TextField()
quantity = models.FloatField(default=0.0)
price = models.FloatField()
def __unicode__(self):
return self.name
admin.py
admin.site.register(Toy)
Try to do it like that:
First create a form in your admin.py:
class ToyForm(forms.ModelForm):
class Meta:
model = Toy
fields = ('__all__')
def __init__(self, *args, **kwargs):
#if not an edit
if 'instance' not in kwargs:
#we get the last object
last_object = Toy.objects.all().order_by("id")[0]
#we add the last object informations to the initial data
initial = {'description': last_object.description, 'price': last_object.price}
kwargs['initial'] = initial
super(ToyForm, self).__init__(*args, **kwargs)
Then add this form to the Admin model:
class ToyAdmin(admin.ModelAdmin):
form = ToyForm
Fianally:
admin.site.register(ToyAdmin, Toy)
In modeladmin class there is a method get_form, you can override the method to return, the values returned from you conditioned queryset results ( Mymodel.objects.last() )
from django.contrib import admin
#admin.register(Toy)
class ToyAdmin(admin.ModelAdmin):
def get_form(self, request, obj=None, **kwargs):
form = super(ToyAdmin, self).get_form(request, obj, **kwargs)
# code logic to check condition & queryset like Toy.objects.last()
form.base_fields['name'].initial = 'value'
return form

How to assign the User object to save method in Django

I am trying to log the activities during save operation to track all the changes to user model. my approach is as follows.
class User(AbstractUser):
undergrad_college = models.CharField(max_length=20, choices=COLLEGE_CHOICES)
undergrad_degree = models.CharField(max_length=20, choices=COLLEGE_DEGREES)
postgrad_college = models.CharField(max_length=20, choices=COLLEGE_CHOICES)
postgrad_degree = models.CharField(max_length=20, choices=COLLEGE_DEGREES)
currently_working_on = models.TextField()
previous_work_experience = models.TextField()
previous_internship_experience = models.TextField()
def __str__(self):
return self.username
def save(self, *args, **kwargs):
Log(user=User, actions="Updated profile",
extra={"undergrad_college": self.undergrad_college,
"undergrad_degree": self.undergrad_degree,
"postgrad_college": self.postgrad_college,
"postgrad_degree": self.postgrad_degree,
"currently_working_on": self.currently_working_on,
"previous_work_experience": self.previous_work_experience,
"previous_internship_experience": self.previous_internship_experience
})
super(User, self).save(args, **kwargs)
my views are like this for handling the logging.
class ActivityMixin(LoginRequiredMixin):
def get_context_data(self, **kwargs):
context = super(ActivityMixin, self).get_context_data(**kwargs)
context['activities'] = Log.objects.filter(user=self.request.user)
return context
class IndexListView(ActivityMixin, ListView):
template_name = 'pages/home.html'
model = User
I get this error while performing the update action.
Cannot assign "<class 'users.models.User'>": "Log.user" must be a "User" instance.
Update view is as follows
class UserUpdateView(LoginRequiredMixin, UpdateView):
form_class = UserForm
# we already imported User in the view code above, remember?
model = User
# send the user back to their own page after a successful update
def get_success_url(self):
return reverse("users:detail",
kwargs={"username": self.request.user.username})
def get_object(self, **kwargs):
# Only get the User record for the user making the request
return User.objects.get(username=self.request.user.username)
How to assign the User model instance to the Log function. I cant get this working. I am Django newbie.
Looks like pretty straightforward, replace User with self:
Log(user=User, ...
Log(user=self, ...

Categories

Resources