Custom validation of ModelForm in Django - django

I am working on a Django app to register sales. I have created three models: Project, Employee, and Sale.
The project and employee models are as follows:
class Project(models.Model):
project_id = models.IntegerField(primary_key = True)
name = models.CharField(max_length = 100, unique = True)
class Meta:
ordering = ['name']
def __str__(self):
return self.name
class Employee(models.Model):
employee_id = models.IntegerField(primary_key = True)
name = models.CharField(max_length = 50)
email = models.CharField(max_length = 40)
class Meta:
ordering = ['name']
def __str__(self):
return self.name
Then the sale model:
class Sale(models.Model):
sale_name = models.CharField(max_length = 30)
project = models.ForeignKey('Project', on_delete = models.CASCADE)
proactive_seller = models.ManyToManyField(Employee, related_name = 'proactive')
participants = models.ManyToManyField(Employee, related_name = 'participant')
doers = models.ManyToManyField(Employee, related_name = 'doer')
start_date = models.DateField()
end_date = models.DateField()
def __str__(self):
return self.sale_name
So the each sale object contains information on what project the sale is related to, which employee was the proactive/lead seller, which employees were participating in the sale, and also which employees will be doing the actual project.
In my forms.py I want to make sure that the sales are unique in the sense that I want to raise an error if the user is trying to enter a sale which already has the same project, same date and the same doers, i.e. the doers can't be allocated to the project more than once at a time.
My forms.py is currently looking like this:
class SaleForm(ModelForm):
class Meta:
model = Sale
widgets = {
'start_date': DatePickerInput(),
'end_date': DatePickerInput(),
}
I tried the following:
def clean(self):
cleaned_data = super.clean()
start = cleaned_data.get('start_date')
end = cleaned_data.get('end_date')
doers = cleaned_data.get('doers')
project = cleaned_data.get('project')
if start and end and doers and project:
queryset = Sale.objects.all()
# Filter based on project
q = queryset.filter(project__name=project, start_date = start, end_date = end)
for employee in doers:
q = q.filter(doers__name=employee)
if q.count() > 1:
raise forms.ValidationError('Sale has already been registered.')
However, the validation does not work as expected: I am still allowed to allocate “employees” to the same “project” at the same time (ie start date and end date).
Help is much appreciated.

What you're attempting to do is validate every instance of the M2M relationship for a given instance. This can be fairly difficult to do. What should suffice is to filter down on sales with the same number of doers and filter out sales that contain a different doer on them.
from django.db.models import F, OuterRef, Exists, Q
q = queryset.filter(project__name=project, start_date=start, end_date = end)
other_doers = Employee.objects.filter(
# Exclude any employee with the name of the doers on this project.
# We only want other doers.
~Q(name__in=[e.name for e in doers]),
# This links the subquery to the main query (Sale)
doer=OuterRef('id'),
)
q = q.annotate(
# Get the count of doers per sale
doer_count=Count('doers__id', distinct=True),
# Check if other doers are in the project
has_other_doer=Exists(other_doers)
).filter(
# Only look for sales with the same number of doers
doer_count=len(doers),
# Filter out sales that contain other doers
has_other_doer=False,
)

Related

django 3.0 inline admin form foreignkey -> foreignkey

