ModelForm with user-defined fields and choices from foreign keys - python

In my app, I have Study as the central model. Studys have multiple Strata, and each Strata has multiple Levels. Users create Allocations for a study by choosing one level for each Strata linked to that study, like:
class Study(models.Model):
name = models.CharField(max_length=100)
class Stratum(models.Model):
study = models.ForeignKey(Study, on_delete=models.CASCADE, related_name='strata')
name = models.CharField(max_length=100)
class Level(models.Model):
stratum = models.ForeignKey(Stratum, on_delete=models.CASCADE, related_name='levels')
label = models.CharField(max_length=100)
class Allocation(models.Model):
study = models.ForeignKey(Study, on_delete=models.CASCADE, related_name='allocations')
code = models.CharField(blank=False, max_length=100)
levels = models.ManyToManyField(Level, related_name='allocations')
In order to create fields for the allocation creation form, I am currently finding all the Strata and associated levels in the form's constructor, but hiding the strata as the user doesn't interact with them:
class AllocationForm(forms.ModelForm):
class Meta:
model = Allocation
fields = ('code',)
def __init__(self, *args, **kwargs):
study = kwargs.pop('study')
super(AllocationForm, self).__init__(*args, **kwargs)
strata = Stratum.objects.filter(study=study)
for stratum in strata:
self.fields[stratum.name] = forms.IntegerField(
widget=forms.HiddenInput()
)
self.fields[stratum.name].initial = stratum.id
self.fields[stratum.name].disabled = True
self.fields[stratum.name + '_level'] = forms.ModelChoiceField(
queryset=Level.objects.filter(stratum=stratum)
)
Is this a safe and sensible way to attach the associated objects to the form? I worry that I will lose track of the connection between Strata and Levels when trying to create the allocation. Is this something that would be better performed in the view?

Related

Django multiple relations in one model

I have been trying to create a model that could represent the form as it is, tried creating an EntryForm model which is linked to EntryFormTable where then each column in the table is a model class all linked to the table, but then this proved to be a long way and one that doesn't even work, maybe there's a short or even a working method to represent this in django models,
It is recommended to model the "things" as they are in real life, and not as they would appear on the screen. So don't create a model called EntryForm, EntryFormTable or EntryFormColumn, but rather name them what they are. Example based on your image:
class CoveredWorkSet(Model):
school: CharField()
learning_area: CharField()
teacher: ForeignKey(Teacher) # or just CharField if you don't have them in your database
role:
grade: CharField()
class CoveredWork(Model):
covered_work_set = ForeignKey(CoveredWorkSet, related_name='records')
date = DateField()
lesson = CharField()
work_done = BooleanField()
reflection = TextField()
class Signature(Model):
"""
Represents a signature on either a CoveredWork record or a complete CoveredWorkSet
"""
ROLE_CHOICES = [
('subject', 'Subject teacher'),
('class', 'Class teacher'),
('head', 'Head teacher'),
]
teacher = ForeignKey(Teacher, related_name='signatures')
role = CharField(choices=ROLE_CHOICES)
covered_work_set = ForeignKey(CoveredWorkSet, null=True)
covered_work = ForeignKey(CoveredWor, null=True)
date = DateTimeField()
signature = ImageField()

Not sure I understand dependancy between 2 django models

