Massage model data before save in Django - python

I'm not sure if this is the best way to do this, but I have some data that is being sent by a form. I have a ModelForm that takes the the request.POST of that form data. All the data that is being sent is a description, amount and deposit (boolean).
When the person submits the data, the amount will be a positive number, but I would like to store it in the database as a negative number if deposit is False.
I was thinking of doing this in either the Model or the ModelForm and sort of massage that amount before saving... So, somewhere in one of those classes, I'd like to have something like:
if not deposit:
amount = -amount
... and then save it as such.
Is there a way to handle this in the ModelForm or Model that would keep me from having to do all that logic inside of the view?

ModelForm's save() method is a good place for this:
class MyForm(models.ModelForm):
...
def save(self):
instance = super(MyForm, self).save(commit=False)
if not self.deposit:
self.amount = -self.amount
instance.save()
return instance

Overwrite model save method is a solution. But I prefear make this operations in clean method and mix it with business rules:
models.py:
from django.db import models
class Issue(models.Model):
....
def clean(self):
rules.Issue_clean(self)
from issues import rules
rules.connect()
rules.py:
from issues.models import Issue
def connect():
from django.db.models.signals import post_save, pre_save, pre_delete
#issues
pre_delete.connect(Issue_pre_delete, sender= Incidencia)
pre_save.connect(Issue_pre_save, sender = Incidencia )
post_save.connect(Issue_post_save, sender = Incidencia )
def Incidencia_clean( instance ):
#pre save:
if not instance.deposit:
instance.amount *= -1
#business rules:
errors = {}
#dia i hora sempre informats
if not instance.account.enoughCredit:
errors.append( 'No enough money.' )
if len( errors ) > 0:
raise ValidationError(errors)
def Issue_pre_save(sender, instance, **kwargs):
instance.clean()
At this way rules are binded to model and you don't need to write code on each form that this model appears (here, you can see this on more detail)

Related

Django Formset - each form with different initial value from M2M-through relationship

I have to models which are connected by a M2M-Field realized by another Class ComponentInModule, so that I can add there the extra information, how often a component is in the module.
class Module(models.Model):
...
component = models.ManyToManyField(Component, through="ComponentInModule")
class Component(models.Model):
...
class ComponentInModule(models.Model):
module = models.ForeignKey(InfrastructureModule, on_delete=models.CASCADE)
component = models.ForeignKey(InfrastructureComponent, on_delete=models.CASCADE)
amount = models.IntegerField(default=1)
Now I am trying to load a Module as a form with its corresponding Components as a formset.
class ComponentForm(ModelForm):
amount = IntegerField()
module = InfrastructureModule.objects.get(id=x)
ComponentFormSet = modelformset_factory(Component, form=ComponentForm, extra=0)
component_formset = ComponentFormSet(queryset=module.get_components())
As you can see my ComponentForm has the extra field for the amount. The question now is, how can I pass the value of amount to the Formset on creation, so that all forms are initialized with the right value? With a single Form it's no problem, because I can just pass the value to the __init__ function of the form and put it into the amount field self.fields["amount"].initial = amount. I tried passing a list of values to the formset with form_kwargs, but then I got the problem, that in the __init__function I dont know which of the values in the list is the right one right now.
Is there any way to do this using formsets? Or is there some other option I am missing how you can include the extra fields from a M2M-relation in a ModelForm?
So I worked it out. I made a custom BaseModelFormSet class:
class BaseCompFormset(BaseModelFormSet):
def get_form_kwargs(self, index):
kwargs = super().get_form_kwargs(index)
amount = kwargs["amount"][index]
return {"amount": amount}
Adjusted the __init__ function of the form:
def __init__(self, *args, **kwargs):
amount = kwargs.pop("amount")
super(ComponentForm, self).__init__(*args, **kwargs)
if self.instance:
self.fields["amount"].initial = amount
And used those to create my modelformset_factory:
amounts = [x.amount for x in module.get_components_in_module()]
ComponentFormSet = modelformset_factory(Component, formset=BaseCompFormset, form=ComponentForm, extra=0)
component_formset = ComponentFormSet(queryset=module.get_components(), form_kwargs={'amount':amounts})
And now succesfully got the forms of the formset with the right initial value for amount!

In django how do I add another instance to an inline model before saving it?

I have a parent model with inline details model. While saving the inline model details, how do I add a new detail record before saving the entire record i.e parent and its details? Example: models.py:
class Voucher(models.Model):
date = models.DateField(default=datetime.datetime.now().date())
number = models.CharField(max_length=20,null=True,blank=True)
class VoucherDetail(models.Model):
voucher = models.ForeignKey(Voucher,blank=True,null=True)
amount = models.DecimalField(decimal_places=2,max_digits=9,default=0,
blank=True,null=True)
narration = models.CharField(max_length=100,null=True,blank=True)
total_amount = models.DecimalField(decimal_places=2,max_digits=9,default=0,
blank=True,null=True)
Before saving a Voucher, I would like to add the amount totals of the formset and create another entry in voucher detail
with some narration and the total amount
I need a complete code sample for the same. Using django 1.6.5, Thx in advance
Use below method in parent model admin and do the necessary calculation to achieve the same:
def save_model(self, request, obj, form, change):
for d in obj.voucherdetail_set.all():
if d.value:
amt_exists = True
pass
else:
amt_exists =False
if amt_exists is False:
try:
dr_detail = VoucherDetail()
dr_detail.voucher = Voucher.objects.get(pk=obj.pk)
dr_detail.narration = "Client remarks"
dr_detail......
dr_detail.save()
except Exception,e:
print e
obj.save()

