07 July 2011

Auto-expanding django formset with jQuery

As it took me quite a while to get it how I like it, here's the relevant bits for making a django formset (custom markup in a table), that automatically adds rows (formset forms) client-side / in the browser keeping up as you fill in the form.

Do with the code as you wish, no licence needed.

In the view (.html file server side) I have:

@login_required
def invoiceEdit(request, invoice_id):
...
InlineInvoiceItemsFormSet = inlineformset_factory(Invoice, InvoiceItem, form=DeleteIfEmptyModelForm, formset=DeleteIfEmptyInlineFormSet, can_delete=True, extra=10)
...
itemFormSet = InlineInvoiceItemsFormSet()
...
return render_to_response('foo/edit.html', {'invoiceForm': invoiceForm, 'itemFormSet': itemFormSet, 'invoice': invoice}, context_instance=RequestContext(request))


In the template I have:

<script type="text/javascript">
$(function() {
setupInvoiceFormset();
});

var initialRows;

function setupInvoiceFormset() {
initialRows = parseInt($('#id_invoiceitem_set-INITIAL_FORMS').val());
// remove all but last two empty rows
resizeInvoiceFormset();
// add handlers to all inputs to automate row adding
$('.invoiceItemRow :input').blur(resizeInvoiceFormset);
}

const targetExtra = 2; // number of extra rows desired

function resizeInvoiceFormset() {
// count the blank rows at the end
var rows = $('.invoiceItemRow').filter(':not(#templateItemRow)');
var totalRows = rows.length
var blankRows = countBlankRows(rows);
var targetRowCount = totalRows - blankRows + targetExtra;
targetRowCount = Math.max(targetRowCount,initialRows); // don't trim off real rows otherwise delete breaks
if (totalRows > targetRowCount) {
// if there too many blank rows remove the extra rows
rows.slice(targetRowCount).remove(); // negative to strip from ends
} else if (totalRows < targetRowCount) {
// add new blank rows to bring the total up to the desired number
for (var newRowIndex = totalRows; newRowIndex < targetRowCount; newRowIndex++) {
addRow(newRowIndex);
}
} else {
return;
}
// update the hidden form with the new form count
$('#id_invoiceitem_set-TOTAL_FORMS').val(targetRowCount);
}

function countBlankRows(rows) {
// count the empty rows from the bottom up, stopping at the first non-blank row
var blankRows = 0;
for (var i = rows.length -1; i>=0; i--) {
if (isEmptyRow(rows[i])) {
blankRows++;
} else {
break;
}
}
return blankRows;
}

function isEmptyRow(row) {
// loop through all the inputs in the row, return true if they are all blank
// whitespace is ignored
var inputs = $(row).find(':input').filter(':not(:hidden)');
for (var j = 0; j < inputs.length; j++) {
if ($.trim(inputs[j].value).length) {
return false;
}
}
return true;
}

function addRow(newRowIndex) {
var newRow = $('#templateItemRow').clone(true);
newRow.addClass('invoiceItemRow');
newRow.removeAttr('id'); //prevent duplicated template row id
newRow.show();
// replace placeholder with row index
newRow.find(':input').each(function() {
$(this).attr("name", $(this).attr("name").replace('__prefix__', newRowIndex));
$(this).attr("id", $(this).attr("id").replace('__prefix__', newRowIndex));
});
$('.invoiceItemRow:last').after(newRow);
}
</script>
...
{{ itemFormSet.management_form }}
<tr id="templateItemRow" class="invoiceItemRow" style="display: none;">
<td><strong>Item:</strong></td>
<td>
{{ itemFormSet.empty_form.id }}
{{ itemFormSet.empty_form.description }}
{{ itemFormSet.empty_form.description.errors }}</td>
<td class="price">£{{ itemFormSet.empty_form.price }} {{ itemFormSet.empty_form.price.errors }}</td></tr>
{% for item in itemFormSet.forms %}
<tr class="invoiceItemRow">
<td><strong>Item:</strong></td>
<td>
{{ item.id }}
{{ item.description }}
{{ item.description.errors }}</td>
<td class="price">£{{ item.price }} {{ item.price.errors }}</td></tr>
{% endfor %}
...


The result is a form that intuitively shrinks/grows as the content is added/removed.

The javascript is of course actually in a separate .js file.

References:


Footnote. You may have noticed the delete-if-empty customisation which I like for usability. References for this at

23 May 2011

Data driven test in NUnit with csv source data

I wanted to test a date parser across a large range of values so wanted a simple test harness to test all the values.