I am struggling to understand django models relationship.
I have this arborescence:
A train have cars, and those cars are divided into parts. Then those parts all contains different references.
Like, for exemple, all the trains have the 6 cars, and the cars 6 parts. Each part have x reference to be associated.
I would like to use all of them in a template later on, where the user can select the train, the car and the part he worked on, then generate a table from his selections with only the references associated to the parts he selected.
It should update the train and the car (I'm trying to update a stock of elements for a company)
I dont really understand which model field give to each of them. After checking the doc, Ive done something like this but i am not convinced:
class Train(Car):
train = models.CharField(max_length=200)
id = models.CharField(primary_key='True', max_length=100)
selected = models.BooleanField()
class Meta:
abstract = True
class Car(Part):
car = models.CharField(max_length=200)
id = models.CharField(primary_key='True', max_length=100)
selected = models.BooleanField()
class Meta:
abstract = True
class Part(Reference):
part = models.CharField(max_length=200)
id = models.CharField(primary_key='True', max_length=100)
selected = models.BooleanField()
class Meta:
abstract = True
class Reference(models.Model):
reference = models.CharField(max_length=200)
id = models.CharField(primary_key='True', max_length=100)
selected = models.BooleanField()
def __str__(self):
return self.reference
Can someone please help me understand this so I can do well ? Thanks!!
1-)if you add abstract = True in your Model Meta class, your class doesn't created on database as a table. If you store data for any class, you mustn't define abstract = True.
2-)For relations, you can use models.ForeignKey . If you add a class into brackets of another class, it names: inheritance.(You can think like parent-child relation). In database management, we can use foreignkey for one-to-many relationship.
3-)In Django ORM, id field automatically generated. So you don't need to define id field.
If I understand correctly, also you want to store parts of user's selected.
So, your model can be like that:
class Train(models.Model):
name = models.CharField(max_length=200) # I think you want to save name of train
class Car(models.Model):
train = models.ForeignKey(Train,on_delete=models.Cascade)
name = models.CharField(max_length=200)
class Part(models.Model):
car = models.ForeignKey(Car,on_delete=models.Cascade)
name = models.CharField(max_length=200)
class Reference(models.Model):
part = models.ForeignKey(Part,on_delete=models.Cascade)
name = models.CharField(max_length=200)
def __str__(self):
return self.reference
#addtional table for storing user's references
class UserReference(models.Model):
user = models.ForeignKey(User,on_delete=models.Cascade)
reference = models.ForeignKey(Reference,on_delete=models.Cascade)
name = models.CharField(max_length=200)
With this definitions, you can store user's definition on UserReference table. And with Django Orm, you can access train object from UserReferenceObject.
#user_reference: UserReference object like that result of UserReference.objects.first()
user_reference.reference.part.car.train.name

Pulling several Django models together into a single list