Validating number of ManyToManyField items (with intermediate model) in Django admin

I have some Django models that look something like this (this isn't my exact code but is a simpler example that has the same structure):
class Player(models.Model):
# Some fields here.
pass
class Team(models.Model):
players = models.ManyToManyField(Player, through='TeamPlayer')
class TeamPlayer(models.Model):
team = models.ForeignKey(Team)
player = models.ForeignKey(Player)
some_other_field = models.BooleanField()
I'm using the through mechanism because I have extra columns on my link table.
My admin classes look something like this (note that I am using an inline admin to add the players):
class TeamPlayerInline(admin.TabularInline):
model = TeamPlayer
max_num = 11
extra = 11
class TeamAdmin(admin.ModelAdmin):
inlines = [TeamPlayerInline]
admin.site.register(Team, TeamAdmin)
The Question: My problem is that in my admin I would like to validate that a team has exactly 11 players. Any fewer should result in an error. How can I do this?
These are the things that I have tried and the reasons why they didn't work:
Validate the number of players in the clean method of the Team model. This doesn't work because the players haven't been saved yet, so for a new object there are always zero players.
Validate the number in the clean_players method of a ModelForm used by the TeamAdmin. This method never gets called. Similar methods for other non-ManyToMany fields do get called.
Validate the number in the clean method of the aforementioned ModelForm. This method gets called but the self.cleaned_data dictionary does not have an entry for 'players'.
Any ideas how I can achieve this type of validation? I'm far from being a Django expert so don't assume that I've necessarily done everything that should be obvious.
You need to set the formset on the TeamPlayerInline. And override the clean method in that form set. For example:
from django.forms.models import BaseInlineFormSet
class TeamPlayerFormset(BaseInlineFormSet):
def clean(self):
"""Check that exactly 11 players are entered."""
super(TeamPlayerFormset, self).clean()
if any(self.errors):
return
count = 0
for cleaned_data in self.cleaned_data:
if cleaned_data and not cleaned_data.get('DELETE', False):
count += 1
if count != 11:
raise forms.ValidationError('You must enter 11 team players.')
class TeamPlayerInline(admin.TabularInline):
model = TeamPlayer
max_num = 11
extra = 11
formset = TeamPlayerFormset
class TeamAdmin(admin.ModelAdmin):
inlines = [TeamPlayerInline]

django: exclude certain form elements based on a condition

I have some form fields that I want to include/exclude based on whether or not a certain condition is met. I know how to include and exclude form elements, but I am having difficulty doing it when I want it elements to show based on the outcome of a function.
Here is my form:
class ProfileForm(ModelForm):
# this_team = get Team instance from team.id passed in
# how?
def draft_unlocked(self):
teams = Team.objects.order_by('total_points')
count = 0
for team in teams:
if team.pk == this_team.pk:
break
count += 1
now = datetime.datetime.now().weekday()
if now >= count:
# show driver_one, driver_two, driver_three
else:
# do not show driver_one, driver_two, driver_three
class Meta:
model = Team
What I am trying to accomplish is, based on the standings of total points, a team should not be able to change their driver until their specified day. As in, the last team in the standings can add/drop a driver on Monday, second to last team can add/drop on Tuesday, and so on...
So the first problem -- how do I get the Team instance inside the form itself from the id that was passed in. And, how do I include/exclude based on the result of draft_unlocked().
Or perhaps there is a better way to do all of this?
Thanks a lot everyone.
This is actually fairly straightforward (conditional field settings) - here's a quick example:
from django.forms import Modelform
from django.forms.widgets import HiddenInput
class SomeForm(ModelForm):
def __init__(self, *args, **kwargs):
# call constructor to set up the fields. If you don't do this
# first you can't modify fields.
super(SomeForm, self).__init__(*args, **kwargs)
try:
# make somefunc return something True
# if you can change the driver.
# might make sense in a model?
can_change_driver = self.instance.somefunc()
except AttributeError:
# unbound form, what do you want to do here?
can_change_driver = True # for example?
# if the driver can't be changed, use a input=hidden
# input field.
if not can_change_driver:
self.fields["Drivers"].widget = HiddenInput()
class Meta:
model = SomeModel
So, key points from this:
self.instance represents the bound object, if the form is bound. I believe it is passed in as a named argument, therefore in kwargs, which the parent constructor uses to create self.instance.
You can modify the field properties after you've called the parent constructor.
widgets are how forms are displayed. HiddenInput basically means <input type="hidden" .../>.
There is one limitation; I can tamper with the input to change a value if I modify the submitted POST/GET data. If you don't want this to happen, something to consider is overriding the form's validation (clean()) method. Remember, everything in Django is just objects, which means you can actually modify class objects and add data to them at random (it won't be persisted though). So in your __init__ you could:
self.instance.olddrivers = instance.drivers.all()
Then in your clean method for said form:
def clean(self):
# validate parent. Do this first because this method
# will transform field values into model field values.
# i.e. instance will reflect the form changes.
super(SomeForm, self).clean()
# can we modify drivers?
can_change_driver = self.instance.somefunc()
# either we can change the driver, or if not, we require
# that the two lists are, when sorted, equal (to allow for
# potential non equal ordering of identical elements).
# Wrapped code here for niceness
if (can_change_driver or
(sorted(self.instance.drivers.all()) ==
sorted(self.instance.olddrivers))):
return True
else:
raise ValidationError() # customise this to your liking.
You can do what you need by adding your own init where you can pass in the id when you instantiate the form class:
class ProfileForm(ModelForm):
def __init__(self, team_id, *args, **kwargs):
super(ProfileForm, self).__init__(*args, **kwargs)
this_team = Team.objects.get(pk=team_id)
teams = Team.objects.order_by('total_points')
count = 0
for team in teams:
if team.pk == this_team.pk:
break
count += 1
now = datetime.datetime.now().weekday()
if now >= count:
# show driver_one, driver_two, driver_three
else:
# do not show driver_one, driver_two, driver_three
class Meta:
model = Team
#views.py
def my_view(request, team_id):
profile_form = ProfileForm(team_id, request.POST or None)
#more code here
Hope that helps you out.

How to make an auto-filled and auto-incrementing field in django admin

[Update: Changed question title to be more specific]
Sorry if I didn't make the question very well, I can't figure how to do this:
class WhatEver():
number = model.IntegerField('Just a Field', default=callablefunction)
...
Where callablefunction does this query:
from myproject.app.models import WhatEver
def callablefunction():
no = WhatEver.objects.count()
return no + 1
I want to automatically write the next number, and I don't know how to do it.
I have errors from callablefunction stating that it cannot import the model, and I think there must be an easier way to do this. There's no need even to use this, but I can't figure how to do it with the pk number.
I've googled about this and the only thing I found was to use the save() method for auto incrementing the number... but I wanted to show it in the <textfield> before saving...
What would you do?
Got it! I hope this will help everyone that has any problems making a auto-filled and auto-incrementing field in django. The solution is:
class Cliente(models.Model):
"""This is the client data model, it holds all client information. This
docstring has to be improved."""
def number():
no = Cliente.objects.count()
if no == None:
return 1
else:
return no + 1
clientcode = models.IntegerField(_('Code'), max_length=6, unique=True, \
default=number)
[... here goes the rest of your model ...]
Take in care:
The number function doesn't take any arguments (not even self)
It's written BEFORE everything in the model
This was tested on django 1.2.1
This function will automatically fill the clientcode field with the next number (i.e. If you have 132 clients, when you add the next one the field will be filled with clientcode number 133)
I know that this is absurd for most of the practical situations, since the PK number is also auto-incrementing, but there's no way to autofill or take a practical use for it inside the django admin.
[update: as I stated in my comment, there's a way to use the primary key for this, but it will not fill the field before saving]
Every Django model already has an auto-generated primary key:
id = models.AutoField(primary_key=True)
It seems you are trying to duplicate an already existing behavior, just use the object primary key.
I, too, came across this problem, my instance of it was customer.number which was relative to the customers Store. I was tempted to use something like:
# Don't do this:
class Customer(models.Model):
# store = ...
number = models.IntegerField(default=0)
def save(self, *args, **kwargs):
if self.number == 0:
try:
self.number = self.store.customer_set.count() + 1
else:
self.number = 1
super(Customer, self).save(*args, **kwargs)
The above can cause several problems: Say there were 10 Customers, and I deleted customer number 6. The next customer to be added would be (seemingly) the 10th customer, which would then become a second Customer #10. (This could cause big errors in get() querysets)
What I ended up with was something like:
class Store(models.Model):
customer_number = models.IntegerField(default=1)
class Customer(models.Model):
store = models.ForeignKey(Store)
number = models.IntegerField(default=0)
def save(self, *args, **kwargs):
if self.number == 0:
self.number = self.store.customer_number
self.store.number += 1
self.store.save()
super(Customer, self).save(*args, **kwargs)
PS:
You threw out several times that you wanted this field filled in "before". I imagine you wanted it filled in before saving so that you can access it. To that I would say: this method allows you to access store.customer_number to see the next number to come.
You have errors in code, that's why you can't import it:
from django.db import models
class WhatEver(models.Model):
number = models.IntegerField('Just a Field', default=0)
and Yuval A is right about auto-incrementing: you don't even need to declare such a field. Just use the pk or id, they mean the same unless there's a composite pk in the model:
> w = Whatever(number=10)
> w
<Whatever object>
> w.id
None
> w.save()
> w.id
1
[update] Well, I haven't tried a callable as a default. I think if you fix these errors, it must work.

Categories

Resources