Problem:
I have a project model and its inline models lets say InlineModel_1 and InlineModel_2.
I want the Project's Add & Change pages can create or edit the Project's fields and InlineModel_1.
Besides, I want an extra Change Page specific for review the Project info and edit InlineModel_2
Django 2.1.7 & Python 3.7
I'm new to Django, so pls advice if anything incorrect, thanks!
Solution Result:
In Changelist Page
Add a link to Original Chang Page and use original link to go to the customized Change Page
Original Change Page, similar to the Add page
New Customized Change Page for inline adding use
Procedure
Setup ProjectAdmin and add a link field to Original Change Page
class ProjectAdmin(BaseAdmin):
list_display = ('id', 'name', 'constructor', 'client',
'total_amount', 'edit_tag',
readonly_fields = ('total_amount', 'order_form', 'project_info_table')
# for add and edit use
default_fields = (
'name',
'constructor',
'client',
'total_amount',
)
# for review and add inline use
default_fieldset = (
('工程项目信息', {
'fields': ('project_info_table',)
}),
('报价单', {
'classes': ('collapse',),
'fields': ('order_form',),
}),
)
# a link to orginal Change Page
def edit_tag(self, obj):
return mark_safe(
f'Edit'
)
edit_tag.short_description = 'Edit'
Add method to ModelAdmin to generate Table Html with field values
class Project(BaseModel):
# ....
# generate table1 html
def project_info_table(self):
# just one row, use thead only instead of a whole table
table = """
<table style="width:100%">
<thead>
<tr>
<th>Project name</th>
<th>Constructor</th>
<th>Client</th>
<th>Total amount</th>
</tr>
</thead>
<tbody>
<tr>
<td>{}</td>
<td>{}</td>
<td>{}</td>
<td>{}</td>
</tr>
</tbody>
</table>
"""
return format_html(
table,
self.name,
self.constructor,
self.client,
self.total_amount()
)
project_info_table.short_description = '工程信息表'
# generate table2 html
def order_form(self):
table = """
<table style="width:100%">
<thead>
<tr>
<th>标号</th>
<th>类型</th>
<th>泵送</th>
<th>自卸</th>
</tr>
</thead>
<tbody>{}{}</tbody>
</table>
"""
return format_html(
table,
# format_html_join can repeat the row with the values
# from a iterator
format_html_join(
'\n', "<tr><td>{}</td><td>{}</td><td>{}</td><td>{}</td></tr>",
((
p.grade, p.type, p.pumpcrete, p.dumpcrete
) for p in self.products.all().order_by(
'-grade', 'type', 'pumpcrete'
))
),
format_html("<tr><td>泵送方数低于:{}</td><td>加出泵费:{}</td><td>方数低于:{}</td><td>加空载费:{}</td></tr>",
self.min_cube_pump,
self.price_pump,
self.min_cube_extra,
self.price_extra
)
)
order_form.short_description = '合同报价单'
# another readonly field
def total_amount(self):
result = self.transaction_set.filter(
is_deleted=False
).aggregate(
Sum('total_price')
)['total_price__sum'] or 0
return '¥ {:,}'.format(result)
If want to keep the original Change Page and the above Inline Change Page, override change_view method in admin to show
class ProjectAdmin(BaseAdmin):
....
# different fields and inline according to GET method's parameter
# need to reset fields and fieldset in different view
def change_view(self, request, object_id, form_url='',
extra_context=None):
if request.GET.get('edit', False):
# Show original Change Page
# Don't use fieldset cause it's only for customized page
self.fieldsets = None
self.fields = self.default_fields
# able to user different inline for different page
self.inlines = [DocumentInline]
else:
# Show customized page
# don't sue fields use fieldset instead
self.fields = None
self.fieldsets = self.default_fieldset
# use another inline for customized page
self.inlines = [TransactionInline]
return super().change_view(request, object_id,
extra_context=extra_context)
def add_view(self, request, form_url='', extra_context=None):
self.fieldsets = None
self.fields = self.default_fields
self.inlines = [DocumentInline]
return super(ProjectAdmin, self).add_view(request)
Related
I have a straight forward admin.ModelAdmin class with an inlines, of which I am overriding the form and formsets with a forms.Model and BaseInlineFormset to add a custom field. I have a custom jQuery script that gets loaded in and whenever a machine is picked from the select2 drop-down it runs an AJAX query to the REST API and grabs the items based on a foreign key value and populates the CleaningEntryInline with information. However, upon saving it is only posting a single record to the database.
class CleaningEntryInline(admin.TabularInline):
model = CleaningEntry
form = CleaningEntryForm
formset = CleaningEntryFormSet
extra = 0
raw_id_fields = ['cleaning_item']
fieldsets = [
(None,{'fields':[('cleaning_item','cleaning_action', 'checked', 'na', 'notes')]})
]
template = 'admin/quality/cleaningentry/edit_inline/tabular_actions.html'
class CleaningLogAdmin(admin.ModelAdmin):
####Save model function override to make and save QC Lab user and make uneditable.
def save_model(self, request, obj, form, change):
obj.lab_user = request.user.username
obj.save()
list_display = ['machine_used','get_product_info','lot_num','start_time','lab_user']
list_filter = ['machine_used']
readonly_fields = ['lab_user', 'cleaning_users']
search_fields = ['machine_cleaned', 'lot_num', 'recipe_cleaned__recipe_name__item_code', 'lab_user']
autocomplete_fields = ['machine_used','recipe_cleaned']
fieldsets = [
('Cleaning Info',{'fields':[('machine_used', 'recipe_cleaned', 'lot_num')]}),
(None,{'fields':[('start_time')]}),
(None,{'fields':[('clean_time', 'lab_user')]})
]
inlines = [CleaningUserInline, CleaningEntryInline]
change_list_template = 'admin/quality/cleaninglog/change_list.html'
list_per_page = 25
form = CleaningEntryForm
class Media:
js = (
'admin/js/vendor/jquery/jquery.min.js',
'admin/js/jquery.init.js',
'admin/js/list_filter_collaspe.js',
'admin/js/equipaction_filter.js',
)
css = {'all':(
'admin/css/vendor/select2/select2.css',
)
}
I've tried things like overriding save_formset() but I'm not entirely sure this is a Django issue and wondering if it's not due to namespaces?
If I append the rows manually by using the "Add another" button after the ajax call and I click "Save and Continue" or "Save" and check POST the form is submitting all the inlines but it only saves the 1st record listed and all the records I manually added.
My equipaction_filter.js:
$(document).ready(function () {
////Row container to append to table...
row_container = $(
'<tr class="form-row dynamic-log_entry row1" id="log_entry-0">'+
'<td class="original"><input type="hidden" name="log_entry-0-id" id="id_log_entry-0-id">'+
'<input type="hidden" name="log_entry-0-log_entry" id="id_log_entry-0-log_entry"></td>'+
'<td class="field-cleaning_item"><input type="text" name="log_entry-0-cleaning_item" class="vForeignKeyRawIdAdminField" id="id_log_entry-0-cleaning_item">'+
'</td>'+
'<td class="field-cleaning_action"><input type="text" name="log_entry-0-cleaning_action" disabled="" id="id_log_entry-0-cleaning_action" style="width: 200px;"></td>'+
'<td class="field-checked"><input type="checkbox" name="log_entry-0-checked" id="id_log_entry-0-checked"></td>'+
'<td class="field-na"><input type="checkbox" name="log_entry-0-na" id="id_log_entry-0-na"></td>'+
'<td class="field-notes"><input type="text" name="log_entry-0-notes" maxlength="512" id="id_log_entry-0-notes" class="vTextField"></td>'+
'<td class="delete"></td></tr>'
);
//// This binds an ".on(select)" event function to the select2 box for the machine_used that
//// preforms an AJAX call using the machine_fk reference id to get all the EquipmentActions
//// records for that machine using REST API.
$("select#id_machine_used").on("select2:select", function(event) {
machine_fk = event.params.data.id;
var origin = window.location.origin;
$.ajax({
url:origin+'/pyscales/v1/quality/?machine_fk='+machine_fk,
crossDomain:true,
dataType:'json',
contentType: 'application/json',
//Upon a successful GET request, data is returned in JSON form.
success: function(data) {
console.log(data);
$("#id_log_entry-TOTAL_FORMS").val(data.count);
$(data.results).each(function (i, item) {
// console.log(i, item);
new_row = $(row_container[0].outerHTML.replace(/log_entry-0/,'log_entry-'+i));
new_row[0].children[1].children[0].value = item.id;
new_row[0].children[2].children[0].value = item.machine_fk+' | '+item.action;
var new_table = $('table.cleaning-table');
new_table.find('tbody').prepend(new_row);
});
}
});
});
});
})(django.jQuery);
I'd appreciate some feedback or constructive criticism. I'm still new to jQuery and I'm having a difficult time trying to understand the ins and outs. If I need to post something else please let me know. Thanks in advance.
After reviewing the code again and looking into the POST data, per #dirkgroten request, I noticed that on POST the .replace() regular expression I was using wasn't properly mapping the indexes to the forms:
new_row = $(row_container[0].outerHTML.replace(/log_entry-0/,'log_entry-'+i));
Should have been:
new_row = $(row_container[0].outerHTML.replace(/log_entry-0/gi,'log_entry-'+i));
When the page mapped the data instead of a record for each row within the inline like so:
log_entry-0-cleaning_item:1
log_entry-1-cleaning_item:2
log_entry-2-cleaning_item:3
It was mapping multiple values to the same inline object that jQuery was creating:
log_entry-0-cleaning_item:
[0]:1
[0]:2
[0]:3
I have a page on my Django website, which is displaying a number of tables based on information stored in the database.
The view being used to create the page displaying the tables is defined with:
def current_budget(request, budget_id):
""" View the active provisional/deposit budget """
budget = Budget.objects.select_related('project', 'project__budget_overview').prefetch_related('project__projectroom_set', 'project__budget_versions', 'budget_items').get(id=budget_id)
project = budget.project
# project.projectroom_set.filter(budgetitem__isnull=True, cciitem__isnull=True).delete()
if project.budget_overview.deposit_budget_saved: return HttpResponseRedirect(reverse('costing:combined_budget', args=[project.id]))
#This is now done in the costing_home view
# if not budget:
# Budget.objects.create(project=project, current_marker=1)
if not budget.budget_items.exists() and not project.budget_overview.deposit_budget_saved: init_budget(budget) # Create initial BudgetItem objects as standard
budget_items = budget.budget_items.select_related('budget', 'budget__project', 'project_room', 'project_room__room', 'room')#.order_by('build_type', 'build_type_detail', 'project_room', 'order') # .exclude(build_type=None)
budget_items2 = None #budget.budget_items.filter(build_type=None).order_by('build_type_detail', 'project_room', 'room')
context = {
'project': project,
'budget': budget,
'offset1': -5,
'offset2': -4,
}
try: context['current_budget'] = project.budget_versions.get(current_marker=1) #For option name/date on top of pdfs
except ObjectDoesNotExist: pass
if request.GET.get('version') or project.budget_overview.deposit_budget_saved: #Make the fields all readonly
context['readonly'] = True
context['offset1'] = -7
if request.GET.get('report'): #Schedule of works report uses same data as current budget form
""" Client view of budget. IMPORTANT: Hidden items are not displayed here """
items_grouped = groupby(budget_items.filter(hidden_cost=False), lambda x: x.build_type)
grouped_items = [(x, list(y)) for x, y in items_grouped]
context['grouped_items'] = grouped_items
if request.GET.get('pdf'):
template = get_template('costing/report_schedule_of_works.html')
html = template.render(context)
file = open('test.pdf', "w+b")
pisaStatus = pisa.CreatePDF(html.encode('utf-8'), link_callback=fetch_resources, dest=file,
encoding='utf-8')
file.seek(0)
pdf = file.read()
file.close()
return HttpResponse(pdf, 'application/pdf')
else:
context['webview'] = 1
context['html'] = render_to_string('costing/report_schedule_of_works.html', context)
context['active_tab'] = '3'
return render(request, 'costing/reports_pre_deposit.html', context)
else:
if not context.get('readonly'):
context['skill_day_rate'] = skill_day_rate
context['labour_day_rate'] = labour_day_rate
# Dict with ProjectRoom ids and the total for the room
room_totals = {}
for project_room in project.projectroom_set.all():
room_totals[project_room.id] = sum(item.total_inc_profit for item in budget_items if item.project_room == project_room)
context['room_totals'] = room_totals
item_formset = BudgetItemFormset(queryset=budget_items, form_kwargs={'project': project})
item_form = item_formset.forms[0]
context['field_count'] = len(item_form.visible_fields())
context['ao_field_count'] = len(item_form.visible_fields())
room_choices = project.room_choices
context['formset'] = item_formset
context['widths'] = budget_item_column_widths #First column is add/delete options to allow for forloop count offset
context['options_width'] = options_width #First column is add/delete options to allow for forloop count offset
context['labour_rate'] = labour_day_rate
context['skill_rate'] = skill_day_rate
context['item_code_options'] = ItemCodeForm()
skill_total = int(budget_items.aggregate(Sum('skill_days'))['skill_days__sum'] or 0)
if budget_items2: labour_total = int(budget_items2.aggregate(Sum('labour_days'))['labour_days__sum'])
else: labour_total = 0
return render(request, 'costing/budget_current.html', context)
I now want to add a 'custom' column to these tables, to allow the user to enter their own notes in (i.e. one that will not be displaying data retrieved from the database, but which should add information to each row in the database when it is saved).
How would I do this? I would have thought that I will do it in the view (Python), rather than in the template (HTML), since the view is where the tables are being constructed?
Edit
So, I've added the extra field to the model in models.py for the app:
class Deposit(models.Model):
""" Each Deposit is linked to a Payment, whose is_booking_deposit field = True """
project = models.OneToOneField('projects.Project', null=True, blank=True)
half_paid = models.DateField(null=True, blank=True)
date_received = models.DateField(null=True, blank=True)
amount_exc_vat = models.DecimalField(decimal_places=2, max_digits=12, null=True, blank=True)
invoice_raised = models.DateField(null=True, blank=True)
notes = models.TextField(null=True, blank=True)
# Create a column for 'custom notes' in the tables:
custom_notes = models.TextField(null=True, blank=True) #Add the column to table in tables.py
created = models.DateTimeField(auto_now_add=True)
modified = models.DateTimeField(auto_now=True)
payment = models.OneToOneField(Payment, null=True)
...
The field that I've added to the Deposit model above is the custom_notes one.
I also tried adding the same field to the BudgetItemTable() in tables.py:
class BudgetItemTable(tables.Table):
# Add a column for 'custom notes' to the tables for current budget
custom_notes = tables.Column(accessor='self.custom_notes')
class Meta:
model = BudgetItem
attrs = {"class": "paleblue"}
exclude = ('id')
I've run python manage.py makemigrations & python manage.py migrate myApp, but when I refresh my browser to view this page again, the new column for my table is not displayed- do I need to add it to the view somehow? How would I do this?
Edit
The HTML file for the view that displays the table I want to add a column to has the following structure:
...
{% block page_options_style %}allow-footer{% endblock page_options_style %}
{% block page_options %}
...
{% block report %}
<div id='budget_form' class="col-12" data-pr="{{project.id}}" style="margin-bottom:7em;">
<form class="autosave_form formset num_refresh text-sm" data-view-url="{% url 'costing:save_items' budget.id %}">{% csrf_token %}
{{formset.management_form}}
<div>
<table ...>
<thead class=... >
<tr class=...>
...
<!-- code to get table headings here -->
<th style="width:{{options_width}}">Custom Notes</th>
<!-- I added the 'Custom Notes' heading myself -->
</tr>
</thead>
...
<tbody>
...
<tr id="item_{{forloop.counter}}" class="{% cycle 'odd' 'even' %}">
...
<!-- code add columns and populate rows here -->
<td>
<a class="delete" ... ></a>
<!-- I can see this column with the 'delete' values is the last column in the table on the webpage, so I want to manually add another column after it in the table- I tried doing this by adding the following tags: -->
</td>
<td>
<a class="custom_notes" type="text" value=""></a>
</td>
...
</tr>
</tr>
</tbody>
</table>
...
</tbody>
</table>
</div>
</form>
I tried to add the 'Custom Notes' column to the table with the lines:
<td>
<a class="custom_notes" type="text" value=""></a>
</td>
When I view this page in the browser, I can see the Custom Notes column heading that I've added, displayed above the table, but it is displayed just to the right of where the table ends (I expected the column that I added to the table to be displayed below it).
The table column for 'custom notes' that I have tried adding is not displayed in the table at all...
Why is this? How can I get the new column to be displayed in the table on the webpage? I've added the corresponding field to the model in models.py, so although there won't be any data in this field for any of the items in the database at the moment, when the user enters data in this field in the table, there is somewhere for those values to be stored in the model in the database...
Edit
I managed to add this additional field to the table by appending the following HTML to the table, just inside the last two </tr> tags, inside </tbody></table>:
<td>
<a class="custom_notes" type="text" value=""></a>
<input id="budget_notes" type="text" width="100" value="">
</td>
But for some reason, the text input field that I have added is very narrow- you can't see more than a few characters of text at a time when typing into it. I tried specifying the width of the field using width="100", as shown above, but this doesn't appear to have made any difference. How can I force the cells in this column to have a set width?
I want to add to the cart the actual product I'm in (product_detail.html).
So in the product_unit, is just needed to specify the quantity of the product.
Anyway I can't make the unit_product, automatically add the actual product I'm in.
forms.py
class Product_unitForm(forms.ModelForm):
class Meta:
model = Product_unit
fields = [
'product',
'quantity',
]
widgets = {'product': forms.HiddenInput()}
I hide the product from the template, because it is just the actual product, no need to specify.
views.py
def product_detail(request, id_category=None,id_product=None):
actual_product = Product.objects.get(id = id_product)
#Has an actual customer
#FORM
form_product_unit = Product_unitForm(request.POST or None)
form_product_unit.fields['product'] = actual_product # I try to add the product this way
if form_product_unit.is_valid():
instance_product_unit = form.save(commit=False)
instance_product_unit.product.save()
last_order = Order.objects.last()
is_buying = False
if(last_order.status == "en curso"):
is_buying = True
context = {
"Product" : actual_product,
"Is_buying" : is_buying,
#FORMS
"form_product_unit" : form_product_unit,
}
return render(request, "shopping/product_detail.html", context)
I want to manually from the views, add the product field of product_unit to the actual product it has (actual_product)
template
<img src="{{Product.image.url}}"/>
<h1>{{Product.title}}</h1>
<form method="POST" action="">{% csrf_token %}
{{ form_product_unit.as_p }}
<input type="submit" value="Add" />
</form>
In your views.py file I think you just need to make two changes
def product_detail(request, id_category=None,id_product=None):
actual_product = Product.objects.get(id = id_product)
form_product_unit = Product_unitForm(data=request.POST or None,
initial={'product': actual_product})
And also remove the line form_product_unit.fields['product'] = actual_product. You might need to play around with the initial dictionary a bit to get it to bind the correct value to the field but that's the general idea. The related section in the docs is https://docs.djangoproject.com/en/1.9/ref/forms/api/#dynamic-initial-values
I'm working on something like an online store. I'm making a form in which the customer buys an item, and she can choose how many of these item she would like to buy. But, on every item that she buys she needs to choose what its color would be. So there's a non-constant number of fields: If the customer buys 3 items, she should get 3 <select> boxes for choosing a color, if she buys 7 items, she should get 7 such <select> boxes.
I'll make the HTML form fields appear and disappear using JavaScript. But how do I deal with this on my Django form class? I see that form fields are class attributes, so I don't know how to deal with the fact that some form instance should have 3 color fields and some 7.
Any clue?
Jacob Kaplan-Moss has an extensive writeup on dynamic form fields:
http://jacobian.org/writing/dynamic-form-generation/
Essentially, you add more items to the form's self.fields dictionary during instantiation.
Here's another option: how about a formset?
Since your fields are all the same, that's precisely what formsets are used for.
The django admin uses FormSets + a bit of javascript to add arbitrary length inlines.
class ColorForm(forms.Form):
color = forms.ChoiceField(choices=(('blue', 'Blue'), ('red', 'Red')))
ColorFormSet = formset_factory(ColorForm, extra=0)
# we'll dynamically create the elements, no need for any forms
def myview(request):
if request.method == "POST":
formset = ColorFormSet(request.POST)
for form in formset.forms:
print "You've picked {0}".format(form.cleaned_data['color'])
else:
formset = ColorFormSet()
return render(request, 'template', {'formset': formset}))
JavaScript
<script>
$(function() {
// this is on click event just to demo.
// You would probably run this at page load or quantity change.
$("#generate_forms").click(function() {
// update total form count
quantity = $("[name=quantity]").val();
$("[name=form-TOTAL_FORMS]").val(quantity);
// copy the template and replace prefixes with the correct index
for (i=0;i<quantity;i++) {
// Note: Must use global replace here
html = $("#form_template").clone().html().replace(/__prefix_/g', i);
$("#forms").append(html);
};
})
})
</script>
Template
<form method="post">
{{ formset.management_form }}
<div style="display:none;" id="form_template">
{{ formset.empty_form.as_p }}
</div><!-- stores empty form for javascript -->
<div id="forms"></div><!-- where the generated forms go -->
</form>
<input type="text" name="quantity" value="6" />
<input type="submit" id="generate_forms" value="Generate Forms" />
you can do it like
def __init__(self, n, *args, **kwargs):
super(your_form, self).__init__(*args, **kwargs)
for i in range(0, n):
self.fields["field_name %d" % i] = forms.CharField()
and when you create form instance, you just do
forms = your_form(n)
it's just the basic idea, you can change the code to whatever your want. :D
The way I would do it is the following:
Create an "empty" class that inherits from froms.Form, like this:
class ItemsForm(forms.Form):
pass
Construct a dictionary of forms objects being the actual forms, whose composition would be dependent on the context (e.g. you can import them from an external module). For example:
new_fields = {
'milk' : forms.IntegerField(),
'butter': forms.IntegerField(),
'honey' : forms.IntegerField(),
'eggs' : forms.IntegerField()}
In views, you can use python native "type" function to dynamically generate a Form class with variable number of fields.
DynamicItemsForm = type('DynamicItemsForm', (ItemsForm,), new_fields)
Pass the content to the form and render it in the template:
Form = DynamicItemsForm(content)
context['my_form'] = Form
return render(request, "demo/dynamic.html", context)
The "content" is a dictionary of field values (e.g. even request.POST would do).
You can see my whole example explained here.
Another approach: Rather than breaking the normal field initialization flow, we can override fields with a mixin, return an OrderedDict of dynamic fields in generate_dynamic_fields which will be added whenever its set.
from collections import OrderedDict
class DynamicFormMixin:
_fields: OrderedDict = None
#property
def fields(self):
return self._fields
#fields.setter
def fields(self, value):
self._fields = value
self._fields.update(self.generate_dynamic_fields())
def generate_dynamic_fields(self):
return OrderedDict()
A simple example:
class ExampleForm(DynamicFormMixin, forms.Form):
instance = None
def __init__(self, instance = None, data=None, files=None, auto_id='id_%s', prefix=None, initial=None,
error_class=ErrorList, label_suffix=None, empty_permitted=False, field_order=None,
use_required_attribute=None, renderer=None):
self.instance = instance
super().__init__(data, files, auto_id, prefix, initial, error_class, label_suffix, empty_permitted, field_order,
use_required_attribute, renderer)
def generate_dynamic_fields(self):
dynamic_fields = OrderedDict()
instance = self.instance
dynamic_fields["dynamic_choices"] = forms.ChoiceField(label=_("Number of choices"),
choices=[(str(x), str(x)) for x in range(1, instance.number_of_choices + 1)],
initial=instance.initial_choice)
return dynamic_fields
How do I sort a column of Django elements inside a table?
I notice the Django Admin does this. How?
Any web resources available where I can read up on this?
*By "Django elements" I'm referring to the pluggable template objects that look like this:
{{ x.y }}
Worth having a look at the ChangeList view source code in the django admin:
http://code.djangoproject.com/svn/django/branches/releases/1.2.X/django/contrib/admin/views/main.py
The ordering process is roughly:
you click on a column header in the admin, this reloads the page with the relevant order parameter in the url, available in request.GET
this order parameter is parsed in the ChangeList view:
self.order_field, self.order_type = self.get_ordering()
queryset is generated:
self.query_set = self.get_query_set()
INparticular, the following lines in the function:
if self.order_field:
qs = qs.order_by('%s%s' % ((self.order_type == 'desc' and '-' or ''), self.order_field))
Using the django admin while understanding the source code is one of the best ways to learn django in my opinion!
If you're getting it from a query then use the order_by() method in the view. If it's a list then use the sort() method in the view.
I created this utility class after looking at how the admin did things back in Django 0.96.
It handles creation of order_by criteria based on GET parameters and provides context variables to be used to generating table header sort links which respect the current sort field and direction, reversing the direction when the same header is sorted by again.
ORDER_VAR = 'o'
ORDER_TYPE_VAR = 'ot'
class SortHeaders:
"""
Handles generation of an argument for the Django ORM's
``order_by`` method and generation of table headers which reflect
the currently selected sort, based on defined table headers with
matching sort criteria.
Based in part on the Django Admin application's ``ChangeList``
functionality.
"""
def __init__(self, request, headers, default_order_field=None,
default_order_type='asc', additional_params=None):
"""
request
The request currently being processed - the current sort
order field and type are determined based on GET
parameters.
headers
A list of two-tuples of header text and matching ordering
criteria for use with the Django ORM's ``order_by``
method. A criterion of ``None`` indicates that a header
is not sortable.
default_order_field
The index of the header definition to be used for default
ordering and when an invalid or non-sortable header is
specified in GET parameters. If not specified, the index
of the first sortable header will be used.
default_order_type
The default type of ordering used - must be one of
``'asc`` or ``'desc'``.
additional_params:
Query parameters which should always appear in sort links,
specified as a dictionary mapping parameter names to
values. For example, this might contain the current page
number if you're sorting a paginated list of items.
"""
if default_order_field is None:
for i, (header, query_lookup) in enumerate(headers):
if query_lookup is not None:
default_order_field = i
break
if default_order_field is None:
raise AttributeError('No default_order_field was specified and none of the header definitions given were sortable.')
if default_order_type not in ('asc', 'desc'):
raise AttributeError('If given, default_order_type must be one of \'asc\' or \'desc\'.')
if additional_params is None: additional_params = {}
self.header_defs = headers
self.additional_params = additional_params
self.order_field, self.order_type = default_order_field, default_order_type
# Determine order field and order type for the current request
params = dict(request.GET.items())
if ORDER_VAR in params:
try:
new_order_field = int(params[ORDER_VAR])
if headers[new_order_field][1] is not None:
self.order_field = new_order_field
except (IndexError, ValueError):
pass # Use the default
if ORDER_TYPE_VAR in params and params[ORDER_TYPE_VAR] in ('asc', 'desc'):
self.order_type = params[ORDER_TYPE_VAR]
def headers(self):
"""
Generates dicts containing header and sort link details for
all defined headers.
"""
for i, (header, order_criterion) in enumerate(self.header_defs):
th_classes = []
new_order_type = 'asc'
if i == self.order_field:
th_classes.append('sorted %sending' % self.order_type)
new_order_type = {'asc': 'desc', 'desc': 'asc'}[self.order_type]
yield {
'text': header,
'sortable': order_criterion is not None,
'url': self.get_query_string({ORDER_VAR: i, ORDER_TYPE_VAR: new_order_type}),
'class_attr': (th_classes and ' class="%s"' % ' '.join(th_classes) or ''),
}
def get_query_string(self, params):
"""
Creates a query string from the given dictionary of
parameters, including any additonal parameters which should
always be present.
"""
params.update(self.additional_params)
return '?%s' % '&'.join(['%s=%s' % (param, value) \
for param, value in params.items()])
def get_order_by(self):
"""
Creates an ordering criterion based on the current order
field and order type, for use with the Django ORM's
``order_by`` method.
"""
return '%s%s' % (
self.order_type == 'desc' and '-' or '',
self.header_defs[self.order_field][1],
)
Sample view:
from somewhere import SortHeaders
from django.contrib.auth.models import User
from django.shortcuts import render_to_response
LIST_HEADERS = (
('Username', 'username'),
('First Name', 'first_name'),
('Last Name', 'last_name'),
('Email', None),
)
def user_list(request):
sort_headers = SortHeaders(request, LIST_HEADERS)
users = User.objects.order_by(sort_headers.get_order_by())
return render_to_response('users/user_list.html', {
'users': users,
'headers': list(sort_headers.headers()),
})
Sample template (users/user_list.html):
{% load my_tags %}
<table cellspacing="0">
<thead>
<tr>
{% table_header headers %}
</tr>
</thead>
<tbody>
{% for user in users %}<tr class="{% cycle odd,even %}">
<td>{{ user.username }}</td>
<td>{{ user.first_name }}</td>
<td>{{ user.last_name }}</td>
<td>{{ user.email }}</td>
</tr>
{% endfor %}
</tbody>
</table>
Sample inclusion tag (templatetags/my_tags.py):
from django import template
def table_header(context, headers):
return {
'headers': headers,
}
register = template.Library()
register.inclusion_tag('table_header.html', takes_context=True)(table_header)
Sample inclusion tag template (table_header.html):
{% for header in headers %}<th{{ header.class_attr }}>
{% if header.sortable %}<a href="{{ header.url }}">{% endif %}
{{ header.text }}
{% if header.sortable %}</a>{% endif %}
</th>{% endfor %}