How to handle state / conditionally modify fields within Django forms - python

I have a django model representing a task. This task will go through multiple states from 'DRAFT' to 'COMPLETION' and during that time, various fields will change from requiring user input to explicitly refusing it. For example, you can't change the date a task was completed while it's in draft. Additionally i want different links to appear on the page based on the state (i.e. if it's in checking I want the submit button to say 'Complete this task')
I originally planned for the status to be a model in it's own right but could not work out a way - beyond hard coding - that would bring any value to those models so opted for an explicit list instead. (Any better approaches here would be appreciated).
The key problem I have is how to manage these fields and their states. It seems like the easiest would be to have conditional statements in the template like {% if task.status = 'ACCEPTED' %} but that seems like putting an awful lot of business logic into a template. I also get the impression that disabling a field once you're already in a template is much harder than the form.
My current approach is to use the view to manage these states but that seems messy and doesn't (for me) solve how to change link names and the like in the template.
if task.status = Task.ACCEPTED:
form.fields['datereceived'].disabled = True
if task.status = Task.COMPLETED:
...
Is the view the place to manage these and is there a more pythonic/djangonic to manage these without overloading the template?
Sample code so excuse bugs:
Model
class Task(models.Model):
STATUS_CHOICES = (
(DRAFT, DRAFT),
(ALLOCATED, ALLOCATED),
(ACCEPTED, ACCEPTED),
(CHECKING, CHECKING),
(COMPLETED, COMPLETED),
(WITHDRAWN, WITHDRAWN),
(ON_HOLD, ON_HOLD),
)
status = models.CharField(max_length=20, choices=STATUS_CHOICES,default=DRAFT)
datereceived = models.DateField(blank=True, null=True)
dateworked = models.DateField(blank=True, null=True)
datechecked = models.DateField(blank=True, null=True)
datecompleted = models.DateField(blank=True, null=True)
datedue = models.DateField(blank=True, null=True)
Modelform
class TaskForm(forms.ModelForm):
class Meta:
model = Task
#fields = All fields listed individually but hard to pick out for sample
widgets = {
'datereceived': forms.DateInput(attrs={'class':'datepicker'}),
'datedue': forms.DateInput(attrs={'class':'datepicker'}),
'datecompleted': forms.DateInput(attrs={'class':'datepicker'}),
}

Try putting the logic in the form instantiation code as so:
class TaskForm(forms.ModelForm):
class Meta:
model = Task
def handle_state(self, *args, **kwargs):
task = getattr(self, 'instance', None)
if task:
if task.status = Task.ACCEPTED:
self.fields['datereceived'].disabled = True
elif task.status = Task.COMPLETED:
...
def __init__(self, *args, **kwargs):
super(TaskForm, self).__init__(*args, **kwargs)
self.handle_state(*args, **kwargs)

you can use finite state machine. django-fsm to handle states of your task. In this you can define the source and target state of every transition. for reference you can see this example. https://distillery.com/blog/building-for-flexibility-using-finite-state-machines-in-django/

Related

Get minimum of related model date field (django)

I have the two following classes:
class Incident(models.Model):
iid = models.IntegerField(primary_key=True)
person = models.ForeignKey('Person', on_delete=models.SET_NULL, null=True)
class Source(models.Model):
sid = models.IntegerField(primary_key=True)
incident = models.ForeignKey('Incident', on_delete=models.SET_NULL, null=True)
url = models.TextField(validators=[URLValidator()])
datereported = models.DateField(null=True, blank=True)
I want to create a field within the Incident that will pull the minimum datereported of related sources. Is this best done in the model, or in the template? Unsure what best practice is, or how to execute in this case.
Is this best done in the model, or in the template?
Well a template should - strictly speaking - not contain business logic. It should contain render logic. It should thus specify how something should be visible, not what should be visible. So it dos not really belong in the template layer, only in the model layer.
You can obtain the smallest datereported with:
from django.db.models import Min
class Incident(models.Model):
iid = models.IntegerField(primary_key=True)
person = models.ForeignKey('Person', on_delete=models.SET_NULL, null=True)
#property
def first_reporteddate(self):
return self.source_set.aggregate(first=Min('datereported'))['first']
This will ignore Sources with datereported set to None (so if there are multiple sources, it takes the smallest datereported that is not None). If there are no Source with a datereported not equal to None, or no related Sources at all, it will return None however, since the minimum of an empty set is considered to be NULL (in SQL, or None in Python/Django).
You can then use this in the template like:
{{ some_incident.first_reporteddate }}
In case you want the entire object, you can use self.source_set.order_by('datereported').first() which will give you the earliest related Source instance. But this will have a (quite) small impact on performance (it will take a bit longer). In that case Django will fetch all columns into memory first. If you thus only need one column, this will result in the fact that you did some useless serialization (at the database end) and deserialization (at the Python end).
You can use model's property for this:
class Incident(models.Model):
iid = models.IntegerField(primary_key=True)
person = models.ForeignKey('Person', on_delete=models.SET_NULL, null=True)
#property
def first_datereported(self):
first_source = self.source_set.order_by('datereported').first()
if first_source:
return first_source.datereported
In template or in any other part of code you can use first_datereported as normal model's field:
{{ incident_instance.first_datereported }}

Django - validate unique for a calculated field in the Model and also in the ModelForm

TL;DR both my model and my form calculate the value of the field number_as_char. Can I avoid the double work, but still check uniqueness when using the model without the form?
I use Python 3 and Django 1.11
My model looks as follows:
class Account(models.Model):
parent_account = models.ForeignKey(
to='self',
on_delete=models.PROTECT,
null=True,
blank=True)
number_suffix = models.PositiveIntegerField()
number_as_char = models.CharField(
max_length=100,
blank=True,
default='',
unique=True)
#classmethod
def get_number_as_char(cls, parent_account, number_suffix):
# iterate over all parents
suffix_list = [str(number_suffix), ]
parent = parent_account
while parent is not None:
suffix_list.insert(0, str(parent.number_suffix))
parent = parent.parent_account
return '-'.join(suffix_list)
def save(self, *args, **kwargs):
self.number_as_char = self.get_number_as_char(
self.parent_account, self.number_suffix)
super().save(*args, **kwargs)
The field number_as_char is not supposed to be set by the user because it is calculated based on the selected parent_account: it is obtained by chaining the values of the field number_suffix of all the parent accounts and the current instance.
Here is an example with three accounts:
ac1 = Account()
ac1.parent_account = None
ac1.number_suffix = 2
ac1.save()
# ac1.number_as_char is '2'
ac2 = Account()
ac2.parent_account = ac1
ac2.number_suffix = 5
ac2.save()
# ac2.number_as_char is '2-5'
ac3 = Account()
ac3.parent_account = ac2
ac3.number_suffix = 1
ac3.save()
# ac3.number_as_char is '2-5-1'
It is NOT an option to drop the field and use a model property instead, because I need to ensure uniqueness and also use that field for sorting querysets with order_by().
My form looks as follows:
class AccountForm(forms.ModelForm):
class Meta:
model = Account
fields = [
'parent_account', 'number_suffix', 'number_as_char',
]
widgets = {
'number_as_char': forms.TextInput(attrs={'readonly': True}),
}
def clean(self):
super().clean()
self.cleaned_data['number_as_char'] = self.instance.get_number_as_char(
self.cleaned_data['parent_account'], self.cleaned_data['number_suffix'])
I included number_as_char in the form with widget attribute readonly and I use the forms clean() method to calculate number_as_char (it has to be calculated before validating uniqueness).
This all works (the model and the form), but after validating the form, the value of number_as_char will be calculated again by the models save() method. Its not a big problem, but is there a way to avoid this double calculation?
If I remove the calculation from the forms clean() method, then the uniqueness will not be validated with the new value (it will only check the old value).
I don't want to remove the calculation entirely from the model because I use the model in other parts without the form.
Do you have any suggestions what could be done differently to avoid double calculation of the field?
I can't see any way around doing this in two places (save() and clean()) given that you need it to work for non-form-based saves as well).
However I can offer two efficiency improvements to your get_number_as_char method:
Make it a cached_property so that the second time it is called, you simply return a cached value and eliminate double-calculation. Obviously you need to be careful that this isn't called before an instance is updated, otherwise the old number_as_char will be cached. This should be fine as long as get_number_as_char() is only called during a save/clean.
Based on the information you've provided above you shouldn't have to iterate over all the ancestors, but can simply take the number_as_char for the parent and append to it.
The following incorporates both:
#cached_property
def get_number_as_char(self, parent_account, number_suffix):
number_as_char = str(number_suffix)
if parent_account is not None:
number_as_char = '{}-{}'.format(parent_account.number_as_char, number_as_char)
return number_as_char
To be sure that the caching doesn't cause problems you could just clear the cached value after you're done saving:
def save(self, *args, **kwargs):
self.number_as_char = self.get_number_as_char(
self.parent_account, self.number_suffix)
super().save(*args, **kwargs)
# Clear the cache, in case something edits this object again.
del self.get_number_as_char
I tinkered with it a bit, and I think I found a better way.
By using the disabled property on the number_as_char field of your model form, you can entirely ignore users input (and make the field disabled in a single step).
Your model already calculates the number_as_char attribute in the save method. However, if the Unique constraint fails, then your admin UI will throw a 500 error. However, you can move your field calculation to the clean() method, leaving the save() method as it is.
So the full example will look similar to this:
The form:
class AccountForm(forms.ModelForm):
class Meta:
model = Account
fields = [
'parent_account', 'number_suffix', 'number_as_char',
]
widgets = {
'number_as_char': forms.TextInput(attrs={'disabled': True}),
}
The model:
class Account(models.Model):
# ...
def clean(self):
self.number_as_char = self.get_number_as_char(
self.parent_account, self.number_suffix
)
super().clean()
That way anything that generates form based on your model will throw a nice validation error (provided that it uses the built-in model validation, which is the case for Model Forms).
The only downside to this is that if you save a model that triggers the validation error, you will see an empty field instead of the value that failed the validation - but I guess there is some nice way to fix this as well - I'll edit my answer if I also find a solution to this.
After reading all the answers and doing some more digging through the docs, I ended up using the following:
#samu suggested using the models clean() method and #Laurent S suggested using unique_together for (parent_account, number_suffix). Since only using unique_together doesn't work for me because parent_account can be null, I opted for combining the two ideas: checking for existing (parent_account, number_suffix) combinations in the models clean() method.
As a consecuence, I removed number_as_char from the form and it is now only calculated in the save() method. By the way: thanks to #solarissmoke for suggesting to calculated it based on the first parent only, not iterating all the way to the top of the chain.
Another consecuence is that I now need to explicitly call the models full_clean() method to validate uniqueness when using the model without the form (otherwise I will get the database IntegrityError), but I can live with that.
So, now my model looks like this:
class Account(models.Model):
parent_account = models.ForeignKey(
to='self',
on_delete=models.PROTECT,
null=True,
blank=True)
number_suffix = models.PositiveIntegerField()
number_as_char = models.CharField(
max_length=100,
default='0',
unique=True)
def save(self, *args, **kwargs):
if self.parent_account is not None:
self.number_as_char = '{}-{}'.format(
self.parent_account.number_as_char,
self.number_suffix)
else:
self.number_as_char = str(self.number_suffix)
super().save(*args, **kwargs)
def clean(self):
qs = self._meta.model.objects.exclude(pk=self.pk)
qs = qs.filter(
parent_account=self.parent_account,
number_suffix=self.number_suffix)
if qs.exists():
raise ValidationError('... some message ...')
And my form ends up like this:
class AccountForm(forms.ModelForm):
class Meta:
model = Account
fields = [
'parent_account', 'number_suffix',
]
EDIT
I'll mark my own answer as accepted, because non of the suggestions fully suited my needs.
However, the bounty goes to #samus answer for pointing me in the right direction with using the clean() method.
Another way - probably not as good though - would be to use Django signals. You could make a pre_save signal that would set the correct value for number_as_char field on the instance that's about to get saved.
That way you don't have to have it done in a save() method of your model, OR in the clean() method of your ModelForm.
Using signals should ensure that any operation that uses the ORM to manipulate your data (which, by extend, should mean all ModelForms as well) will trigger your signal.
The disadvantage to this approach is that it is not clear from the code directly how is this property generated. One has to stumble upon the signal definition in order to discover that it's even there. If you can live with it though, I'd go with signals.

Django Admin filter by function / filter only by first object in reverse foreign key lookup

I am trying to build a filter by function in django. From what I've learned by googling this is quite hard to achieve.
So here is my code:
class TrackingEventType(models.Model):
name = models.CharField(blank=False, null=False, max_length=255)
class TrackingEvent(models.Model):
datetime = models.DateTimeField(blank=False, null=False, default=datetime.now, verbose_name="Zeitpunkt")
event_type = models.ForeignKey(TrackingEventType, help_text="Art des Events")
tracking = models.ForeignKey('Tracking')
class Meta:
ordering = ['-datetime']
class Tracking(models.Model):
tracking_no = models.CharField(blank=False, null=False, max_length=10, unique=True, verbose_name="Tracking Nummer")
def get_last_event(self):
"""
Todo: return the latest event.
"""
return TrackingEvent.objects.filter(tracking=self.id).first()
get_last_event.short_description = 'Last event'
class Meta:
ordering = ['-tracking_no']
My goal is to make it possible to filter Tracking objects by their last events type name. Displaying the result of the funtion in django admin is easy, but adding a corresponding filter isn't.
My idea was also to try to build a filter something like:
trackingevent__set__first__event_type__name
But yeah, that would be too easy :)
Any inputs are welcome.
As you've discovered it isn't trivial to filter in that manner. If you are accessing that information regularly it is probably also not very efficient either.
I would suggest that you store a reference to the latest tracking event in the Tracking model itself:
class Tracking(models.Model)
# ...
last_event = models.ForeignKey(TrackingEvent, null=True)
You would then use signals to update this reference whenever a new tracking event is created. Something along the lines of:
from django.db.models.signals import post_save
from django.dispatch import receiver
#receiver(post_save, sender=TrackingEvent)
def update_latest_tracking_event(sender, instance, created, **kwargs):
# Is this a new event?
if created:
# If yes, then update the Tracking reference
tracking = instance.tracking
tracking.last_event = instance
tracking.save()
(Please read the documentation on where to put this code).
Once all this is in place it becomes easy to filter based on the last tracking event type:
# I'm just guess what event types you have...
cancellation = TrackingEventType.objects.get(name='cancel')
Tracking.objects.filter(last_event__event_type=cancellation)

