I'm working on a site using Django and need help with data flow and accessing the foreign key data.
models.py
class Boards(models.Model):
board_no = models.CharField('Board No', max_length=10, unique=True)
board_name = models.CharField('Board Name', max_length=100)
class BoardLocations(models.Model):
board = models.ForeignKey(Boards, on_delete=models.CASCADE, related_name='board')
location_code = models.CharField('Location Code', max_length=10, null=True)
class BoardContacts(models.Model):
board_location = models.ForeignKey(BoardLocations, on_delete=models.CASCADE)
contact_first_name = models.CharField('Contact First Name', max_length=100, null=True)
contact_last_name = models.CharField('Contact Last Name', max_length=100, null=True)
The BoardContacts are tied to a BoardLocation which all come from a Board. I'm displaying the list of BoardContacts properly through a django-tables2 as follows:
List of Contacts
The URL shows:
http://127.0.0.1:8000/adm/boards/view_data_contact_list/6/
where 6 is Board.id. All fine. The Contact First Name link is defined as:
tables.py
class BoardContactsTableView(tables.Table):
selection = tables.CheckBoxColumn(accessor='id')
contact_first_name = tables.LinkColumn('view_board_contact', args=[A('id')], attrs=TABLE_VIEW_ATTRS)
contact_last_name = tables.LinkColumn('view_board_contact', args=[A('id')], attrs=TABLE_VIEW_ATTRS)
class Meta:
model = BoardContacts
template_name = TABLE_TEMPLATE_NAME
attrs = TABLE_ATTRIBUTES
ordering = ['contact_last_name']
fields = ('selection', 'contact_first_name', 'contact_last_name', 'contact_type', 'access_type', 'last_login', 'create_date')
When I select Contact First Name or Last Name, the URL changes to:
http://127.0.0.1:8000/adm/boards/view_data_contact/29/
because source indicates:
<td ><a class="text-decoration-none fw-bold" style="color:gray" href="/adm/boards/view_data_contact/29/">Robert</a></td>
But 29 is the BoardContacts.id. I need to maintain the Board.id of 6 within the URL so I can go back to the list of contacts using the Board Contacts button on the web page. Ideally, it should look like this:
http://127.0.0.1:8000/adm/boards/view_data_contact/6/29/
So, how do I make changes to LinkColumn within tables.py to include 6 and 29?
Instead of rendering as a LinkColumn, you can use a regular Column and build the full url in a render function.
https://django-tables2.readthedocs.io/en/latest/pages/custom-data.html#table-render-foo-methods
def render_contact_first_name(self, record, value):
url = reverse("view_board_contact", args=(record.board_location.board.id, record.id))
return mark_safe(f'{value}')
Related
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).
I have a model where I am using Enum for choices:
class Agreement(models.Model):
class Category(enum.Enum):
EULA = 0
PROVIDER = 1
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
category = models.IntegerField(
choices=[(choice.name, choice.value)
for choice in Category])
title = models.CharField(max_length=128)
content = models.TextField()
I register it using simple admin site registration:
admin.site.register(Agreement)
When admin site renders the object it doesn't allow me to save it? Has anyone had a similar issue?
According to the documentation:
The first element in each tuple is the actual value to be set on the model, and the second element is the human-readable name.
name and value should be the other way around, like this:
category = models.IntegerField(
choices=[(choice.value, choice.name)
for choice in Category])
because category is an integer field and name returns a string.
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.
I'm making a program that helps log missions in a game. In each of these missions I would like to be able to select a number of astronauts that will go along with it out of the astronauts table. This is fine when I only need one, but how could I approach multiple foreign keys in a field?
I currently use a 'binary' string that specifies which astronauts are to be associated with the mission (1 refers to Jeb, but not Bill, Bob, or Val and 0001 means only Val), with the first digit specifying the astronaut with id 1 and so forth. This works, but it feels quite clunky.
Here's the model.py for the two tables in question.
class astronauts(models.Model):
name = models.CharField(max_length=200)
adddate = models.IntegerField(default=0)
experience = models.IntegerField(default=0)
career = models.CharField(max_length=9, blank=True, null=True)
alive = models.BooleanField(default=True)
def __str__(self):
return self.name
class Meta:
verbose_name_plural = "Kerbals"
class missions(models.Model):
# mission details
programid = models.ForeignKey(programs, on_delete=models.SET("Unknown"))
missionid = models.IntegerField(default=0)
status = models.ForeignKey(
missionstatuses, on_delete=models.SET("Unknown"))
plan = models.CharField(max_length=1000)
# launch
launchdate = models.IntegerField(default=0)
crewmembers = models.IntegerField(default=0)
# recovery
summary = models.CharField(max_length=1000, blank=True)
recdate = models.IntegerField(default=0)
def __str__(self):
return str(self.programid) + '-' + str(self.missionid)
class Meta:
verbose_name_plural = "Missions"
I saw a post about an 'intermediate linking table' to store the crew list but that also isn't ideal.
Thanks!
This is the use case for Django's ManyToManyField. Change the appropriate field on the missions:
class missions(models.Model):
crewmembers = models.ManyToManyField('astronauts')
You can access this from the Astronaut model side like so:
jeb = astronaut.objects.get(name='Jebediah Kerman')
crewed_missions = jeb.missions_set.all()
Or from the mission side like so:
mission = missions.objects.order_by('?')[0]
crew = mission.crewmembers.all()
This creates another table in the database, in case that is somehow a problem for you.
I have a situation where I want to create a menu item in the database whenever a record is created.
I need to design a component that will create the mentioned menu item.
I am using django-sitetree as the basic app for the menu.
It has the following model:
class TreeItem(models.Model):
PERM_TYPE_ANY = 1
PERM_TYPE_ALL = 2
PERM_TYPE_CHOICES = (
(PERM_TYPE_ANY, _('Any')),
(PERM_TYPE_ALL, _('All'))
)
title = models.CharField(_('Title'), max_length=100, help_text=_('Site tree item title. Can contain template variables E.g.: {{ mytitle }}.'))
hint = models.CharField(_('Hint'), max_length=200, help_text=_('Some additional information about this item that is used as a hint.'), blank=True, default='')
url = models.CharField(_('URL'), max_length=200, help_text=_('Exact URL or URL pattern (see "Additional settings") for this item.'), db_index=True)
urlaspattern = models.BooleanField(_('URL as Pattern'), help_text=_('Whether the given URL should be treated as a pattern.<br /><b>Note:</b> Refer to Django "URL dispatcher" documentation (e.g. "Naming URL patterns" part).'), db_index=True, default=False)
tree = models.ForeignKey(Tree, verbose_name=_('Site Tree'), help_text=_('Site tree this item belongs to.'), db_index=True)
hidden = models.BooleanField(_('Hidden'), help_text=_('Whether to show this item in navigation.'), db_index=True, default=False)
alias = CharFieldNullable(_('Alias'), max_length=80, help_text=_('Short name to address site tree item from a template.<br /><b>Reserved aliases:</b> "trunk", "this-children", "this-siblings" and "this-ancestor-children".'), db_index=True, blank=True, null=True)
description = models.TextField(_('Description'), help_text=_('Additional comments on this item.'), blank=True, default='')
inmenu = models.BooleanField(_('Show in menu'), help_text=_('Whether to show this item in a menu.'), db_index=True, default=True)
inbreadcrumbs = models.BooleanField(_('Show in breadcrumb path'), help_text=_('Whether to show this item in a breadcrumb path.'), db_index=True, default=True)
insitetree = models.BooleanField(_('Show in site tree'), help_text=_('Whether to show this item in a site tree.'), db_index=True, default=True)
access_loggedin = models.BooleanField(_('Logged in only'), help_text=_('Check it to grant access to this item to authenticated users only.'), db_index=True, default=False)
access_restricted = models.BooleanField(_('Restrict access to permissions'), help_text=_('Check it to restrict user access to this item, using Django permissions system.'), db_index=True, default=False)
access_permissions = models.ManyToManyField(Permission, verbose_name=_('Permissions granting access'), blank=True)
access_perm_type = models.IntegerField(_('Permissions interpretation'), help_text='<b>Any</b> — user should have any of chosen permissions. <b>All</b> — user should have all chosen permissions.', choices=PERM_TYPE_CHOICES, default=PERM_TYPE_ANY)
# These two are for 'adjacency list' model.
# This is the current approach of tree representation for sitetree.
parent = models.ForeignKey('self', verbose_name=_('Parent'), help_text=_('Parent site tree item.'), db_index=True, null=True, blank=True)
sort_order = models.IntegerField(_('Sort order'), help_text=_('Item position among other site tree items under the same parent.'), db_index=True, default=0)
# More code here...
Is it reasonable to configure inmenu, inbreadcrumbs etc. in the model's Meta class?
Is there a better way to do this?
Is it reasonable to check if a there is field name called title and use it for the menu item's title first and then search for a function called get_menu_item_title in the model?
Or should I pass the field/callable to the component's constructor? Or maybe this should be defined in the meta class as well?
You can use Signals.
from django.db.models.signals import post_save
from django.dispatch import receiver
from myapp.models import Record, TreeItem
#receiver(post_save, sender=Record)
def my_handler(sender, instance, created, raw):
if created:
item = TreeItem() # ..
Whenever new record is created you can create another item.