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

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]

Related

How can I Generate 10 Unique digits in Model Form and Pass Form Context Variable in Django Class Based ListView

I am new to Django Class Based Views and I am working on a project where on the template I want to have Form for creating customer accounts on the left and list of existing customers on the right.
So far I have the list of existing customers displayed but for the form I don't know how to pass its variable context to the same template, or it is not possible to Pass a Form that would be submitted inside a ListView Method. And I also want to generate unique account numbers of 10 Digits in ModelForm which I want the form field to be auto-filled and disabled
Here is my form code:
import secrets
#I want to Generate Account Number of 10 Digits but getting only 2
account = secrets.randbits(7)
#class for Customer Account Form
class CustomerAccountForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super().init(*args, **kwargs)
self.fields['accountnumber'].initial = account
class Meta:
model = Customer
fields = ['accountnumber','surname','othernames','address','phone']
Code for my views (ListView)
class CustomerListView(ListView):
model = Customer
form_class = CustomerAccountForm
template_name = 'dashboard/customers.html'
#Function to get context data from queries
def get_context_data(self, **kwargs):
context_data = super().get_context_data(**kwargs)
#Get Day of today from current date and time
now = datetime.datetime.now()
#Get the date today
date_today = datetime.datetime.now().date
#Count Number of Withdrawals Today and passing in context
context_data['count_withdrawals_today'] = Witdrawal.objects.filter(date__year=now.year, date__month=now.month, date__day=now.day).count()
context_data['count_deposits_today'] = Deposit.objects.filter(date__year=now.year, date__month=now.month, date__day=now.day).count()
context_data['count_accounts'] = Customer.objects.count()
context_data['count_users'] = User.objects.count()
#Calculate today Deposit Today
context_data['total_deposit']= Deposit.objects.filter(date__year=now.year, date__month=now.month, date__day=now.day).aggregate(total_deposit=Sum('deposit_amount')).get('total_deposit') or 0
#Calculate today Withdrawal Today
context_data['total_withdrawal']= Witdrawal.objects.filter(date__year=now.year, date__month=now.month, date__day=now.day).aggregate(total_withdrawal=Sum('withdrawal_amount')).get('total_withdrawal') or 0
return context_data
Someone should please help me on how this is properly done and the form would be submitted successfully. Thanks in anticipation for your answer.
secrets.randbits(k) generates a random integer within a bit range.
For k=4 then Unsigned integer From 0 to 15.
For k=8 then Unsigned integer From 0 to 255.
For k=16 then Unsigned integer From 0 to 65,535, and so on.
If you want 10 random digits then you can use for example:
import random
account = "".join(str(random.randint(0, 9)) for _ in range(10))
After going through many tutorials and blogs on Django Class Based Views with ListViews for Forms, I discovered that ListViews was designed to populate Model items while FormViews is designed for creating and processing forms both can't be used on one template. Although many developers have had a way around it with the use of Multiple Mixins but no best practice has be mentioned for or against their findings yet.
In these case, I concluded that for a Django Template to be able to process Model Form and at the same time Populate Database items, it needs not to use Class Based Views but Function Based Views.

I want to crate a Django field in my model to display a word after each new entry

So I want to create a field in my Django models.py, so that the user can select a number of years as integer (e.g. 3) and then after each new entry the words 'years' to be automatically displayed. I do not want to do this with CharField.
class StudyProgramme(models.Model):
period = models.IntegerField(2)
You can write a serializer for the same.
from rest_framework import serializers
class StudyProgrammeSerializer(serializers.ModelSerializer):
def get_period(self, obj):
return str(obj.period) + "years"
class Meta:
model = StudyProgramme
fields = ['period']
I don't get it yet but i think you can do it like:
# Check the model StudyProgramme
If StudyProgramme.Objects.all().count() > 0: //Show
Else: //request to put it it
# And for sure, return it to template

Massage model data before save in Django

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)

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