How can I init ManyToMany field in django models that can't relate to itself(object level)?

Example:
class MyUser(models.Model):
blocked_users = models.ManyToManyField("self", blank=True, null=True)
user = MyUser.object.get(pk=1)
user.blocked_users.add(user)
user.blocked_users.all()[0] == user # (!!!)
Can It be prevented on model/db level? Or we need just do check somewhere in app.
Looking at the Django docs for ManyToManyField arguments, it does not seem possible.
The closest argument to what you want is the limit_choices_to However, that only limits choices on ModelForms and admin (you can still save it like you did in your example), and there is currently no easy way to use it to limit based on another value (pk) in the current model.
If you want to prevent it from happening altogether, you'll have to resort to overriding the save method on the through model--something like:
class MyUser(models.Model):
blocked_users = models.ManyToManyField(..., through="BlockedUser")
class BlockedUser(models.Model):
user = models.ForeignKey(MyUser)
blocked = models.ForeignKey(MyUser)
def save(self, *args, **kwargs):
# Only allow this relationship to be created if
if self.user != self.blocked:
super(BlockedUser, self).save(*args, **kwargs)
You could of course also do this with signals.

django forms logged in user problem

I am writing an application to help employees track projects the are working on.
Part of the form should allow logged in employees to click a drop down and see all projects they are working on. This is the part I am struggling with; specifically getting ONLY the logged in user's projects populated in a drop down. Any help or insight is much appreciated. Thanks……
models.py
class Photo(models.Model):
image = models.ImageField(upload_to='uploads/images/photo')
title = models.CharField(max_length=50)
def __unicode__(self):
return self.title
class Employee(models.Model):
user = models.ForeignKey(User, unique=True)
photo = models.ImageField(upload_to='uploads/images')
department = models.ForeignKey(Department, null=True)
phone = PhoneNumberField("Phone")
def __unicode__(self):
return self.user.get_full_name()
class Projects(models.Model):
name = models.CharField(max_length=40)
student = models.ForeignKey(Student)
photos = models.ManyToManyField(Photo, blank=True, null=True)
forms.py
class ProjectsForm(forms.ModelForm):
employee = get_object_or_404(Employee, user=user)
employee_projects = employee.projects_set.all()
name = forms.ModelChoiceField(queryset=employee_projects,
empty_label="(Select a Project)", required=True)
class Meta:
model = Projects
You need to put first two lines from ProjectsForm class definition to its initialization method and change them a bit.
class ProjectsForm(forms.ModelForm):
name = forms.ModelChoiceField(queryset=Employee.objects.all(),
empty_label="(Select a Project)", required=True)
class Meta:
model = Projects
def __init__(self, user, *args, **kwargs):
super(self, ProjectsForm).init(*args, **kwargs)
employee = get_object_or_404(Employee, user=user)
self.fields['name'].queryset = employee.projects_set.all()
Now, some explanation. Hope someone will find it useful.
In your original ProjectsForm definition, you're trying to get employee's projects when your class is defined. But this happens once your forms.py file is compiled, which takes place rarely (when you change code, for example). Also, you of course have no data that is necessary to filter projects, i.e., no user object, at that stage.
Instead, you need to do this each time the class is initialized. Initialization in Python takes place in special __init__() method. For example, when you're doing something like
form = ProjectsForm(data=request.POST)
in your view, what happens is that ProjectsForm.__init__(data=request.POST) is called to initialize ProjectsForm into an instance.
So, in our new definition, we're requiring a new argument (user) to be passed to the form when it's instantiated. So you can do something like this in your view:
form = ProjectsForm(request.user, data=request.POST)
In new initialization method, first we're calling the initialization method of parent class (which does some internal django things and should be called anyway), then use argument user to get related employee, and then assign a queryset with that employee's projects to the name field.
Sorry if I'm being too verbose.
See also:
Django, passing information when modifying queryset for ModelForm
django - dynamic select fields in forms
Why not have a many to many field in the Employee model that points to all the projects the employee can work on?
https://docs.djangoproject.com/en/dev/ref/models/fields/#django.db.models.ManyToManyField
I think that would be the best way to do it.

Categories