The test framework options around c# / .net seem to be:
  • MSTest - can do csv via jet, but can't do inline test data which is something I also want.
  • NUnit - can do inline data driven test data (with the TestCase(data...) attribute), and has support for extending this via the TestCaseSource attribute.
  • xUnit - confusing (aka flexible), doesn't seem to get me to my end result any faster after a bit of searching around.
I've used NUnit and combined TestCaseSource with a simple wrapper class around the csv parsing library

To get this to work:
  • Save your csv file in your test project
  • add the file to your project (in visual studio 2008 in this case)
  • right-click on the csv file in solution explorer, click properties, change "Copy to Output Directory" to "Copy Always"
  • download the binaries (dlls) for csv reader from code project, add a reference to this in your test project
  • add a private method to your test class for reading the csv file and returning an enumarable (see code below)
  • add the TestCaseSource attribute to your test method(s) that you want to use the csv data, referencing your new IEnumerable method (see code below)


using System.Collections.Generic;
using System.IO;
using LumenWorks.Framework.IO.Csv;
using NUnit.Framework;

namespace mytests
{
    class MegaTests
    {
        [Test, TestCaseSource("GetTestData")]
        public void MyExample_Test(int data1, int data2, int expectedOutput)
        {
            var methodOutput = MethodUnderTest(data2, data1);
            Assert.AreEqual(expectedOutput, methodOutput, string.Format("Method failed for data1: {0}, data2: {1}", data1, data2));
        }

        private int MethodUnderTest(int data2, int data1)
        {
            return 42; //todo: real implementation
        }

        private IEnumerable<int[]> GetTestData()
        {
            using (var csv = new CsvReader(new StreamReader("test-data.csv"), true))
            {
                while (csv.ReadNextRecord())
                {
                    int data1 = int.Parse(csv[0]);
                    int data2 = int.Parse(csv[1]);
                    int expectedOutput = int.Parse(csv[2]);
                    yield return new[] { data1, data2, expectedOutput };
                }
            }
        }
    }
}


references:

18 May 2011

Reliable javascript checkbox events


Some sites have checkboxes which show/hide another element when you click them. This a handy feature, but not all sites take into account the fact that firefox remembers the contents of a form when you reload the page (this is a good thing).

So here's how you avoid that with jQuery:

<script type="text/javascript">
$(function() {
// initialise show/hide to match the checkbox value
$('.targetelements').toggle($('#mycheckbox').attr('checked'));
// attach click handler for show/hide to checkbox
$('#mycheckbox').click(function(){ $('.targetelements').toggle(this.checked);})
});
</script>

Simples!

You could use the same principle without jQuery if you need to. Simply read the value of the checkbox with javascript the old fashioned way before deciding whether to hide when you initialise you page.

15 December 2010

Using Pidgin for IRC

pidgin is quite a good irc client.

Once you have downloaded and installed pidgin:

  • Accounts > Manage Accounts
  • Add...
  • Protocol: IRC
  • Username: your preferred nickname (please use your real name)
  • Server: your irc server, eg irc.freenode.org
  • Password - leave blank
  • Add
  • Close (the "Accounts" window)

Back in the main pidgin window:

  • Buddies > Join A Chat...
  • Account: the one you just created
  • Channel: #favouriteroom, eg #pidgin
  • Password - leave blank
  • Join

In the new chat window for the chatroom:

  • Conversation > Add...
  • Tick "Autojoin when account connects."
  • Tick "Remain in chat after window is closed."
  • Leave everything else as defaults
  • Add

Now when pidgin launches you will have "#favouriteroom" in your buddy list, and you can double click to open the chatroom.

You may also want to make pidgin start when windows starts;

From the main window:

  • Tools > Plugins
  • Tick "Windows Pidgin Options"
  • Configure Plugin
  • Tick "Start Pidgin on Windows startup"
  • Close
  • Close (plugin window)

I also recommend enabling the Markerline plugin to help see what is new in the channel.

03 September 2010

configuring kdiff3 as a mergetool in msysgit

How to configure kdiff3 as a mergetool in msysgit. (I think if you install kdiff3 *before* msysgit it is picked up automatically, if not, do the following after installing both).

In git bash:

git config --global merge.tool kdiff3
git config --global mergetool.kdiff3.path "c:\Program Files\KDiff3\kdiff3.exe"


double check:
cat ~/.gitconfig
[merge]
tool = kdiff3
[mergetool "kdiff3"]
path = c:\\Program Files\\KDiff3\\kdiff3.exe

refs:


Under cygwin, the setup would be:

git config --global merge.tool kdiff3
git config --global mergetool.kdiff3.path /cygdrive/c/Program\ Files\ \(x86\)/KDiff3/kdiff3.exe

Giving the config file contents:

[merge]
tool = kdiff3
[mergetool "kdiff3"]
path = /cygdrive/c/Program Files (x86)/KDiff3/kdiff3.exe

