Django admin change list integration with change form - python

Basically I have a structure of 3 models in the order:
class Model_A(models.Model):
# (actually, it doesn't matter what type m_aid is...)
m_aid = models.IntegerField(primary_key=True)
# ...
class Model_B(models.Model):
m_bid = models.IntegerField(primary_key=True) # same as previous
m_a = models.ForeignKey('Model_A', db_column='m_aid')
# ...
class Model_C(models.Model):
m_cid = models.IntegerField(primary_key=True) # same as the previous 2
m_b = models.ForeignKey('Model_B', db_column='m_bid')
# ...
What I'm looking for is to use the admin change form templates with a slight difference: in the change_form of Model_A I want to show the change list of its child - Model_B and in the change_form of Model_B I would like to put the change list of Model_C.
Any ideas how to do it?
P.S You might see it as a silly question, but I'm new to django(and python)

You can use inline models to accomplish that.
They will allow you to edit one or more instances of "Model B" when editing an instance of "Model A" and so on. It's not identical to what you see in the change list, but there are two built-in layouts, stacked and tabular, for inlines. Tabular will get you closest in appearance to what you want.

Related

Django - create child models

Let say that the model name is BHA, and I populate this field. In Django-Admin homepage, I will have a tab looks like this:
MY_APP_NAME
BHA List
Other Model 1
Other Model 2
Upon clicking BHA List, I will be navigate to a page that has a list of populated BHA:
BHA List
BHA_1
BHA_2
BHA_3
BHA_4
And each BHA needs a separate table that has their own information. So all BHA's (BHA_1, BHA_2, BHA_3, BHA_4) will have exact same child field Bit data, Sensor Data, Component Data. And Each of these sub-fields will have its own subfields too. How should I design my models.py to make this work? Can anyone provide any example code set that enables this feature?
So far I know only a really basic models.py structure that looks like this:
class SomeModel(models.Model):
field_1 = models.CharField(max_length=100, primary_key=True)
field_2 = models.CharField(max_length=100)
field_3 = models.CharField(max_length=100)
Technically, these are not child classes. They have no inheritance. If I understand you correctly, you will have to use ForeignKey.
BHA(models.Model):
bha_name = models.CharField(params)
BitData(models.Model):
bha = models.ForeignKey(params with reference to BHA)
model_field = models.CharField(params)
SensorData(models.Model):
bha = models.ForeignKey(params with reference to BHA)
model_field = models.CharField(params)
To see them on your page the way you want to will probably involve changing the widget that is used.
You will also have to reference all of your models on the page. Multiple Models Form

dependent multi object validation in django admin