I have a MySQL database with four related tables: project, unit, unit_equipment, and equipment. A project can have many units; a unit can have many related equipment entries. A single unit can only belong to one project, but there is a many-to-many between equipment and unit (hence the unit_equipment bridge table in the DB). I'm using Django and trying to create a view (or a list?) that shows all 3 models on the same page, together. So it would list all projects, all units, and all equipment. Ideally, the display would be like this:
Project --------- Unit ------------- Equipment
Project 1 first_unit some_equipment1, some_equipment2
Project 1 second_unit more_equipment1, more_equipment2
Project 2 another_unit some_equipment1, more_equipment1
Project 2 and_another_unit some_equipment2, more_equipment2
but at this point I'd also be happy with just having a separate line for each piece of equipment, if comma-separating them is a pain.
Although it seems straightforward to create a form where I can add a new project and add related unit and equipment data (using the TabularInline class), I cannot for the life of me figure out how to bring this data together and just display it. I just want a "master list" of everything in the database, basically.
Here's the code I have so far:
models.py
class Project(models.Model):
name = models.CharField(max_length=255, blank=True, null=True)
class Meta:
managed = False
db_table = 'project'
def __str__(self):
return self.name
class Unit(models.Model):
project = models.ForeignKey(Project, models.DO_NOTHING, blank=True, null=True)
name = models.CharField(max_length=255, blank=True, null=True)
class Meta:
managed = False
db_table = 'unit'
def __str__(self):
return self.name
class UnitEquipment(models.Model):
unit = models.ForeignKey(Unit, models.DO_NOTHING, blank=True, null=True)
equipment = models.ForeignKey(Equipment, models.DO_NOTHING, blank=True, null=True)
class Meta:
managed = False
db_table = 'unit_equipment'
class Equipment(models.Model):
name = models.CharField(max_length=100, blank=True, null=True)
description = models.CharField(max_length=255, blank=True, null=True)
class Meta:
managed = False
db_table = 'equipment'
def __str__(self):
return self.name
views.py
def project_detail_view(request):
obj = Project.objects.all()
context = {'object': obj}
return render(request, "project/project_detail.html", context)
urls.py
urlpatterns = [
path('project/', project_detail_view),
path('', admin.site.urls),
]
admin.py
class UnitTabularInLine(admin.TabularInline):
model = Unit
extra = 0
class ProjectAdmin(admin.ModelAdmin):
inlines = [UnitTabularInLine]
class Meta:
model = Project
# a list of displayed columns name.
list_display = ['name']
# define search columns list, then a search box will be added at the top of list page.
search_fields = ['name']
# define filter columns list, then a filter widget will be shown at right side of list page.
list_filter = ['name']
# define model data list ordering.
ordering = ('name')
I think I need to somehow add more entries to the list_display in the admin file, but every time I try to add unit or equipment it throws an error. I've also tried adding more attributes to Project, but I can't seem to get the syntax right, and I'm never sure which model class I'm supposed to make it.
I've also looked at FormSets, but I cannot get my head around how to alter my current code to get it to work.
How do I get these models together into a unified view?
You don't need to edit the admin view to add your own view: which you may find you are able to do in this case to get your data displayed exactly as you want.
If you do want to show the related object values in the admin list, then you can use lookups and custom columns: however in this case your list would be based upon the Unit.
# You don't need an explicit UnitEquipment model here: you can
# use a simple ManyToManyField
class Unit(models.Model):
project = ...
name = ...
equipment = models.ManyToManyField(Equipment, related_name='units')
def equipment_list(admin, instance):
return ', '.join([x.name for x in instance.equimpent.all()])
class UnitAdmin(admin.ModelAdmin):
class Meta:
model = Unit
list_display = ['project__name', 'name', equipment_list]
def get_queryset(self, request):
return super().get_queryset(request)\
.select_related('project')\
.prefetch_related('equipment')
Note that you need to have the queryset override, otherwise there will be a bunch of extra queries as each unit also requires fetching the project and list of equipment for that unit.
There's also a further improvement you can make to your queries: you could aggregate the related equipment names using a Subquery annotation, and prevent the second query (that fetches all related equipment items for the units in the queryset). This would replace the prefetch_related()
Thanks to #Matthew Schinckel, I was able to find my way to the answer. Here's what my files look like now (only edited the Unit class in models.py):
models.py
class Unit(models.Model):
project = models.ForeignKey(Project, models.DO_NOTHING, blank=True, null=True)
name = models.CharField(max_length=255, blank=True, null=True)
equipment = models.ManyToManyField(Equipment, related_name='units')
class Meta:
managed = False
db_table = 'unit'
def __str__(self):
return self.name
def equipment_list(self):
return ', '.join([x.name for x in self.equipment.all()])
admin.py
class UnitAdmin(admin.ModelAdmin):
class Meta:
model = Unit
# a list of displayed columns name.
list_display = ('project', 'name', 'equipment_list')
# define search columns list, then a search box will be added at the top of list page.
search_fields = ['project']
# define filter columns list, then a filter widget will be shown at right side of list page.
list_filter = ['project', 'name']
# define model data list ordering.
ordering = ('project', 'name')
def get_queryset(self, request):
return super().get_queryset(request)\
.select_related('project')\
.prefetch_related('equipment')
So the changes I made were:
1. Make list_display a tuple instead of a list.
2. Throw def equipment_list(self) into the Unit class (so it's callable as an attribute of Unit) and pass (self) instead of (admin, instance) (I kept getting an error that was looking for the instance argument).

Django: MultiChoiceField does not show saved choices added after creation

I am currently trying to create a dynamic product model that will allow admins to create add their own "option sets" to products.
For example, Product A has flap valve with 400mm, 500mm and 600mm widths available.
To facilitate this I have created 3 models.
models.py
# A container that can hold multiple ProductOptions
class ProductOptionSet(models.Model):
title = models.CharField(max_length=20)
# A string containing the for the various options available.
class ProductOption(models.Model):
value = models.CharField(max_length=255)
option_set = models.ForeignKey(ProductOptionSet)
# The actual product type
class HeadwallProduct(Product):
dimension_a = models.IntegerField(null=True, blank=True)
dimension_b = models.IntegerField(null=True, blank=True)
# (...more variables...)
flap_valve = models.CharField(blank=True, max_length=255, null=True)
...and a form...
forms.py
class HeadwallVariationForm(forms.ModelForm):
flap_valve = forms.MultipleChoiceField(required=False, widget=forms.SelectMultiple)
def __init__(self, *args, **kwargs):
super(HeadwallVariationForm, self).__init__(*args, **kwargs)
self.fields['flap_valve'].choices = [(t.id, t.value) for t in ProductOption.objects.filter(option_set=1)]
def save(self, commit=True):
instance = super(HeadwallVariationForm, self).save(commit=commit)
return instance
class Meta:
fields = '__all__'
model = HeadwallProduct
This works fine for during the initial creation of a product. The list from the MultipleChoiceForm is populated with entries from the ProductOptionSet and the form can be saved.
However, when the admin adds a 700mm flap valve as an option to the ProductOptionSet of Product A things fall apart. Any new options will show up in the admin area of the existing product - and will even be persisted to the database when the product is saved - but they will not be shown as selected in the admin area.
If a Product B is created the new options work as intended, but you cannot add new options to an existing product.
Why does this happen and what can I do to fix it? Thanks.
Urgh... after about 4 hours I figured it out...
Changing:
class ProductOption(models.Model):
value = models.CharField(max_length=20)
option_set = models.ForeignKey(ProductOptionSet)
to
class ProductOption(models.Model):
option_value = models.CharField(max_length=20)
option_set = models.ForeignKey(ProductOptionSet)
Fixed my issue.

Additional variable passed to django formset

I have a quite peculiar issue, regarding creating a formset out of a certain form. The thing is, that the form has to has an undisclosed number of fields, so I want to pass a variable to that form, which will take a proper model from database, and create proper fields within form's init method.
StrangeForm(forms.Form):
def __init__(self, id, *args, **kwargs):
super(StrangeForm, self).__init__(*args, **kwargs)
# get the form's base entry
self.certain_entry = Certain_Entry.objects.get(id=id)
# create a hidden input to keep the id
self.fields["id"] = forms.HiddenInput()
self.fields["id"].initial = id
# initiate text field
self.fields["text_field"] = forms.CharField(widget=forms.TextInput(attrs={'style': 'width:95%', }),
required=False)
self.fields["text_field"].initial = self.certain_entry.text
# create and initiate fields for undisclosed number of subentries
subordinates = Certain_Entry_Subordinates.objects.filter(foreign_key=self.certain_entry)
for each in subordinates:
the_name = "%d_%s_%s" % (each.further.id, each.further.name)
self.fields[the_name] = forms.ChoiceField(choices=(("Ok", "Ok"), ("Not Ok", "Not Ok"), ("Maybe" "Maybe"),
required=False)
self.fields[the_name].initial = each.option
The models are as follows:
class Certain_Entry(models.Model):
text = models.TextField(max_length=128, null=True, blank=True)
class Certain_Entry_Subordinates(models.Model):
certain_entry = models.ForeignKey(Certain_Entry, null=False, blank=False)
option = models.TextField(max_length=8, null=True, blank=True)
further = models.ForeignKey(Further, null=False, blank=False)
class Further(models.Model):
name = models.TextField(max_length=32, null=True, blank=True)
So as you see, I need to pass this ID into the form. When it's a single form, it's all OK, but I can't find any information as to how create a formset with forms that require a variable. Any ideas?
PS: As to WHY... Don't ask, I just need to do it that way, trust me. I'm an engineer.

Categories