Which by the way you can view with

git config -e --global

04 February 2010

openlayers svn into git

Initial clone:
git svn clone -T trunk/openlayers/ -t tags/openlayers/ -b branches/openlayers/ http://svn.openlayers.org/ openlayers.git

"http://svn.openlayers.org/tags/openlayers/docs-2.8/" is in the wrong place and gets pulled in by the git clone.

I should have used --no-follow-parent to avoid the docs-2.8 tag pulling in docs history but not going to re-clone now. If you are repeating this, try this instead:
git svn clone --no-follow-parent -T trunk/openlayers/ -t tags/openlayers/ -b branches/openlayers/ http://svn.openlayers.org/ openlayers.git

Find the errant docs branches & eliminate:
cd openlayers.git
for x in `git for-each-ref --format="%(refname)" 'refs/remotes/tags/docs*'`; do git update-ref -d $x; done

# http://dound.com/2009/04/git-forever-remove-files-or-folders-from-history/
# expunge old objects (I think this works)
git reflog expire --all
git gc --aggressive --prune

Then run: http://www.shatow.net/fix-svn-refs.sh to create real git tags.

If you just want the result you can download a copy complete with svn metadata from http://www.timwise.co.uk/openlayers-dev/openlayers.git.tgz

You will then be able to run
git svn fetch

to get updates from the openlayers svn server.

There is a published copy at http://github.com/timabell/openlayers, though it doesn't have the svn metadata.



I also tackled the docs folder:

The docs directory has no matching branch or tag directories, so the following is sufficient:
git svn clone -T trunk/doc http://svn.openlayers.org/ openlayers-doc.git
git gc --aggressive --prune

You can download this from http://www.timwise.co.uk/openlayers-dev/openlayers-doc.git.tgz



Anything else I come up with will end up at http://www.timwise.co.uk/openlayers-dev/

18 December 2009

backing up Vista

So here's a tale of annoying things.

I generally try and avoid running anything proprietary at home, and especially anything from Microsoft. But for reasons beyond my control a copy of Vista has embedded itself in our household.

For my linux backups I've settled on dar (Disk ARchiver) + dargui as it applies the keep it simple rule (certainly in comparison with a lot of other tools I tried, such as the ubuntu default "home user backup" tool, which has a simple ui but offers limited control and I'm not sure how easily I could recover files from it in a disaster). Dar is like tar (Tape ARchiver) but more designed with backup to disk in mind, which is what I was after in order to backup to usb hdd, and most importantly does incremental/differential backups the way I want.

So then I came to back up this Vista home directory (sorry, Users directory / profiles). I have disliked the old fashioned microsoft backup files (.bkf) from Windows XP and before ever since I tried to get files out of one from a Linux box. Turns out it's not exactly the best supported format. I didn't have much luck with mtftar. It seems Microsoft have produced a replacement (plus ça change) to the old windows backup that is evidently designed to be simple, which has very strange ideas about how you might want to back up your pc. It seems keener to back up the sample image files from the office install than than the user's photos. I very quickly fell out with this tool and moved on.

Some people might recommend windows home server, but I am not about to pay for more shoddy Microsoft software in order to solve problems created by other shoddy Microsoft software. Vote with your wallet, as they say.

So next on the list, remote backup from a linux box. backup pc looked great, and has many nifty features, however having got it all set up I got permissions errors in the My Documents etc folders, which are the important ones. I tried different user permissions for the backup user, and different group memberships, though stopped short of resetting all user directory permissions so as to not break anything but couldn't get past these errors. More details on that attempt in my backuppc and windows vista blog entry. There is a hint in the Robocopy wikipedia that there is some special mode needed to be able to get past these permissions issues.
"The so-called Backup mode is an administrative privilege that allows Robocopy to override permissions settings (specifically, NTFS ACLs) for the purpose of making backups."
But I didn't get any further than that.

So finally I come to the conclusion that Vista just doesn't want you to do backups without paying microsoft more money, and that they have forgotten or never knew the KISS mantra that makes *nix such a pleasure to work with. (Rather opting for their usual "making simple things easy and difficult things impossible".)

It was Linux Format 127 Ubuntu 9.10 Cover Disc that came to the rescue. Popped the disc in,rebooted, connected my usb hdd (formatted with ext3 of course), ran apt-get install dar, opened the disk icon on the desktop representing the evil vista installation partition on the local disk (to get it mounted), opened the usb hdd disk icon (also to get it mounted), then ran dar -c /media/usbhdd/backups/vistargh -z from the directory /media/vista/Users/. This ran fine and I was able to read the file from a better operating system with no issues.