I have any models and one Manager
app/models.py
class castratedListStudent(models.Manager):
use_in_migrations = False
def get_query_set(self):
return super().get_query_set().filter(isOn=1)
class student(models.Model):
id = models.AutoField(primary_key=True)
firstName = models.CharField(max_length=20)
lastName = models.CharField(max_length=20)
isOn = models.BooleanField()
default_manager = castratedListStudent()
objects = castratedListStudent()
class discipline(models.Model):
id = models.AutoField(primary_key=True)
nameDiscipline = models.CharField(max_length=100, null=False)
itemIdToDiscip = models.ForeignKey(item, on_delete=models.CASCADE, default=1)
class listOfStudForDiscipline(models.Model):
id = models.AutoField(primary_key=True)
discipListId = models.ForeignKey(discipline, on_delete=models.CASCADE)
studId = models.ForeignKey(student, on_delete=models.CASCADE)
I am using django inline
accounts/admin.py
class discipStudentInline(admin.TabularInline):
model = listOfStudForDiscipline
admin.TabularInline.verbose_name = 'Student'
extra = 0
def has_change_permission(self, request, obj=None):
return False
def get_queryset(self, request):
return self.model.objects.filter(studId__isOn=1)
class disciplineAdmin(admin.ModelAdmin):
model = discipline
inlines = [discipStudentInline]
The Inline form is displayed on the HTML page and filter (studId__isOn=1) works. But the problem is that on the HTML page below there is a field that allows you to add another student and the list of students is not filtered by the filter rule(studId__isOn=1) When I check in the DEBUG_SQL console, I can see how the query goes without the WHERE expression "FROM journal_student".
(0.000) SELECT `journal_listofstudfordiscipline`.`id`, `journal_listofstudfordiscipline`.`discipListId_id`, `journal_listofstudfordiscipline`.`studId_id` FROM `journal_listofstudfordiscipline` INNER JOIN `journal_student` ON (`journal_listofstudfordiscipline`.`studId_id` = `journal_student`.`id`) WHERE (`journal_student`.`isOn` = 1 AND journal_listofstudfordiscipline`.`discipListId_id` = 1) ORDER BY `journal_student`.`lastName` DESC; args=(True, 1)
(0.000) SELECT `journal_student`.`id`,..., `journal_student`.`descriptionStudent` FROM journal_student` ORDER BY `journal_student`.`lastName` ASC, `journal_student`.`firstName` ASC; args=()
I couldn't solve the problem using the model Manager.
I solved the problem. It turned out that after django version 1.7, the get_query_set() method was renamed to get_queryset(). And the Manager will now look like this:
class castratedListStudent(models.Manager):
use_in_migrations = False
def get_query_set(self):
return super(liveListStudent, self).get_queryset().filter(isOn=1)

Filtering Django models by user & object

I'm learning Django with a dummy example but having difficulty in understanding how to correctly filter my Django models by an authorised user in my views.
In my view I want to list the transactions associated with a users portfolio. The code below runs but when trying to access the result of 't' I get the error:
'ValueError: The QuerySet value for an exact lookup must be limited to one result using slicing.'
Any help would be much appreciated, thanks.
if request.user.is_authenticated:
# Get model data
pf = Portfolio.objects.filter(user=request.user)
t = Transaction.objects.filter(pf=pf)
My model is as below:
from django.db import models
from django.contrib.auth.models import User
class Portfolio(models.Model):
# Portfolio has one user associated with it
user = models.ForeignKey(User, on_delete=models.CASCADE)
name = models.CharField(max_length=100, default='-')
def __str__(self):
return self.name
class Transaction(models.Model):
# Transaction has one equity associated with it
equity = models.ForeignKey('Equity', on_delete=models.CASCADE, null=True)
# Transaction has one portfolio associated with it
pf = models.ForeignKey('Portfolio', on_delete=models.CASCADE)
BUY = 'BUY'
SELL = 'SELL'
BUY_OR_SELL = (
(BUY, 'BUY'),
(SELL, 'SELL'),
)
action = models.CharField(choices=BUY_OR_SELL, default=BUY, max_length=5)
num = models.FloatField(default=1)
price = models.FloatField(default=0)
date = models.DateField('date')
fee = models.FloatField(default=0)
def __str__(self):
return f'{self.equity}, {self.num}x{self.price}, {self.date:%d %b %Y}'
class Equity(models.Model):
class Meta:
verbose_name_plural = "Equities"
CUR_EUR = 'EUR'
CUR_GBP = 'GBP'
CUR_USD = 'USD'
CURRENCY_CHOICES = (
(CUR_EUR, 'EUR'),
(CUR_GBP, 'GBP'),
(CUR_USD, 'USD'),
)
symbol = models.CharField(max_length=20, default='-')
exchange = models.CharField(max_length=100, default='-')
currency = models.CharField(max_length=15, choices=CURRENCY_CHOICES, default=CUR_USD)
def __str__(self):
return self.symbol
Many thanks!
pf is here a collection of Portfolio objects, so you can query it with the __in lookup [Django-doc]:
Transaction.objects.filter(pf__in=pf)
Or if you are not interested in the Porfolio objects itself, you can make a query like:
Transaction.objects.filter(pf__user=request.user)
The query below will result in a query like:
SELECT transaction.*
FROM transaction
JOIN portfolio ON transaction.pf_id = portfolio.id
WHERE porfolio.user_id = 123
(with 123 the id of the request.user)

View and Template for m2m with Through

In my app i need to store invoices (Invoice) of known products (Product) to calculate points for each seller (User). I'm trying to create form to insert basic invoice data plus inline form with sold products info. To handle it i create model like this:
class Product(models.Model):
group = models.CharField(max_length = 200, blank = False)
mark = models.CharField(max_length = 200, blank = True)
points = models.IntegerField(blank = False)
class Invoice(models.Model):
price = models.FloatField(blank=False)
file = models.FileField(blank=False)
product = models.ManyToManyField(Product, through='Sold')
user = models.ForeignKey(User, on_delete=models.CASCADE)
date = models.DateField()
date_created = models.DateField(auto_now_add=True)
date_updated = models.DateField(auto_now=True)
class Sold(models.Model):
invoice = models.ForeignKey(Invoice, on_delete=models.CASCADE)
product = models.ForeignKey(Product, on_delete=models.CASCADE)
quantity = models.PositiveIntegerField(default=1)
I tried to manage it via django-admin and it work fine with admin.py:
class ProductTabular(admin.TabularInline):
model = Invoice.product.through
class InvoiceAdmin(admin.ModelAdmin):
inlines = [ProductTabular]
exclude = ('product', )
class Meta:
model = Invoice
admin.site.register(Invoice, InvoiceAdmin)
but i'm unable to create such form in own templates. Please, can you help me with views.py and template to get same result as for the django-admin?
I tried via invoce form with inlineformset_factory for the Sold model, but i can't figure out how to save it. Thanks for any help!

How to use djando-table2 with class based view to display data from multiple table?

models.py
class DailyRecordManager(models.Manager):
def get_query_set(self):
qs = super(DailyRecordManager, self).get_query_set().order_by('date_organised')
return qs
class DailyRecord(models.Model):
date_organised = models.DateField('Ogransied Date', help_text=('Enter Date when the program is organised: CCYY-MM-DD'))
program_name = models.TextField('program name',)
venue = models.CharField('venue', max_length = 255, blank=True)
organiser = models.ForeignKey(Organiser, verbose_name = 'Organiser', related_name = 'organisers')
objects = models.Manager()
public = DailyRecordManager()
class Meta:
verbose_name = 'dailyrecord'
verbose_name_plural = 'dailyrecords'
ordering = ['-date_organised']
def __str__(self):
return self.program_name
class Participant(models.Model):
participant = models.CharField(max_length= 50, unique = True)
daily_record = models.ForeignKey(DailyRecord, verbose_name = 'program_name')
class Meta:
verbose_name = 'participant'
verbose_name_plural = 'participants'
def __str__(self):
return self.participant
views.py
class DailyActivityPageView(SingleTableView):
table_class = DailyRecordTable
queryset = DailyRecord.public.all()
# queryset = Participant(DailyRecord.public.all()) is not working
template_name = 'dailyrecord/daily-activity-record.html'
tables.py
class DailyRecordTable(tables.Table):
date_organised = tables.Column('Date')
program_name = tables.Column( 'Program Name')
venue = tables.Column( 'Venue')
organiser = tables.Column( 'Organiser')
participant = tables.Column( 'dailyrecord.participant')
# daily = tables.RelatedLinkColumn()
class Meta:
model = DailyRecord
Now what I need is to display the data from participant table too, corresponding to the daily_record foreign key.
Click this link to view the snapshot. see the last column of the table. I need the data of Participant.partcipant column here
Sorry for poor English.
Thank You
There are two problems here.
First is, that a daily record can have multiple participants. Thus, in order to fill last column you have to aggregate all possible participants into that column (for example as list).
Second is, that you should make Participant backwards related to DailyRecord by adding attribute "related_name" to daily_record in your Participant model, like this:
daily_record = models.ForeignKey(DailyRecord, verbose_name = 'program_name', related_name="participants")
Now, you should simply get participants from daily_record like this:
participants = DailyRecord.public.first().participants.all()
If you had OneToOneField instead of ForeignKey you could add (single) participant to table like this:
participant = tables.Column( "Participant", accessor='participant')

django - adding a counter for every ManyToMany field added

i have these models:
#model.py
class Subject(models.Model):
name = models.CharField("Name",max_length=50, blank=True)
...
...
class Activity(models.Model):
label = models.CharField("Act. name",max_length=150)
price = models.DecimalField("price", max_digits=10, decimal_places=2,default=0)
count = models.IntegerField("Count", default=0)
def __unicode__(self):
return u"%s" % (self.label)
class Meta:
verbose_name_plural = "Activities"
class Invoice(models.Model):
subject = models.ForeignKey(Subject)
date = models.DateField(default=date.today())
activities = models.ManyToManyField(Activity)
....
....
while creating a new Invoice instance on admin, i can select the many to many fields 'activities', but i'd like to have an additional counter (eg. an IntegerField) as an Invoice field to count and save the quantity of each activity added to my Invoice instance. Is this possible?
thanks,
Luke
You could have a field on the model and override the save method
class Invoice(models.Model):
subject = models.ForeignKey(Subject)
date = models.DateField(default=date.today())
activities = models.ManyToManyField(Activity)
activity_count = models.PositiveIntegerField(default=0)
def save(self):
self.activity_count = self.activities.count()
super(Invoice, self).save()