Permalink
Browse files

[#212] Add Account Autocompletes to Formsets

This modifies formsets to use Selectize.js widgets instead of the
default HTML Selects. To prevent excessive HTML generation, the options
for each Account field are stored in a single javascript variable.
Accounts are now shown if their description contains what the user has
typed(previously only the name was used for filtering). When the user
types in some text, an AJAX request is sent in case new Accounts have
been created.

* Add the djangoajax dependency.
* Add the all_accounts context processor & a global accounts_array
  javascript variable containing the options for the account selects.
* Add the accounts_query AJAX view to create an array of JSON objects
  for the account select options.
  • Loading branch information...
prikhi committed Apr 2, 2015
1 parent f0bb3e3 commit 6e76a3a51cfa7144af136ba02e4eb230b86af16d
@@ -44,6 +44,7 @@ def project_root(path):
'mptt',
'parsley',
'south',
+ 'django_ajax',
'core',
'accounts',
@@ -108,6 +109,7 @@ def project_root(path):
"django.core.context_processors.request",
"django.contrib.messages.context_processors.messages",
"constance.context_processors.config",
+ "accounts.context_processors.all_accounts",
)
TEMPLATE_DIRS = (project_root('templates'),)
@@ -0,0 +1,17 @@
+"""Context processors related to Accounts."""
+
+import json
+from accounts.models import (Account)
+
+
+def all_accounts(request):
+ """Inject the `accounts_json` variable into every context.
+
+ This is used to pre-populate every AJAX Account Select widget.
+
+ """
+ accounts = Account.objects.order_by('name')
+ values = [{'text': account.name,
+ 'description': account.description,
+ 'value': account.id} for account in accounts]
+ return {'accounts_json': json.dumps(values)}
@@ -11,11 +11,11 @@
class QuickAccountForm(forms.Form):
account = forms.ModelChoiceField(
- queryset=Account.objects.active().order_by('name'),
- widget=forms.Select(attrs={'onchange': 'this.form.submit();',
- 'class': 'form-control autocomplete-select',
- 'placeholder': 'Jump to an Account'}),
- label='', empty_label=''
+ queryset=Account.objects.none(), label='', empty_label='',
+ widget=forms.Select(
+ attrs={'onchange': 'this.form.submit();',
+ 'class': 'form-control account-autocomplete',
+ 'placeholder': 'Jump to an Account'}),
)
@@ -20,4 +20,6 @@
url(r'^bank-journal/(?P<account_slug>[-\w]+)/$', 'bank_journal',
name='bank_journal'),
+
+ url(r'^ajax/accounts/$', 'accounts_query', name='accounts_query'),
)
@@ -1,6 +1,7 @@
import datetime
from dateutil import relativedelta
+from django_ajax.decorators import ajax
from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.core.urlresolvers import reverse
@@ -329,3 +330,25 @@ def reconcile_account(request, account_slug,
'statement_balance': remove_trailing_zeroes(reconciled_balance)
})
return render(request, template_name, locals())
+
+
+@ajax
+def accounts_query(request):
+ """AJAX endpoint for querying Account Names & Descriptions.
+
+ Returns an array of JSON objects that contain the GET parameter ``q`` in
+ their ``name`` or ``description``. Each object has a ``text``, ``value`` &
+ ``description`` property. Defaults to all :class:`~accounts.models.Account`
+ if ``q`` is not present in the ``GET`` parameters.
+
+ """
+ if 'q' in request.GET:
+ q = request.GET['q']
+ accounts = Account.objects.filter(
+ Q(name__icontains=q) | Q(description__icontains=q))
+ else:
+ accounts = Account.objects.all()
+ return [{'text': account.name,
+ 'description': account.description,
+ 'value': account.id
+ } for account in accounts]
@@ -35,6 +35,30 @@ body { padding-top: 30px; }
#id_quick_account .selectize-control { width: 178px; }
#id_quick_bank .selectize-control { width: 197px; }
+/* Properly align Selectize inputs in tables */
+#transaction-table div.selectize-input {
+ vertical-align: middle;
+}
+/* Prevent empty Selectize account widgets from being tiny */
+.account, .source, .destination {
+ min-width: 200px;
+}
+/* Split the Selectize text & descriptions */
+.account-autocomplete.selectize-control .option .text {
+ display: block;
+}
+/* De-emphasize the description in Selectize Account dropdowns */
+.account-autocomplete.selectize-control .option .description {
+ font-size: 10px;
+ display: block;
+ font-style: italic;
+}
+/* Use the Primary color to highlight Selectize dropdown selections */
+.selectize-dropdown .active {
+ color: #ffffff;
+ background-color: #428bca;
+}
+
/* Print Styles */
@media print {
@@ -85,7 +85,8 @@ class Meta:
fields = ('account', 'detail', 'debit', 'credit', 'event',)
widgets = {
'account': forms.Select(
- attrs={'class': 'account form-control enter-mod'}),
+ attrs={'class': 'account account-autocomplete '
+ 'form-control enter-mod'}),
'detail': forms.TextInput(
attrs={'class': 'form-control enter-mod'}),
'event': forms.Select(
@@ -94,7 +95,7 @@ class Meta:
def __init__(self, *args, **kwargs):
super(TransactionForm, self).__init__(*args, **kwargs)
- _allow_only_active_accounts(self, 'account')
+ _set_minimal_queryset_for_account(self, 'account')
self._assign_balance_delta_to_debit_or_credit_field()
def clean(self):
@@ -139,10 +140,24 @@ def _assign_balance_delta_to_debit_or_credit_field(self):
self.initial['credit'] = remove_trailing_zeroes(balance_delta)
-def _allow_only_active_accounts(form_instance, field):
- """Modify the form's field to only display active Accounts."""
- active_accounts = Account.objects.active().order_by('name')
- form_instance.fields[field].queryset = active_accounts
+def _set_minimal_queryset_for_account(form, field_name):
+ """Show Only a Pre-Existing Selection for a ModelChoiceField.
+
+ This replaces the `queryset` of the `field_name` on the `form` with a
+ queryset containing only it's instance's Account or an empty queryset -
+ depending on if `form` has an instance or not.
+
+ This is used to limit the size of HTML when the options for `field_name`
+ are loaded by AJAX.
+
+ """
+ if form.is_bound:
+ return
+ if hasattr(form, 'instance') and form.instance.account_id is not None:
+ form.fields[field_name].queryset = Account.objects.filter(
+ id=form.instance.account_id)
+ else:
+ form.fields[field_name].queryset = Account.objects.none()
class BaseTransactionFormSet(RequiredBaseInlineFormSet):
@@ -204,12 +219,14 @@ class TransferForm(BaseEntryForm, forms.Form):
"""
source = forms.ModelChoiceField(
queryset=Account.objects.active().order_by('name'),
- widget=forms.Select(attrs={'class': 'source form-control enter-mod'})
+ widget=forms.Select(attrs={'class': 'source account-autocomplete '
+ 'form-control enter-mod'})
)
destination = forms.ModelChoiceField(
queryset=Account.objects.active().order_by('name'),
widget=forms.Select(
- attrs={'class': 'destination form-control enter-mod'})
+ attrs={'class': 'destination account-autocomplete '
+ 'form-control enter-mod'})
)
# TODO: This is repeated in BaseBankForm & AccountReconcileForm and ugly
amount = forms.DecimalField(
@@ -223,6 +240,11 @@ class TransferForm(BaseEntryForm, forms.Form):
widget=forms.TextInput(attrs={'class': 'form-control enter-mod'})
)
+ def __init__(self, *args, **kwargs):
+ super(TransferForm, self).__init__(*args, **kwargs)
+ _set_minimal_queryset_for_account(self, 'source')
+ _set_minimal_queryset_for_account(self, 'destination')
+
def clean(self):
"""
Ensure that the source and destination
@@ -412,7 +434,8 @@ class Meta:
fields = ('account', 'detail', 'amount', 'event',)
widgets = {
'account': forms.Select(
- attrs={'class': 'account form-control enter-mod'}),
+ attrs={'class': 'account account-autocomplete '
+ 'form-control enter-mod'}),
'detail': forms.TextInput(
attrs={'class': 'form-control enter-mod'}),
'event': forms.Select(
@@ -421,7 +444,7 @@ class Meta:
def __init__(self, *args, **kwargs):
super(BankTransactionForm, self).__init__(*args, **kwargs)
- _allow_only_active_accounts(self, 'account')
+ _set_minimal_queryset_for_account(self, 'account')
self._assign_balance_delta_to_amount_field()
def clean(self):
@@ -290,9 +290,9 @@ <h4 class="modal-title" id="voidConfirmModalLabel">Voiding Entry Confirmation</h
$(this).keypress(function(e) {
if (e.which == 13) {
{% if journal_type == "Transfer" %}
- $(that).closest('tr').next().find('.source').focus();
+ $(that).closest('tr').next().find('.source input').focus();
{% else %}
- $(that).closest('tr').next().find('.account').focus();
+ $(that).closest('tr').next().find('.account input').focus();
{% endif %}
e.preventDefault();
}
@@ -332,6 +332,18 @@ <h4 class="modal-title" id="voidConfirmModalLabel">Voiding Entry Confirmation</h
});
addActions();
assignEnterKey();
+ /* Select the next input when pressing tab on Selectize
+ * autocompletes */
+ $('#transaction-table .selectize-control input').each(function() {
+ var that = this;
+ $(this).keydown(function(e) {
+ if (e.which == 9 && !e.shiftKey) {
+ var inputs = $(':input').filter(':visible').filter(':enabled');
+ inputs[inputs.index(that) + 1].focus();
+ e.preventDefault();
+ }
+ });
+ });
});
{% if journal_type == 'GJ' %}
@@ -28,8 +28,38 @@
<script type="text/javascript" src="{{ STATIC_URL }}js/bootstrap.min.js"></script>
<script type="text/javascript" src="{{ STATIC_URL }}js/selectize.min.js"></script>
<script type="text/javascript">
+ // A global variable containing accounts for ajax selects
+ accounts_array = {{ accounts_json|safe }};
$(document).ready(function() {
- // Turn Select widgets into Autocompletes
+ // Turn Select Widgets for Accounts into AJAX Autocompletes
+ $('.account-autocomplete').each(function() {
+ $(this).selectize({
+ options: accounts_array,
+ selectOnTab: true,
+ allowEmptyOption: true,
+ sortField: 'text',
+ searchField: ['text', 'description'],
+ render: {
+ option: function(data, escape) {
+ return '<div class="option">' +
+ '<span class="text">' + escape(data.text) + '</span>' +
+ '<span class="description">' + escape(data.description) + '</span>' +
+ '</div>';},
+ },
+ load: function(query, callback) {
+ if (!query.length) return callback();
+ $.ajax({
+ url: '{% url accounts_query %}',
+ type: 'GET',
+ data: {q: query},
+ error: function() { callback(); },
+ success: function(res) {
+ callback(res.content);
+ }
+ });
+ },
+ });
+ });
$('.autocomplete-select').selectize();
// Allow the user to click anywhere in a row to visit the link
$('tr.clickable').click(function() {
View
@@ -8,4 +8,5 @@ django-localflavor==1.0
django-mptt==0.6.0
django-parsley==0.3
django-picklefield==0.3.0
+djangoajax>=2.2
South==0.8.4

0 comments on commit 6e76a3a

Please sign in to comment.