Each "time range" entry of the TimeClass is dependent on each other.
They cannot overlap and start_time < end_time.
models.py
class Xyz(models.Model):
...
class TimeRangeClass(models.Model)
start_time = models.TimeField()
end_time = models.TimeField()
xyz = models.ForeignKey(Xyz)
# other fields here
def clean(self):
# Here I loop through TimeRangeClass.objects.all() and
# check for conflicts through my custom "my_validator_method".
# If there is a conflict I throw an error
#(I've since modified it to just be one single query as per Titusz advice)
for each in TimeRangeClass.objects.filter(xyz=self.xyz).exclude(id=self.id):
my_validator_method(start_time1=self.start_time,
end_time1=self.end_time,
start_time2=each.start_time,
end_time2=each.end_time)
admin.py
from .models import TimeRangeClass, Xyz
class TimeRangeClassInLine(admin.TabularInline):
model = TimeRangeClass
extra = 3
#admin.register(Xyz)
class Xyz(admin.ModelAdmin):
exclude = []
inlines = [TimeRangeClassInLine]
Problem: I can edit/add multiple TimeRangeClass's at once through the admin. But given that the models.Model clean method only evaluates 1 change at a time I can't validate multiple edits against each other.
Example:
Save an Entry1 & Entry2 without conflict
Change Entry2 to produce a validation error
Adjust Entry1 (instead of #2) so they do not overlap
This doesn't register because neither changes are written to the db.
I'm looking for a workaround.
Some hints on the problem:
You should not iterate over the full table when checking for overlapping rows. Just filter for the problematic rows... something like:
overlaps = TimeRangeClass.objects.filter(
Q(start_time__gte=self.start_time, start_time__lt=self.end_time) |
Q(end_time__gt=self.start_time, end_time__lte=self.end_time)
)
overlaps is now a queryset that evaluates when you iterate over it and only returns the conflicting objects.
If you are using Django with postgres you should check out https://docs.djangoproject.com/es/1.9/ref/contrib/postgres/fields/#datetimerangefield.
Once you have the conflicting objects you should be able to change their start and end times within the function and save the changes. Model.save() will not automatically call the model.clean() method. But be aware, if you save an object from the Django admin it will call the model.clean() method before saving.
So something like that:
def clean():
overlaps = TimeRangeClass.overlaps.for_trc(self)
for trc_object in overlaps:
fixed_object = fix_start_end(trc_object, self)
fixed_object.save()
If you feel brave you should also read up on transactions to make the mutation of multiple objects in the database all succeed or all fail and nothing in between.
def clean():
with transaction.atomic():
# do your multi object magic here ...
Update on clarified question:
If you want to validate or pre/process data that comes from admin inlines you have to hook into the corresponding ModelAdmin method(s). There are multiple ways to approach this. I guess the easiest would be to override ModelAdmin.save_fromset. Here you have access to all the inlineforms before they have been saved.

Display icons in Django admin for each item

I want to add a specified icon for each model on admin index page. I added an attribute named "picture" on each model then I modified /contrib/admin/sites.py to pass that picture name to template and checked and use it on index.html template of admin to get the result.
I wonder to know if there is a better way
class Product(models.Model):
abbr = models.CharField(max_length=20,unique=True)
title = models.CharField(max_length=200,unique=True)
owner = models.ForeignKey(UserProxy)
des = models.TextField(blank=True,null=True)
picture = 'product.png'
def __unicode__(self):
return self.abbr
class Meta:
none
What You did seems OK, only small tips that can make Your code a little better:
Instead of modifying django/contrib/admin/sites.py You can subclass the AdminSite class (if You didn't do that already).
Modify the AdminSite.index() method to pass not picture, but whole admin class (there is a model_admin variable available in the index() method).
Assign picture in the ModelAdmin classes, not models, to separate admin stuff from models.
The answer from 'Python Fanboy' was so brief but useful to me, I could avoid modifying django base classes
picture field were moved to admin class
I subclass AdminSite as CustomAdminSite and copied index and app_index and made modification
(I don't know if there is a better way than copying whole of index and app_index like overriding)
In urls.py, 'admin.sites.url' replaced by 'custom_site.url'
(custom_site is instance of CustomAdminSite)
I did not want another url instead of '/admin' like '/my_admin' so I have to use instance of CustomAdminSite for all my models even User, Group & Site
I'm using admin_tools, now I've lose my application menu
Any better Idea or solution for my new encountered problems?

I want to write a generic function to pair two database fields

Let's say that I have two teams, "red" and "black". And let's say that I have a Story class, which presents similar information in two very different ways, depending on your team:
class Story(models.Model):
red_title = models.CharField()
black_title = models.CharField()
red_prologue = models.TextField()
black_prologue = models.TextField()
# ... and so on ...
def get_field(self, genericName, team):
"""Return the field with suffix genericName belonging to the given team.
>>>self.get_field("prologue", "red") is self.red_prologue
True
>>>self.get_field("title", "black") is self.black_title
True
"""
assert(team in ["red", "black"])
specificName = "{}_{}".format(team, genericName)
return self.__dict__[specificName]
I'm happy with the getter function, but I feel like I should be able to refactor the code which created the fields in the first place. I'd like a function that looks something like this:
def make_fields(self, genericName, fieldType, **kwargs):
"""Create two fields with suffix genericName.
One will be 'red_{genericName}' and one will be 'black_{genericName}'.
"""
for team in ["red", "black"]:
specificName = "{}_{}".format(team, genericName)
self.__dict__[specificName] = fieldType(**kwargs)
But self and __dict__ are meaningless while the class is first defined, and I think Django requires that database fields be class variables rather than instance variables.
So... is there some way to create this make_fields function within Django, or am I out of luck?
Not sure why you're even doing this. A much more sane model would be:
TEAMS = (
("r","red"),
("b","black"),
)
class Story(models.Model):
team = models.CharField(max_length=1, choices=TEAMS)
title = models.CharField()
prologue = models.TextField()
Your current model is creating lots of duplicate columns (for red and black) that should just be defined by a column itself. Using the model above, you queries would be like Story.objects.filter(team="r").
You then wouldn't need your get_field function at all.
No. A Django model shouldn't be treated as something that can be dyamically constructed; it's a Python representation of a database table. For instance, what would be the semantics of changing the format of specificName after you had already run syncdb? There's no definitive, obvious answer - so Django doesn't try to answer it. You columns are defined at the class level, and that's that.
(At some level, you can always drill into the internal ORM data structures and set up these fields - but all you're doing is opening yourself up to a world of ambiguity and not-well-defined problems. Don't do it.)

Ordered ManyToManyField that can be used in fieldsets

I've been working through an ordered ManyToManyField widget, and have the front-end aspect of it working nicely:
Unfortunately, I'm having a great deal of trouble getting the backend working. The obvious way to hook up the backend is to use a through table keyed off a model with ForeignKeys to both sides of the relationship and overwrite the save method. This would work great, except that due to idiosyncrasies of the content, it is an absolute requirement that this widget be placed in a fieldset (using the ModelAdmin fieldsets property), which is apparently not possible.
I'm out of ideas. Any suggestions?
Thanks!
In regard to how to set up the models, you're right in that a through table with an "order" column is the ideal way to represent it. You're also right in that Django will not let you refer to that relationship in a fieldset. The trick to cracking this problem is to remember that the field names you specify in the "fieldsets" or "fields" of a ModelAdmin do not actually refer to the fields of the Model, but to the fields of the ModelForm, which we are free to override to our heart's delight. With many2many fields, this gets tricky, but bear with me:
Let's say you're trying to represent contests and competitors that compete in them, with an ordered many2many between contests and competitors where the order represents the competitors' ranking in that contest. Your models.py would then look like this:
from django.db import models
class Contest(models.Model):
name = models.CharField(max_length=50)
# More fields here, if you like.
contestants = models.ManyToManyField('Contestant', through='ContestResults')
class Contestant(models.Model):
name = models.CharField(max_length=50)
class ContestResults(models.Model):
contest = models.ForeignKey(Contest)
contestant = models.ForeignKey(Contestant)
rank = models.IntegerField()
Hopefully, this is similar to what you're dealing with. Now, for the admin. I've written an example admin.py with plenty of comments to explain what's happening, but here's a summary to help you along:
Since I don't have the code to the ordered m2m widget you've written, I've used a placeholder dummy widget that simply inherits from TextInput. The input holds a comma-separated list (without spaces) of contestant IDs, and the order of their appearance in the string determines the value of their "rank" column in the ContestResults model.
What happens is that we override the default ModelForm for Contest with our own, and then define a "results" field inside it (we can't call the field "contestants", since there would be a name conflict with the m2m field in the model). We then override __init__(), which is called when the form is displayed in the admin, so we can fetch any ContestResults that may have already been defined for the Contest, and use them to populate the widget. We also override save(), so that we can in turn get the data from the widget and create the needed ContestResults.
Note that for the sake of simplicity this example omits things like validation of the data from the widget, so things will break if you try to type in anything unexpected in the text input. Also, the code for creating the ContestResults is quite simplistic, and could be greatly improved upon.
I should also add that I've actually ran this code and verified that it works.
from django import forms
from django.contrib import admin
from models import Contest, Contestant, ContestResults
# Generates a function that sequentially calls the two functions that were
# passed to it
def func_concat(old_func, new_func):
def function():
old_func()
new_func()
return function
# A dummy widget to be replaced with your own.
class OrderedManyToManyWidget(forms.widgets.TextInput):
pass
# A simple CharField that shows a comma-separated list of contestant IDs.
class ResultsField(forms.CharField):
widget = OrderedManyToManyWidget()
class ContestAdminForm(forms.models.ModelForm):
# Any fields declared here can be referred to in the "fieldsets" or
# "fields" of the ModelAdmin. It is crucial that our custom field does not
# use the same name as the m2m field field in the model ("contestants" in
# our example).
results = ResultsField()
# Be sure to specify your model here.
class Meta:
model = Contest
# Override init so we can populate the form field with the existing data.
def __init__(self, *args, **kwargs):
instance = kwargs.get('instance', None)
# See if we are editing an existing Contest. If not, there is nothing
# to be done.
if instance and instance.pk:
# Get a list of all the IDs of the contestants already specified
# for this contest.
contestants = ContestResults.objects.filter(contest=instance).order_by('rank').values_list('contestant_id', flat=True)
# Make them into a comma-separated string, and put them in our
# custom field.
self.base_fields['results'].initial = ','.join(map(str, contestants))
# Depending on how you've written your widget, you can pass things
# like a list of available contestants to it here, if necessary.
super(ContestAdminForm, self).__init__(*args, **kwargs)
def save(self, *args, **kwargs):
# This "commit" business complicates things somewhat. When true, it
# means that the model instance will actually be saved and all is
# good. When false, save() returns an unsaved instance of the model.
# When save() calls are made by the Django admin, commit is pretty
# much invariably false, though I'm not sure why. This is a problem
# because when creating a new Contest instance, it needs to have been
# saved in the DB and have a PK, before we can create ContestResults.
# Fortunately, all models have a built-in method called save_m2m()
# which will always be executed after save(), and we can append our
# ContestResults-creating code to the existing same_m2m() method.
commit = kwargs.get('commit', True)
# Save the Contest and get an instance of the saved model
instance = super(ContestAdminForm, self).save(*args, **kwargs)
# This is known as a lexical closure, which means that if we store
# this function and execute it later on, it will execute in the same
# context (i.e. it will have access to the current instance and self).
def save_m2m():
# This is really naive code and should be improved upon,
# especially in terms of validation, but the basic gist is to make
# the needed ContestResults. For now, we'll just delete any
# existing ContestResults for this Contest and create them anew.
ContestResults.objects.filter(contest=instance).delete()
# Make a list of (rank, contestant ID) tuples from the comma-
# -separated list of contestant IDs we get from the results field.
formdata = enumerate(map(int, self.cleaned_data['results'].split(',')), 1)
for rank, contestant in formdata:
ContestResults.objects.create(contest=instance, contestant_id=contestant, rank=rank)
if commit:
# If we're committing (fat chance), simply run the closure.
save_m2m()
else:
# Using a function concatenator, ensure our save_m2m closure is
# called after the existing save_m2m function (which will be
# called later on if commit is False).
self.save_m2m = func_concat(self.save_m2m, save_m2m)
# Return the instance like a good save() method.
return instance
class ContestAdmin(admin.ModelAdmin):
# The precious fieldsets.
fieldsets = (
('Basic Info', {
'fields': ('name', 'results',)
}),)
# Here's where we override our form
form = ContestAdminForm
admin.site.register(Contest, ContestAdmin)
In case you're wondering, I had ran into this problem myself on a project I've been working on, so most of this code comes from that project. I hope you find it useful.

Categories

Resources