<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>Technology Against You</title>
	<atom:link href="http://davidmburke.com/feed/" rel="self" type="application/rss+xml" />
	<link>http://davidmburke.com</link>
	<description>David Burke&#039;s blog</description>
	<lastBuildDate>Fri, 18 May 2012 14:33:51 +0000</lastBuildDate>
	<language>en</language>
	<sy:updatePeriod>hourly</sy:updatePeriod>
	<sy:updateFrequency>1</sy:updateFrequency>
	<generator>http://wordpress.org/?v=3.3.2</generator>
		<item>
		<title>Setting &#8220;Desktop&#8221; to be ~/Documents/Desktop</title>
		<link>http://davidmburke.com/2012/05/10/settings-desktop-to-be-documentsdesktop/</link>
		<comments>http://davidmburke.com/2012/05/10/settings-desktop-to-be-documentsdesktop/#comments</comments>
		<pubDate>Thu, 10 May 2012 21:01:03 +0000</pubDate>
		<dc:creator>admin</dc:creator>
				<category><![CDATA[Uncategorized]]></category>

		<guid isPermaLink="false">http://davidmburke.com/?p=352</guid>
		<description><![CDATA[xdg is a unix thing that tells you where &#8220;important&#8221; folders are. Such as Documents, Music, Desktop, etc. A simple (and documented example in the Ubuntu conf file) would be to have subfolders. To emulate windows I could say that &#8230; <a href="http://davidmburke.com/2012/05/10/settings-desktop-to-be-documentsdesktop/">Continue reading <span class="meta-nav">&#8594;</span></a>]]></description>
			<content:encoded><![CDATA[<div>xdg is a unix thing that tells you where &#8220;important&#8221; folders are. Such as Documents, Music, Desktop, etc.</div>
<div></div>
<div>A simple (and documented example in the Ubuntu conf file) would be to have subfolders. To emulate windows I could say that Desktop=Documents/Desktop in /etc/xdg/user-dirs.defaults</div>
<div></div>
<div>However if Documents or Desktop is a mount point bad things happen. It just defaults Desktop=Home. Actually it does work randomly&#8230;a race condition between pam_mount and xdg I suppose. Not that xdg creates folders if they don&#8217;t exists&#8230;at least it usually does. So the issue isn&#8217;t that the folder doesn&#8217;t exists. Just look in /etc/skel there is no Desktop folder.</div>
<div></div>
<div>There is a command xdg-user-dirs-update that can reset these if you pass &#8211;force. It works! Run it, go to nautilus, click desktop and it shows you Documents/Desktop in the mount point! However it does not refresh nautilus desktop so the actual desktop still points to Home. One could fix it manually in this terrible hack.</div>
<div></div>
<div>Edit ~/.profile or /etc/skel/.profile for new users too, add to it</div>
<pre>/opt/xdgfix.sh&amp;</pre>
<div>Now create a file /opt/xdgfix.sh and make it</div>
<pre>sleep 6
xdg-user-dirs-update --force
killall nautilus
nautilus -n&amp;</pre>
<p>That&#8217;s sleep 6 seconds, update our xdg user dirs, stop nautilus, start nautilus.</p>
<p>So what good is xdg if it doesn&#8217;t work reliably? It seems to have totally erratic behavior in each version of Ubuntu. Before 12.04 it just wouldn&#8217;t ever work with mount points. Now we have sometimes kinda works&#8230;.some progress <img src='http://davidmburke.com/wp-includes/images/smilies/icon_sad.gif' alt=':(' class='wp-smiley' /> </p>
<p>Of course this is just one problem out of many that stem for Linux having nothing even remotely close to Windows Folder Redirection which if you haven&#8217;t guessed is what I&#8217;m trying to painstakingly emulate.</p>
]]></content:encoded>
			<wfw:commentRss>http://davidmburke.com/2012/05/10/settings-desktop-to-be-documentsdesktop/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Ubuntu 12.04 deployment with Active Directory</title>
		<link>http://davidmburke.com/2012/04/26/ubuntu-12-04-deployment-with-active-directory/</link>
		<comments>http://davidmburke.com/2012/04/26/ubuntu-12-04-deployment-with-active-directory/#comments</comments>
		<pubDate>Thu, 26 Apr 2012 22:29:13 +0000</pubDate>
		<dc:creator>admin</dc:creator>
				<category><![CDATA[Uncategorized]]></category>

		<guid isPermaLink="false">http://davidmburke.com/?p=334</guid>
		<description><![CDATA[** Updated &#8211; Changed for centrifydc &#8211; likewise open doesn&#8217;t work with pam_mount and /etc/skel. This is a follow up to my past post. I want to deploy 12.04 for what I consider a typical enterprise environment. That means centralized authentication, file shares, &#8230; <a href="http://davidmburke.com/2012/04/26/ubuntu-12-04-deployment-with-active-directory/">Continue reading <span class="meta-nav">&#8594;</span></a>]]></description>
			<content:encoded><![CDATA[<p>** Updated &#8211; Changed for centrifydc &#8211; likewise open doesn&#8217;t work with pam_mount and /etc/skel.</p>
<p>This is a follow up to my <a title="Linux and Active Directory round 2" href="http://davidmburke.com/2010/09/16/linux-and-active-directory-round-2/">past post</a>. I want to deploy 12.04 for what I consider a typical enterprise environment. That means centralized authentication, file shares, roaming profiles, etc. Your first step should be to acknowledge this is a very hard project to do. If you need to deploy some computers this week, go get your Windows install CD right now. Linux takes about 10 times as long to set up (though easy to clone).</p>
<p><a href="http://davidmburke.com/files/2012/04/Screenshot-from-2012-04-27-12_46_17.png"><img class="alignnone size-large wp-image-341" title="Screenshot from 2012-04-27 12_46_17" src="http://davidmburke.com/files/2012/04/Screenshot-from-2012-04-27-12_46_17-1024x575.png" alt="" width="584" height="327" /></a></p>
<h1>Authentication to Active Directory</h1>
<p>Options include Centrify, likewise open, winbind, and pam_ldap. I went with centrify because it works with pam_mount and /etc/skel  and this is critical for me. The big disadvantage of Centrify is that it&#8217;s very slow. It adds about 30 seconds to boot time in my deployment. However I will include alternative instructions for likewise which I used before 12.04 when it worked better for me.</p>
<p>Either way they both suffer from a fatal flaw that would probably keep all but the most dedicated linux sysadmins far far away from Linux. They don&#8217;t work on wifi! I&#8217;ve talked about this issue before, basically lightdm starts up before networking is up. The user types in a name, password, gets an error, calls tech support and has the impression that this &#8220;Ubuntu&#8221; is quite the useless operating system.</p>
<p>First with either option you need to allow users to type in a login in lightdm. Edit /etc/lightdm/lightdm.conf and make it so users can type in a username by adding</p>
<pre>greeter-hide-users=true</pre>
<p>Also make sure your wireless network is available to all users. (Click the network icon, Edit Connections&#8230;). Next follow instructions for either Likewise or Centrify.</p>
<h2>Likewise Open</h2>
<p>It&#8217;s terribly easy to install it. It even has a GUI. So I won&#8217;t include instructions for this. However if you need it working on boot as I described above do this. Place this script somewhere like /opt and make it executable. It just checks to see if any networking is up or time out at 20 seconds.</p>
<pre>#!/bin/bash
i=0
while [ $i -lt 20 ]; do
 sleep 1
 is_up=$(ping -q -w 1 -c 1 `ip r | grep default | cut -d ' ' -f 3` &gt; /dev/null &amp;&amp; echo 1 || echo 0)
 let i=$i+1
 if [ $is_up -eq 1 ]; then
 let i=999
 fi
done</pre>
<p>Now edit /etc/init/lightdm.conf and add under the emits section. This script just runs ping test before starting lightdm then waits 5 extra seconds to give things time to settle.</p>
<pre>pre-start script
 /opt/pingtest.sh
 sleep 5
end script</pre>
<h2>Centrify</h2>
<p>It&#8217;s also easy to install. You can get it from the Ubuntu partner repo&#8217;s. If you have it, you need to check this off in Ubuntu Software Center, Edit, Software Sources. Then update then install centrifydc. Join your domain like</p>
<pre>adjoin -w mydomain.org</pre>
<p><a href="http://community.centrify.com/t5/DirectControl-Express-for-UNIX/Centrify-DS-improper-PAM-registration-bug/td-p/1884">Centrify doesn&#8217;t play nice with pam-auth-update</a> so create a conf file for it called /usr/share/pam-configs/centrifydc</p>
<pre>Name: Centrify DC
Default: yes
Priority: 257
Auth-Type: Primary
Auth:
        [success=end default=ignore]                            pam_centrifydc.so try_first_pass
Account-Type: Primary
Account:
        [success=end new_authtok_reqd=done default=ignore]      pam_centrifydc.so
Session-Type: Additional
Session:
        required                                                pam_centrifydc.so homedir
Password-Type: Primary
Password:
        [success=end new_authtok_reqd=done ignore=ignore default=die]   pam_centrifydc.so try_first_pass
Password-Initial:
        [success=end new_authtok_reqd=done ignore=ignore default=die]   pam_centrifydc.so</pre>
<p>Now run pam-update-auth and centrify will play nice with other modules such as pam-mount</p>
<p>Next we need to resolve the can&#8217;t log in on first boot problem. In likewise we just made a simple is networking up yet test. This doesn&#8217;t work with Centrify. From what I can tell this is what happens.</p>
<ol>
<li>Networking starts</li>
<li>pingtest script launches lightdm only after networking is up. Right now we could ping the domain controller</li>
<li>Centrify attempts to contact the domain controller but fails for some unknown reason. It then sites and does nothing for a bit. Then tries again and succeeds. This takes about 30 seconds.</li>
</ol>
<div>Instead we need to modify /etc/init.d/centrify and add some upstart emits. These will tell us when centrify is ready to use. Look in the start section and add this emit as shown in bold</div>
<div>
<div dir="ltr">
<pre>start)
 adclient_check
 echo -n "Starting $NAME: "
 start-stop-daemon --start --quiet --exec $DAEMON --pidfile $PIDFILE \
 -- $OPTIONS
 RETVAL=$?
 if [ $RETVAL -eq 0 ]; then
 echo "OK"
 wait_adclient
 # upstart won't start gdm until we say we're connected
 <strong>initctl emit centrify-connected</strong> # added
 else
 echo "FAIL</pre>
</div>
</div>
<div>This emit signal doesn&#8217;t do anything in itself. It&#8217;s just an upstart signal we can look for else where, such as /etc/init/lightdm.conf where we will add it at a dependency to start.</div>
<div>
<pre>start on (filesystem
 and started dbus
 and (graphics-device-added fb0 PRIMARY_DEVICE_FOR_DISPLAY=1
 or drm-device-added card0 PRIMARY_DEVICE_FOR_DISPLAY=1
 or stopped udevtrigger)
 <strong>and centrify-connected</strong>)
stop on runlevel [016]</pre>
</div>
<div>But what if the user is at home and will never connect to the domain controller? Well it will take a long time to boot. I&#8217;m sorry. If you have a better solution please comment. Let&#8217;s compare are boot time to see if it&#8217;s reasonable. All tests are on a Lenovo x120e. They are from power button to login prompt</div>
<div></div>
<div>Ubuntu 12.04 with my modified init scripts.</div>
<div>53 seconds (28 of those seconds are just waiting idle for Centrify)</div>
<div>Windows 7</div>
<div>47 seconds</div>
<div>Windows XP</div>
<div>A blazing 26 seconds! It would be pretty hard to recommend Linux if MS didn&#8217;t screw up NT with Windows Vista/7.</div>
<div></div>
<div>Anyway I&#8217;ve determined we are almost as fast as Win 7 which is what a x120e comes with. While it makes me cringe that I&#8217;m making the computer even slower, it&#8217;s on par and that makes it a good enough solution. Factor in time waiting for Win 7 to be generally slow after login and one could argue Linux is just as&#8230;.slow. As much as I hate Linux, I hate Windows more.</div>
<div></div>
<h1>Desktop Environment</h1>
<p>People seem to like Unity these days, but I still don&#8217;t. It&#8217;s also unfamiliar with new users. I&#8217;m more concerned about reducing tech support calls than wowing users. It&#8217;s fairly easy to remove it (but why no gui way?). Install gnome-fallback-session. Now edit /etc/lightdm/lightdm.conf and set</p>
<pre>user-session=gnome-classic</pre>
<p>Log in as a new user to test, to ensure it&#8217;s not just using the previous session. I like to just have one bottom gnome-panel sort of like classic windows.</p>
<p>Also install compiz config manager and enable alt tab which is notably missing!</p>
<p>Copy your ~/.config file into /etc/skel but then delete anything you don&#8217;t want (firefox, chrome, etc) so that new users get the same configuration you have now. There used to be a program called sabayon that did this in a more user friendly way but it&#8217;s too buggy and it&#8217;s faster to just copy things to /etc/skel. All in all the Windows way of having All Users is much easier IMO.</p>
<h1>Centralized Printing</h1>
<p>You can control printing by having a cups server. The newer printer menu doesn&#8217;t let you add cups servers&#8230;but the old gui is still there. Run system-config-printer and click Server Settings. Check Show printer shared by other systems. Hit Ok. Now Click Server settings again (this is a bug). Now advanced. Now click add and type in the address of your cups server. The printers will just show up like magic! If users aren&#8217;t allowed it to print it just won&#8217;t print. Which is really annoying actually. Why can&#8217;t it prompt for a password? It used to try this but the prompt was broken and never actually worked <img src='http://davidmburke.com/wp-includes/images/smilies/icon_sad.gif' alt=':(' class='wp-smiley' /> </p>
<p>I submitted a <a href="https://bugs.launchpad.net/ubuntu/+source/cups/+bug/989852">bug report</a> about it not asking for credentials.</p>
<h1>Windows Applications</h1>
<p>There&#8217;s probably a lot of Windows only applications you need to run. Crossover/Wine work for some like Office 2010. It&#8217;s really really buggy though. See my experience <a title="Computer Lab on the Cheap" href="http://davidmburke.com/2012/02/26/computer-lab-on-the-cheap/">here</a>. For programs that won&#8217;t run in wine you can use rdp. Ulteo makes a cool platform that lets users launch apps from a website. Great for Internet Explorer and centralized systems like proprietary databases.</p>
<h1>Java</h1>
<p>In short &#8211; Oracle and Canonical want to kill you in your sleep. It&#8217;s just about impossible to install Java now. Really I have no work around. Maybe you can get away with OpenJDK which runs on about 0 out of 10 applications I use. Canonical might just <a href="http://news.softpedia.com/news/Canonical-Will-Remove-Java-From-Ubuntu-241147.shtml">take away your java</a> without warning. You could install it from Oracle&#8230;but your users won&#8217;t get updates this way. You could try various<a href="http://www.webupd8.org/2012/01/install-oracle-java-jdk-7-in-ubuntu-via.html"> PPA scripts</a> but it seems Oracle is actively trying to stop this. I went with the Oracle installation which will leave me wide open to exploits. Lovely. You&#8217;re an IT person if your reading this. Do me a favor and DON&#8217;T EVER BUY ANYTHING FROM ORACLE.</p>
<h1>File Shares &#8211; Samba</h1>
<p>Samba works!!!! You can save files to a file share! Progress!!! I refer to <a href="https://bugs.launchpad.net/ubuntu/+source/libreoffice/+bug/835969">this</a> bug. You still might prefer to use pam_mount. I already talked about it in my previous <a title="Linux and Active Directory round 2" href="http://davidmburke.com/2010/09/16/linux-and-active-directory-round-2/">post</a> so I won&#8217;t again. It&#8217;s better if you want shares to come up automatically for users. If you just have a few users they can use nautilus to find the shares. Just type in smb://yourserver/share and save it as a bookmark. There is a browse share feature, but I&#8217;ve never seen this work in my life. But beware the bookmark thing won&#8217;t appear in wine! So if you need MS Office you users won&#8217;t be able to save as into the share!</p>
<p>If you do use pam_mount make sure to work around this <a href="https://bugs.launchpad.net/ubuntu/+source/likewise-open5/+bug/552001">bug</a> that prevents likewise-open from working with it. FYI <a href="https://bugs.launchpad.net/ubuntu/+source/libpam-mount/+bug/992101">This</a> bug is what keep /etc/skel from working with likewise open. If you followed my instructions you are using Centrify and don&#8217;t have this issue.</p>
<h1>Imaging</h1>
<p>I like to use clonezilla for imaging. Here is a script you can use to automate AD joining. Take a minute to look this over and put in your info. Then run it on cron @reboot. It runs only if it sees the hostname as image. Note this script is for likewise open. For centrify just replace the join command.</p>
<pre>#!/bin/bash
hostCurrent=$(hostname)
hostOld='image'
commonauth='/etc/pam.d/common-auth'
if [ "$hostCurrent" == "$hostOld" ]
then
 date &gt; /opt/ad.log # overwrite log the first time around
 # DOE mangled machines to the point that the first serial number is blank!
 host1=$(/usr/sbin/dmidecode | /bin/grep -E 'Serial Number:[[:space:]]*[^[:space:]]+' | /bin/sed 's/.*: \(.*\)/\1/;q')
 host=$(echo $host | /bin/sed 's/[ ]*//g')
 hostname $host
 echo $host &gt; /etc/hostname
 /opt/pingtest.sh
 sleep 10
 (
 /usr/bin/domainjoin-cli join your.domain.org 'user' 'password'
 adreturn=$?
 ) 2&gt;&amp;1 &gt;&gt; /opt/ad.log
 if [ $adreturn -ne 0 ]
 then
 echo "adjoin failed." &gt;&gt; /opt/ad.log
 exit 1
 fi
 echo "I'm itching to reboot." &gt;&gt; /opt/ad.log
 /sbin/reboot
fi</pre>
<p>I made <a href="http://code.google.com/p/django-inventory/">this</a> to take inventory. Sorry it sort of sucks and has no installer. Basically the idea to that a script runs that feeds some stats like hostname and hd freespace to some database.</p>
<h1>Admin access</h1>
<p>If you want to give some users sudo edit /etc/sudoers and add something like</p>
<pre><code>%ADMIN\\UnixAdmins ALL = (ALL) ALL</code></pre>
<h1><strong><span style="font-family: Monaco, Consolas, 'Andale Mono', 'DejaVu Sans Mono', monospace; font-size: x-small;"><span style="line-height: normal;">File Sync (roaming profiles)</span></span></strong></h1>
<p>You can try using <a href="http://davidmburke.com/2011/08/26/fully-automatic-backupsync-script-with-unison/">unison</a>. Owncloud has a desktop sync tool but it&#8217;s still in beta. I&#8217;m using that unison script for now. For desktops just use samba instead since you don&#8217;t need the files to copy to the local hard drive.</p>
<h1>Web Browser</h1>
<p>I use Chrome as the default web browser. There are some not so well documented ways you can make it a nicer experience with default preferences. I really hate the many pop ups it has on first launch for instance. First read this<br />
<a href="http://www.chromium.org/administrators/configuring-other-preferences" target="_blank">http://www.chromium.org/<wbr>administrators/configuring-<wbr>other-preferences<br />
</wbr></wbr></a>I don&#8217;t think it does a good job explaining that master_preferences goes next to the google-chrome executable (i.e. /opt/google/chrome/master_<wbr>preferences)</wbr></p>
<div>The real exhaustive list of preferences is here right in the source:</div>
<p><a href="http://src.chromium.org/viewvc/chrome/trunk/src/chrome/common/pref_names.cc?view=markup" target="_blank">http://src.chromium.org/<wbr>viewvc/chrome/trunk/src/<wbr>chrome/common/pref_names.cc?<wbr>view=markup</wbr></wbr></wbr></a></p>
<div>&#8216;user_skipped&#8217; disables the sync promo for instance. Here is my master preferences file. Over 9000 promo views is just for fun.</div>
<pre>{
 "homepage" : "companyportal.page.com",
 "homepage_is_newtabpage" : false,
 "browser" : {
 "show_home_button" : true,
 "check_default_browser" : false
 },
 "distribution" : {
 "skip_first_run_ui" : true,
 "show_welcome_page" : false,
 "make_chrome_default" : false
 },
 "first_run_tabs" : [
 "http://www.cristoreybrooklyn.org/portal"
 ],
 "sync_promo": {
 "startup_count": 1,
 "user_skipped": true,
 "view_count": 9001
 }
}</pre>
<h1>Overall thoughts</h1>
<p>12.04 IMO is as many steps forward as it is back. Some bugs are fixed and some are introduced. It&#8217;s a perfect example of why corporations don&#8217;t use linux outside IT. You can spend weeks setting up the perfect image in 11.10 but then 12.04 comes out and nothing you did before works. Here are my thoughts overall</p>
<p>+ Much better battery life<br />
+ Gnome fall back session is less buggy.<br />
- Gnome Classic is not as good as 10.04. For example the missing alt tab. Also sometimes wifi manager doesn&#8217;t display right for me. Again this is a trivial issue but for an end user to means calling tech support and for a company it means Linux is wasting money.<br />
- Likewise open is more buggy (doesn&#8217;t work with /etc/skel)<br />
- CUPS is more buggy (no warning about authorization failure) Also the better GUI isn&#8217;t accessible without knowing the cli command.<br />
- Less configurable overall compared to gnome 2.<br />
- Not Ubuntu&#8217;s fault, but java is now a nightmare</p>
<p>My advice to Ubuntu is to focus on quality control instead of UI. But it looks like they are going after consumers and not businesses. If they just fixed the bugs I mentioned I would recommend Linux to any small business. As is, I wouldn&#8217;t only recommend it to companies that have a deep understanding of Linux already. Windows Server will continue to dominate until Ubuntu is Linux for human beings who have jobs.</p>
<p>As a LTS release 12.04 disappoints me. After all the configuration work &#8211; the end user gets a worse experience in my case. A longer boot time and cups is less clear about authentication. But 11.10 is not LTS and not viable for long term deployment. My best hope is that some bugs will be fixed after release but based on past experience this is highly unlikely.</p>
]]></content:encoded>
			<wfw:commentRss>http://davidmburke.com/2012/04/26/ubuntu-12-04-deployment-with-active-directory/feed/</wfw:commentRss>
		<slash:comments>7</slash:comments>
		</item>
		<item>
		<title>Computer Lab on the Cheap</title>
		<link>http://davidmburke.com/2012/02/26/computer-lab-on-the-cheap/</link>
		<comments>http://davidmburke.com/2012/02/26/computer-lab-on-the-cheap/#comments</comments>
		<pubDate>Sun, 26 Feb 2012 21:46:46 +0000</pubDate>
		<dc:creator>david</dc:creator>
				<category><![CDATA[Uncategorized]]></category>

		<guid isPermaLink="false">http://davidmburke.com/?p=317</guid>
		<description><![CDATA[Budget &#8211; a bit over $3000. Goal &#8211; a 26 seat computer lab for a high school. Is it possible? Requirements The computers need to run basic office productivity, web browsing software, authentication, and monitoring what students are doing. Software &#8230; <a href="http://davidmburke.com/2012/02/26/computer-lab-on-the-cheap/">Continue reading <span class="meta-nav">&#8594;</span></a>]]></description>
			<content:encoded><![CDATA[<p>Budget &#8211; a bit over $3000. Goal &#8211; a 26 seat computer lab for a high school. Is it possible?</p>
<h1>Requirements</h1>
<p>The computers need to run basic office productivity, web browsing software, authentication, and monitoring what students are doing.</p>
<h1>Software</h1>
<p>I went with Linux Terminal Services Project (<a href="http://www.ltsp.org/">LTSP</a>). It doesn&#8217;t require a hard drive on the clients. The <a href="https://help.ubuntu.com/community/UbuntuLTSP">Ubuntu LTSP wiki</a> is a good resource for setting this up. I tried using Proxmox (openvz) at first so I could have 2 LTSP nodes mixed on the same hardware as less intensive applications. This became problematic as openvz doesn&#8217;t easily support fuse. When I plugged in a USB drive to a thin client it wouldn&#8217;t work. I could have tried to build my own kernel with openvz and fuse support but decided against it. Instead I placed everything on one dedicated server.</p>
<p>For authentication I use existing OpenLDAP and samba servers. I won&#8217;t go into specific on setting these up in this post. I will say use the Ubuntu server guide and not the community wiki for openldap. The wiki is terribly out of date and in many cases incomplete or wrong. LTSP uses the underlying authentication so if you can log into your server with ldap you should be good to go. I didn&#8217;t find anything different about LTSP. pam_mount, pam_ldap, etc all work as they would on a traditional desktop. I used pam_mount to mount the student&#8217;s document folder automatically. I wrote about pam_mount before when messing with <a title="Linux and Active Directory round 2" href="http://davidmburke.com/2010/09/16/linux-and-active-directory-round-2/">Active Directory</a>.</p>
<p>For printing I used cups 1.5. Cups 1.4 in Ubuntu has some nasty bugs that make the server hang in some cases in my experience. I used an existing cups server but it could also just exist on the LTSP server. By using a central cups server though you can have non thin client desktops use it as well. This makes administration pretty easy. I will say I hate how it always show all printers even ones the user doesn&#8217;t have permission for! Also just set your MaxClients to some really high number! I found you can reach the default (100) pretty easily even with just 40 or so users! Why is the default so low? A modern computer can probably handle millions of clients.</p>
<p>To customize the user interface, just drop files in /etc/skel. Setup the desktop how you want it and copy in ~/.config</p>
<p>To view what students are doing you can use italc. The <a href="https://help.ubuntu.com/community/UbuntuLTSP/iTalc">wiki</a> makes it sound super easy to install on LTSP. The wiki rather confusing. I put the lts.conf file at /var/lib/tftpboot/ltsp/i386/lts.conf The man page on lts.conf doesn&#8217;t say anything about START_ITALC. My file looks like this:</p>
<pre>[Default]
 START_ITALC = True</pre>
<h2>Windows Applications on LTSP</h2>
<p>While I would have rather have used LibreOffice, it was determined we needed Office 2007. This is somewhat of a project of it&#8217;s own. For personal use wine works great with Office. For multiple, non-technical users? Forget it! I used <a href="http://www.codeweavers.com/products/crossover/">Crossover Professional</a> which comes with multi-user support and hooks to do extra scripting. Of course it does cost money and I sank about a third of my budget into this. Even with crossover be ready for lots of bugs and hacks. My issues included</p>
<ul>
<li>Windows desktop is not the actual ~/Desktop folder! Why not!? I used a <a href="http://www.codeweavers.com/support/docs/crossover-pro/bottlehooks">script</a> to symlink them. I named by script according to their convention 01.create-stub so it gets called when a user logs into the first time and gets a new MS Office &#8220;stub&#8221;, which is just the user specific parts owned by the local user instead of root. In regular wine you would only have this and thus you can&#8217;t have multi-user support.<br />
#!/bin/sh<br />
rm -rf &#8220;$WINEPREFIX/dosdevices/c:/users/crossover/Desktop&#8221;<br />
ln -s -f &#8220;$HOME/Desktop&#8221; &#8220;$WINEPREFIX/dosdevices/c:/users/crossover/Desktop&#8221;</li>
<li>Can&#8217;t print to PDF. wine/crossover won&#8217;t allow it. It sort of works if you install cups-pdf but it&#8217;s too buggy for production use. Office 2007 has an add-on to save directly as PDF. So install it and any SP&#8217;s for Office.</li>
<li>wine remembers every printer forever! If you connect a printer via cups and remove it, Office will forever think it still exists. Again fine for personal use but in a school forget it! And what if you ever want to rename printers in cups? Duplicates. Delete all printer references in /opt/cxoffice/support/ms_office/system.reg and user.reg. At least this way the user starts out with no dead printers. I need to make a script to delete them on startup or something, it&#8217;s on my to do list.</li>
<li>Default applications &#8211; It doesn&#8217;t work. I wanted to make .doc open by default with Office. Crossover people<a href="http://www.codeweavers.com/support/tickets/browse/?ticket_id=867751"> tell me</a> it&#8217;s a bug in Ubuntu. You can fix it by editing<br />
/etc/X11/Xsession.d/55gnome-session_gnomerc<br />
Change<br />
XDG_DATA_DIRS=/usr/share/gnome:/usr/local/share:/usr/share<br />
to<br />
XDG_DATA_DIRS=/usr/local/share:/usr/share/gnome:/usr/share</li>
<li>Lots of minor glitchy bugs such as the &#8220;Office button&#8221; menu often disappearing.</li>
<li>Excel uses a mdi form that doesn&#8217;t display correctly on the gnome task bar. I have no solution to this other than starting new Excel processes (by starting Excel each time, not just opening xls files). Why do MDI forms still exists? This is not DOS. We have operating system level window management these days.</li>
</ul>
<div>Finally you can package your wine &#8220;bottle&#8221; as a deb for easy deployment. Fun fact &#8211; because Word is a single process application if you log into 2 LTSP as the same user and both run it, two windows will come up on the first users screen and none on the other! But with Excel you can run multiple processes. Ha and people say LibreOffice has a messy code base. This doesn&#8217;t happen when using different users so it&#8217;s not a problem.</div>
<div></div>
<h1>Hardware</h1>
<p>I find it pretty easy to come across donated Pentium 4 desktops and servers. My dedicated server was a HP ProLiant G5 with 16GB ram and two four-core Xeon processors. That&#8217;s 8 cores total. A very nice server. One could probably get away with using slightly older models or could just add many into a cluster. For a non profit it seems reasonable to me that you can find such equipment for free, though I do live in NYC which might make it easier as there are not shortage of mega financial firms with cycling through equipment.</p>
<p>Old desktops are gross, so hide them under the desk if possible.</p>
<p><a href="http://davidmburke.com/files/2012/02/IMG_20120225_135329.jpg"><img src="http://davidmburke.com/files/2012/02/IMG_20120225_135329-300x226.jpg" alt="" width="300" height="226" /></a></p>
<p>Flat screen monitors are usually never given away. Be prepared to buy them. About a 1/3rd of my budget went to this and keyboards/mice.</p>
<p>It&#8217;s easier to find it easier to get old non networked printers. If they have a parallel port you can buy a &#8220;Print Server&#8221; and get it online.</p>
<h2>Networking</h2>
<p>I ran 2 gigabits of bandwidth from the server to the lab over two 1 gigabit cables/switches. Check out the <a href="http://edubuntu.org/documentation/10.10/installation-guide">Edubuntu LTSP requirements page</a>. I used 2 switches in the lab each with a 1 gigabit backend and 100 megabits to each thin client. Another third of my budget went into networking equipment. Thin Clients are of course going to be network intensive! I found some volunteers to help with running cables and computers. Thanks volunteers!!</p>
<div id="attachment_325" class="wp-caption alignnone" style="width: 319px"><a href="http://davidmburke.com/files/2012/02/Screenshot-at-2012-02-27-111949.png"><img class="size-full wp-image-325" src="http://davidmburke.com/files/2012/02/Screenshot-at-2012-02-27-111949.png" alt="" width="309" height="207" /></a><p class="wp-caption-text">Expenses</p></div>
<p><a href="http://davidmburke.com/files/2012/02/IMG_20120225_122510.jpg"><img class="alignnone size-medium wp-image-319" src="http://davidmburke.com/files/2012/02/IMG_20120225_122510-300x225.jpg" alt="" width="300" height="225" /></a></p>
<div id="attachment_318" class="wp-caption alignnone" style="width: 594px"><a href="http://davidmburke.com/files/2012/02/IMG_20120225_173503.jpg"><img class=" wp-image-318" src="http://davidmburke.com/files/2012/02/IMG_20120225_173503-1024x771.jpg" alt="" width="584" height="439" /></a><p class="wp-caption-text">The Volunteers and I</p></div>
<p><a href="http://davidmburke.com/files/2012/02/IMG_20120227_165855.jpg"><img class="alignnone size-medium wp-image-330" src="http://davidmburke.com/files/2012/02/IMG_20120227_165855-300x225.jpg" alt="" width="300" height="225" /></a><a href="http://davidmburke.com/files/2012/02/IMG_20120227_165913.jpg"><img class="alignnone size-medium wp-image-329" src="http://davidmburke.com/files/2012/02/IMG_20120227_165913-300x225.jpg" alt="" width="300" height="225" /></a><a href="http://davidmburke.com/files/2012/02/IMG_20120227_165838.jpg"><img class="alignnone size-medium wp-image-328" src="http://davidmburke.com/files/2012/02/IMG_20120227_165838-300x225.jpg" alt="" width="300" height="225" /></a></p>
<h1>Results</h1>
<p>LTSP runs fast with the right hardware! If you ever used VNC (please don&#8217;t it&#8217;s terribly slow!) you might be scared of remote access. LTSP uses X&#8217;s natural networking capabilities and it might not be as fast as RDP, but it&#8217;s close. Mouse input is very smooth. Programs start up super fast (they are probably all already in ram). I&#8217;ll probably try playing around with deploying some fat clients on some of the better machines to increase performance.</p>
<p>If you read my other posts you know I really get annoyed with all the Linux Desktop bugs. But most of that is mitigated by using thin clients. The server is always on and there are no laptop users. Syncing files and fighting upstart just becomes a moot point.</p>
<p>Installing new application is dead simple. I can apt-get install them on the ltsp server and they just appear even if a user is already logged in.</p>
<p>I just set up the lab, so I&#8217;ll update this post with results of actual student usage! And be sure to donate to <a href="http://cristoreybrooklyn.org/support-cristo-rey/donations/">Cristo Rey Brooklyn High School</a> so we can get more computers! Otherwise I&#8217;ll assume you hate education.</p>
]]></content:encoded>
			<wfw:commentRss>http://davidmburke.com/2012/02/26/computer-lab-on-the-cheap/feed/</wfw:commentRss>
		<slash:comments>1</slash:comments>
		</item>
		<item>
		<title>Django End User Defined Custom Fields</title>
		<link>http://davidmburke.com/2011/11/07/django-end-user-defined-custom-fields/</link>
		<comments>http://davidmburke.com/2011/11/07/django-end-user-defined-custom-fields/#comments</comments>
		<pubDate>Mon, 07 Nov 2011 23:50:17 +0000</pubDate>
		<dc:creator>david</dc:creator>
				<category><![CDATA[Uncategorized]]></category>

		<guid isPermaLink="false">http://davidmburke.com/?p=309</guid>
		<description><![CDATA[I want a way for end users to add fields to my Django app without programming. I&#8217;ll publish this eventually, but here how to implement it. Latest version is now at http://code.google.com/p/django-custom-field/ First I made 2 models. One for the field and one &#8230; <a href="http://davidmburke.com/2011/11/07/django-end-user-defined-custom-fields/">Continue reading <span class="meta-nav">&#8594;</span></a>]]></description>
			<content:encoded><![CDATA[<p>I want a way for end users to add fields to my Django app without programming. <del>I&#8217;ll publish this eventually, but here how to implement it</del>. Latest version is now at <a href="http://code.google.com/p/django-custom-field/">http://code.google.com/p/django-custom-field/</a></p>
<p><a href="http://davidmburke.com/files/2011/11/screenshot4.png"><img class="alignnone size-full wp-image-312" src="http://davidmburke.com/files/2011/11/screenshot4.png" alt="" width="953" height="174" /></a><a href="http://davidmburke.com/files/2011/11/screenshot3.png"><img class="alignnone size-full wp-image-311" src="http://davidmburke.com/files/2011/11/screenshot3.png" alt="" width="369" height="161" /></a></p>
<p>First I made 2 models. One for the field and one for the values associated with the field.</p>
<pre>from django.contrib.contenttypes.models import ContentType
from django.db import models

class CustomField(models.Model):
    """
    A field abstract -- it describe what the field is.  There are one of these
    for each custom field the user configures.
    """
    name = models.CharField(max_length=75)
    content_type = models.ForeignKey(ContentType)
    field_type = models.CharField(max_length=1, choices=(('t','Text'),('i','Integer'),('b','Boolean (checkbox)'),), default='t')

    def get_value_for_object(self,obj):
        return CustomFieldValue.objects.get_or_create(field=self,object_id=obj.id)[0]

    def __unicode__(self):
        return unicode(self.name)

    class Meta:
        unique_together = ('name','content_type')

class CustomFieldValue(models.Model):
    """
    A field instance -- contains the actual data.  There are many of these, for
    each value that corresponds to a CustomField for a given model.
    """
    field = models.ForeignKey(CustomField, related_name='instance')
    value = models.CharField(max_length=255,blank=True,null=True)
    object_id = models.PositiveIntegerField()
    #content_type = models.ForeignKey(ContentType)

    def __unicode__(self):
        return unicode(self.value)</pre>
<p>Next I wanted this to work with admin. So I extend any ModelAdmin you want with this I need. Notice we don&#8217;t handle errors! I have my 3 simple data types all do client side validation, then if the server validation comes up invalid, it just throws them away. It&#8217;s less than ideal but I wasn&#8217;t sure how to get it to display the errors correctly.</p>
<pre>from django import forms
from django.contrib.contenttypes.models import ContentType
from django.contrib import admin
from django.forms.widgets import TextInput

from models import *

class NumberInput(TextInput):
    input_type = 'number'

class CustomFieldAdmin(admin.ModelAdmin):
    def __create_custom_form(self, obj_id=None):
        custom_fields = CustomField.objects.filter(content_type=ContentType.objects.get_for_model(self.model))

        custom_form = forms.Form(prefix="cstm")
        for field in custom_fields:
            if field.field_type == 'i':
                custom_form.fields[field.name] = forms.IntegerField(label=field.name, required=False, widget=NumberInput(attrs={'style':'text-align:right;','step':1}))
            elif field.field_type == 'b':
                custom_form.fields[field.name] = forms.BooleanField(label=field.name, required=False)
            else:
                custom_form.fields[field.name] = forms.CharField(label=field.name, max_length=255, required=False)
            if obj_id:
                value = CustomFieldValue.objects.get_or_create(field=field,object_id=obj_id)[0]
                custom_form.fields[field.name].initial = value
        return custom_form

    def render_change_form(self, request, context, *args, **kwargs):
        context['custom_form'] = self.__create_custom_form(context['original'].id)
        return super(CustomFieldAdmin, self).render_change_form(request, context, *args, **kwargs)

    def save_model(self, request, obj, form, change):
        custom_form = self.__create_custom_form()
        custom_form.data = request.POST
        custom_form.is_bound = True
        if custom_form.is_valid():
            data = custom_form.cleaned_data
            for key,data_field in data.items():
                custom_field = CustomField.objects.get_or_create(content_type=ContentType.objects.get_for_model(self.model), name=key)[0]
                custom_value = CustomFieldValue.objects.get_or_create(field=custom_field,object_id=obj.id)[0]
                custom_value.value = data_field
                custom_value.save()
        # Hope that client side validation works since we don't handle errors here!

        return super(CustomFieldAdmin, self).save_model(request, obj, form, change)</pre>
<p>Now we have to edit the admin template. This isn&#8217;t ideal but I couldn&#8217;t think of any other way. Edit change_form.html and add</p>
<pre>{% include "admin/includes/custom_field_fieldset.html" with custom_form=custom_form %}</pre>
<p>wherever you want. I say it&#8217;s not ideal because if you make lots of customizations it&#8217;s hard to keep track. Blocks help with this, BUT Django&#8217;s admin content block is pretty big, and I wanted to add mine basically in the middle of the content. Now you need the referenced custom_field_fieldset.html</p>
<pre>{% spaceless %}
{% if custom_form.fields %}

  &lt;fieldset class="module"&gt;
    &lt;h2 class="collapse-handler"&gt;Custom Fields&lt;/h2&gt;

    {% for field in custom_form %}
      &lt;div class="row cells-1 {{ custom_form.prefix }}-{{ field.name }}"&gt;
        &lt;div class="column span-4"&gt;
          {{ field.label_tag }}
        &lt;/div&gt;
        &lt;div class="column span-flexible"&gt;
          {{ field }}
        &lt;/div&gt;
      &lt;/div&gt;
    {% endfor %}
  &lt;/fieldset&gt;

{% endif %}
{% endspaceless %}</pre>
<p>That&#8217;s it. So now for the real test, can I add a custom field to my custom field model?</p>
<p><a href="http://davidmburke.com/files/2011/11/yodawg.png"><img class="alignnone size-full wp-image-310" src="http://davidmburke.com/files/2011/11/yodawg.png" alt="" width="743" height="462" /></a></p>
<p>Now you can customize your fields while you customize fields! If you need to access the custom fields programmatically you can make shortcuts like</p>
<pre>def get_custom_fields(self):
    return CustomField.objects.filter(content_type=ContentType.objects.get_for_model(Whatever))</pre>
<p>Next I need to add integration with my applications import tool, make something so you can extend Model to get that helper function, and integrate with <a title="Django admin: Export ForeignKey subfields to XLS" href="http://davidmburke.com/2011/05/19/django-admin-export-foreignkey-subfields-to-xls/">django-admin-export</a>. Maybe even create a dropdown field_type which would require another model to store the data in. Then I promise to post to pypi.</p>
]]></content:encoded>
			<wfw:commentRss>http://davidmburke.com/2011/11/07/django-end-user-defined-custom-fields/feed/</wfw:commentRss>
		<slash:comments>1</slash:comments>
		</item>
		<item>
		<title>Fully automatic backup/sync script with Unison</title>
		<link>http://davidmburke.com/2011/08/26/fully-automatic-backupsync-script-with-unison/</link>
		<comments>http://davidmburke.com/2011/08/26/fully-automatic-backupsync-script-with-unison/#comments</comments>
		<pubDate>Fri, 26 Aug 2011 03:22:55 +0000</pubDate>
		<dc:creator>david</dc:creator>
				<category><![CDATA[Uncategorized]]></category>

		<guid isPermaLink="false">http://davidmburke.com/?p=300</guid>
		<description><![CDATA[I&#8217;ve been searching for something to replace offline files in Linux. It&#8217;s a great feature that Linux just doesn&#8217;t have where files on a share can be stored locally and synced when back on the network. A Dropbox like solution &#8230; <a href="http://davidmburke.com/2011/08/26/fully-automatic-backupsync-script-with-unison/">Continue reading <span class="meta-nav">&#8594;</span></a>]]></description>
			<content:encoded><![CDATA[<p>I&#8217;ve been searching for something to replace offline files in Linux. It&#8217;s a great feature that Linux just doesn&#8217;t have where files on a share can be stored locally and synced when back on the network. A Dropbox like solution seems like a close enough alternative, but these programs are expensive and can&#8217;t be centrally managed. So I made my own, albeit less fully featured. Unison is an open source bidirectional sync tool. It can be set to use time based conflict resolution without notifying the user. Let&#8217;s hope the clocks are correct.</p>
<p>One problem with Unison is deploying it automatically. I have a file server that I mount with pam_mount. I mount it to ~/.whatever so it stays hidden to the user. Next I put a script to insert it into crontab. Now all the user has to do is run that script and they files are backed up. libnotify even tells them so. The script also takes care of the initial server to local sync. If we didn&#8217;t do this unison would think we wanted to delete all files on the server (don&#8217;t worry it would throw an error unless you used a specific argument). So here it is.</p>
<pre class="brush: python; title: ; notranslate">import os

# Place this in a cron job for say every 2 minutes. To script this use
# crontab -l | { cat; echo &quot;0 0 0 0 0 some entry&quot;; } | crontab -

# You may also want to run ntpd on all clients to keep the time in sync.

# Array of locations of local and remote locations. I recommend mounting remote folders in
# Example (('/home/user/Documents', '/remotehostname/somefolder'),)
sync_locations = (
    (&quot;/home/david/local&quot;, &quot;/home/david/server&quot;),
)
# Check if host is up
check_host = &quot;localhost&quot;

# Only allow this program to run once!
try:
    import socket
    s = socket.socket()
    host = socket.gethostname()
    port = 35636    #make sure this port is not used on this system
    s.bind((host, port))
except:
    exit()
if not os.path.exists(os.getenv(&quot;HOME&quot;) + '/.unison_backup'):
    os.mkdir(os.getenv(&quot;HOME&quot;) + '/.unison_backup')

if os.path.exists(sync_locations[0][0]) and 0 == os.system('ping -c 1 ' + check_host):
    if not os.path.isfile(os.getenv(&quot;HOME&quot;) + &quot;/.unison_backup/first_sync_complete&quot;):
        os.system('notify-send &quot;Unison Backup&quot; &quot;Starting Initial Sync. You will be notified when finished.&quot;')
        for sync_location in sync_locations:
            exit_code = os.system('rsync -r ' + sync_location[1] + &quot;/ &quot; + sync_location[0])
            if exit_code != 0:
                os.system('notify-send &quot;Could not sync!&quot;')
                exit()
        open(os.getenv(&quot;HOME&quot;) + '/.unison_backup/first_sync_complete', 'w').close()
        os.system('notify-send &quot;Unison Backup&quot; &quot;Initial Sync Complete&quot;')

    # Run Unison
    for sync_location in sync_locations:
        os.system('unison %s %s -batch -prefer newer -times=true' % (sync_location[0], sync_location[1]))
</pre>
]]></content:encoded>
			<wfw:commentRss>http://davidmburke.com/2011/08/26/fully-automatic-backupsync-script-with-unison/feed/</wfw:commentRss>
		<slash:comments>2</slash:comments>
		</item>
		<item>
		<title>Mail Merge in Libreoffice</title>
		<link>http://davidmburke.com/2011/08/10/mail-merge-in-libreoffice/</link>
		<comments>http://davidmburke.com/2011/08/10/mail-merge-in-libreoffice/#comments</comments>
		<pubDate>Wed, 10 Aug 2011 16:33:18 +0000</pubDate>
		<dc:creator>david</dc:creator>
				<category><![CDATA[Uncategorized]]></category>
		<category><![CDATA[amitheonlywhodoesthis]]></category>
		<category><![CDATA[libreoffice]]></category>
		<category><![CDATA[shouldalreadyexist]]></category>
		<category><![CDATA[terriblesoftware]]></category>

		<guid isPermaLink="false">http://davidmburke.com/?p=290</guid>
		<description><![CDATA[A less developer oriented post today. I find there to be an incredible lack of tutorials on how to mail merge in LibreOffice. So I made my own. How to Mail Merge with LibreOffice  This tutorial assumes you have an &#8230; <a href="http://davidmburke.com/2011/08/10/mail-merge-in-libreoffice/">Continue reading <span class="meta-nav">&#8594;</span></a>]]></description>
			<content:encoded><![CDATA[<p>A less developer oriented post today. I find there to be an incredible lack of tutorials on how to mail merge in LibreOffice. So I made my own.</p>
<h1 style="text-align: left" align="CENTER"><span style="font-family: 'Liberation Sans', Arial, sans-serif"><strong>How to Mail Merge with LibreOffice</strong></span><span class="Apple-style-span" style="font-size: 13px;font-weight: normal"> </span></h1>
<p align="LEFT">This tutorial assumes you have an xls or ods file with data you want to “merge” to a document.</p>
<ol>
<li>
<p align="LEFT">Ensure you have proper headers on your spreadsheet and save it. Take note where you save it to!</p>
<p align="LEFT"><a href="http://davidmburke.com/files/2011/08/1.png"><img class="size-full wp-image-291 alignnone" src="http://davidmburke.com/files/2011/08/1.png" alt="Compiz why did you make this blue?" width="230" height="264" /></a></p>
</li>
<li>
<p align="LEFT">In Writer, Click Edit then Exchange Database&#8230; Then click Browse.  Select the file you just made</p>
<p><div id="attachment_292" class="wp-caption alignnone" style="width: 758px"><a href="http://davidmburke.com/files/2011/08/2.png"><img class="size-full wp-image-292" src="http://davidmburke.com/files/2011/08/2.png" alt="" width="748" height="252" /></a><p class="wp-caption-text">Why is it named exchange database?</p></div></li>
<li>
<p align="LEFT">Click View, Data sources (or press F4)</p>
</li>
<li>
<p align="LEFT">On the left is a list of data sources. These should include the file you just made. Select it, then Tables, then Sheet1 (or the name of the sheet you want)<br />
<a href="http://davidmburke.com/files/2011/08/3.png"><img class="alignnone size-full wp-image-293" src="http://davidmburke.com/files/2011/08/3.png" alt="" width="780" height="180" /></a></p>
</li>
<li>
<p align="LEFT">You will see the data from the spreadsheet. Click and drag the column you want into your document. For example if I wanted First Name, I would click and drag the First Name column title and not an individual cell like Bob. You will notice it appears gray in your document. You are free to cut and paste it or change the formatting. These words will be replaced with the data in your spreadsheet.</p>
<p align="LEFT"><a href="http://davidmburke.com/files/2011/08/4.png"><img class="alignnone size-full wp-image-294" src="http://davidmburke.com/files/2011/08/4.png" alt="" width="389" height="187" /></a></p>
</li>
<li>
<p align="LEFT">Click Tools, Mail Merge Wizard or click the envelope icon in data sources. This stuff is mostly stupid and deals with preformatted address blocks, etc. I&#8217;ve never under any circumstance use them but you will need to disable them all. Work through each step and click next to continue:</p>
<ol>
<li>
<p align="LEFT"><span style="font-size: x-small">Select starting Document: Select current document.</span></p>
</li>
<li>
<p align="LEFT"><span style="font-size: x-small">Select document type: In this example we intend to print or save the file so select Letter.</span></p>
</li>
</ol>
</li>
</ol>
<ol>
<ol start="3">
<li>
<p align="LEFT"><span style="font-size: x-small">Insert address block: Ensure &#8220;This document shall contain an address block&#8221; is </span><span style="font-size: x-small">un</span><span style="font-size: x-small">checked. </span></p>
</li>
<li>
<p align="LEFT"><span style="font-size: x-small">Create salutation: Ensure &#8220;This document should contain salutation&#8221; is unchecked.</span></p>
</li>
<li>
<p align="LEFT"><span style="font-size: x-small">Edit document: By clicking the left and right arrow we can preview each page.</span></p>
</li>
<li>
<p align="LEFT"><span style="font-size: x-small">Personalize document: This screen shows us the finished document which is editable.</span></p>
</li>
<li>
<p align="LEFT"><span style="font-size: x-small">Save, print or send: What do you want to do this the finished document? You may save it, print it, or email it (may require additional setup). Save merged document will save the finished multi page document and Save starting document will save your template for later use.</span></p>
</li>
</ol>
</ol>
]]></content:encoded>
			<wfw:commentRss>http://davidmburke.com/2011/08/10/mail-merge-in-libreoffice/feed/</wfw:commentRss>
		<slash:comments>10</slash:comments>
		</item>
		<item>
		<title>Adding new form in a formset</title>
		<link>http://davidmburke.com/2011/07/25/adding-new-form-in-a-formset/</link>
		<comments>http://davidmburke.com/2011/07/25/adding-new-form-in-a-formset/#comments</comments>
		<pubDate>Mon, 25 Jul 2011 17:19:49 +0000</pubDate>
		<dc:creator>david</dc:creator>
				<category><![CDATA[Uncategorized]]></category>
		<category><![CDATA[django]]></category>

		<guid isPermaLink="false">http://davidmburke.com/?p=274</guid>
		<description><![CDATA[Everything I read about adding a new form to a formset with javascript involves cloning an existing form. This is a terrible method, what if the initial forms are 0? What about initial data? Here&#8217;s IMO better way to do &#8230; <a href="http://davidmburke.com/2011/07/25/adding-new-form-in-a-formset/">Continue reading <span class="meta-nav">&#8594;</span></a>]]></description>
			<content:encoded><![CDATA[<p>Everything I read about adding a new form to a formset with javascript involves cloning an existing form. This is a terrible method, what if the initial forms are 0? What about initial data? Here&#8217;s IMO better way to do it that uses empty_form, a function Django gives you to create a form where i is __prefix__ so you can easily replace it.</p>
<p><a href="http://davidmburke.com/files/2011/07/screenshot2.png"><img src="http://davidmburke.com/files/2011/07/screenshot2.png" alt="" width="408" height="224" class="alignnone size-full wp-image-279" /></a></p>
<p>Add this under you &#8220;Add new FOO&#8221; button. In my case I have a question_form with many answers (answers_formset).</p>
<pre class="brush: xml; title: ; notranslate">
&lt;script&gt;
      var form_count_{{ question_form.prefix }} = {{ answers_formset.total_form_count }};
      $('#add_more_{{ question_form.prefix }}').click(function() {
          var form = '{{answers_formset.empty_form.as_custom|escapejs}}'.replace(/__prefix__/g, form_count_{{ question_form.prefix }});
          $('#answers_div_{{ question_form.prefix }}').append(form);
          form_count_{{ question_form.prefix }}++;
          $('#id_{{ answers_formset.prefix }}-TOTAL_FORMS').val(form_count_{{ question_form.prefix }});
      });
&lt;/script&gt;
</pre>
<p>This creates you empty_form right in javascript, replaces the __prefix__ with the correct number and inserts it, in my case I made an answers_div. See empty_form.as_custom, you could just do empty_form but that would just give the you basic form html. I want custom html. Make a separate template for this. Here&#8217;s mine but this just an example.</p>
<pre class="brush: xml; title: ; notranslate">
{{ answer.non_field_errors }}
{% for hidden in answer.hidden_fields %} {{ hidden }} {% endfor %}
&lt;table&gt;
    &lt;tr&gt;
        &lt;td&gt;
            &lt;span class=&quot;answer_span&quot;&gt;{{ answer.answer }} {{ answer.answer.errors }}&lt;/span&gt;
        &lt;/td&gt;
        ......etc.......
    &lt;/tr&gt;
&lt;/table&gt;
</pre>
<p>In your original template you can add the forms like this {% include &#8220;omr/answer_form.html&#8221; with answer=answer %}<br />
But for the as_custom you need to edit your form itself to add the function.</p>
<pre class="brush: python; title: ; notranslate">
def as_custom(self):
    t = template.loader.get_template('answer_form.html')
    return t.render(Context({'answer': self},))
</pre>
<p>I find this method far more stable than trying to clone existing forms. It seems to play well with the javascript I have in some of my widgets. Clone on the other hand gave me tons of trouble and hacks needed to fix it.</p>
]]></content:encoded>
			<wfw:commentRss>http://davidmburke.com/2011/07/25/adding-new-form-in-a-formset/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Django get_or_default</title>
		<link>http://davidmburke.com/2011/07/23/django-get_or_default/</link>
		<comments>http://davidmburke.com/2011/07/23/django-get_or_default/#comments</comments>
		<pubDate>Sat, 23 Jul 2011 14:27:48 +0000</pubDate>
		<dc:creator>david</dc:creator>
				<category><![CDATA[Uncategorized]]></category>
		<category><![CDATA[django]]></category>

		<guid isPermaLink="false">http://davidmburke.com/?p=270</guid>
		<description><![CDATA[Quick hack today. Often I find myself wanting to get some django object, but in the case it doesn&#8217;t exist default it to some value. Specially I keep my end user configurable settings in my database. Typically I set this &#8230; <a href="http://davidmburke.com/2011/07/23/django-get_or_default/">Continue reading <span class="meta-nav">&#8594;</span></a>]]></description>
			<content:encoded><![CDATA[<p>Quick hack today. Often I find myself wanting to get some django object, but in the case it doesn&#8217;t exist default it to some value. Specially I keep my end user configurable settings in my database. Typically I set this up with initial data so all the settings are already there, but sometimes I&#8217;ll add a setting and forgot to add it on some site instance. </p>
<pre class="brush: python; title: ; notranslate">class Callable:
    def __init__(self, anycallable):
        self.__call__ = anycallable

def get_or_default(name, default=None):
        &quot;&quot;&quot; Get the config object or create it with a default. Always use this when gettings configs&quot;&quot;&quot;
        object, created = Configuration.objects.get_or_create(name=name)
        if created:
            object.value = default
            object.save()
        return object
    get_or_default = Callable(get_or_default)</pre>
<p>Now I can safely call things like edit_all = Configuration.get_or_default(&#8220;Edit all fields&#8221;, &#8220;False&#8221;) which will return my configuration object with the value set as False if not specified. Much better than a 500 error. There are plenty of other uses for this type of logic. Get_or_return_none for example. The goal for me is to stop 500 errors from my own carelessness by having safe defaults.</p>
]]></content:encoded>
			<wfw:commentRss>http://davidmburke.com/2011/07/23/django-get_or_default/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Django admin: Export ForeignKey subfields to XLS</title>
		<link>http://davidmburke.com/2011/05/19/django-admin-export-foreignkey-subfields-to-xls/</link>
		<comments>http://davidmburke.com/2011/05/19/django-admin-export-foreignkey-subfields-to-xls/#comments</comments>
		<pubDate>Thu, 19 May 2011 19:00:52 +0000</pubDate>
		<dc:creator>david</dc:creator>
				<category><![CDATA[Uncategorized]]></category>

		<guid isPermaLink="false">http://davidmburke.com/?p=256</guid>
		<description><![CDATA[Last post I made an export to XLS tool for Django&#8217;s admin interface. A common request is to quickly export related field data as well. Today I&#8217;ll show you how to export foreign key fields, recursively. It doesn&#8217;t work for &#8230; <a href="http://davidmburke.com/2011/05/19/django-admin-export-foreignkey-subfields-to-xls/">Continue reading <span class="meta-nav">&#8594;</span></a>]]></description>
			<content:encoded><![CDATA[<p><a href="http://davidmburke.com/files/2011/05/screenshot3.png"><img src="http://davidmburke.com/files/2011/05/screenshot3.png" alt="" width="854" height="365" class="alignnone size-full wp-image-257" /></a></p>
<p>Last post I made an export to XLS tool for Django&#8217;s admin interface. A common request is to quickly export related field data as well. Today I&#8217;ll show you how to export foreign key fields, recursively. It doesn&#8217;t work for ManyToMany which are of course much more complicated since it can return more than one result! First I though it would be nice to have a view permission for my models. If I just want to export them, why would I need edit permission? <a href="http://blog.nyaruka.com/adding-a-view-permission-to-django-models">This</a> blog shows a quick way to get view permissions to all models. Note you could just use the edit permission instead if that works for you.</p>
<p>This time AJAX is a must. I&#8217;m using it to insert html where I want it to dynamically construct the export view. This has the advantage of not getting caught up in loops of references referencing other references&#8230;uck recursion. I&#8217;ve heavily modified my files from the <a href="http://davidmburke.com/2011/05/17/django-admin-better-export-to-xls/">last post</a>. If you really want to understand how all this works, I suggest you start there. Note I still use the export_simple_selected_objects function from before, left untouched. Here are my new views:</p>
<pre class="brush: python; title: ; notranslate">
def get_fields_for_model(request):
    &quot;&quot;&quot; Get the related fields of a selected foreign key &quot;&quot;&quot;
    model_class = ContentType.objects.get(id=request.GET['ct']).model_class()
    queryset = model_class.objects.filter(pk__in=request.GET['ids'].split(','))

    rel_name = request.POST['rel_name']
    related = model_class
    for item in rel_name.split('__'):
        related = getattr(related, item).field.rel.to

    model = related
    model_fields = model._meta.fields
    previous_fields = rel_name

    for field in model_fields:
        if hasattr(field, 'related'):
            if request.user.has_perm(field.rel.to._meta.app_label + '.view_' + field.rel.to._meta.module_name):
                field.perm = True

    return render_to_response('sis/export_to_xls_related.html', {
        'model_name': model_class._meta.verbose_name,
        'model': model._meta.app_label + &quot;:&quot; + model._meta.module_name,
        'fields': model_fields,
        'previous_fields': previous_fields,
    }, RequestContext(request, {}),)

def admin_export_xls(request):
    model_class = ContentType.objects.get(id=request.GET['ct']).model_class()
    queryset = model_class.objects.filter(pk__in=request.GET['ids'].split(','))
    get_variables = request.META['QUERY_STRING']
    model_fields = model_class._meta.fields

    for field in model_fields:
        if hasattr(field, 'related'):
            if request.user.has_perm(field.rel.to._meta.app_label + '.view_' + field.rel.to._meta.module_name) or request.user.has_perm(field.rel.to._meta.app_label + '.change_' + field.rel.to._meta.module_name):
                field.perm = True

    if 'xls' in request.POST:
        workbook = xlwt.Workbook()
        worksheet = workbook.add_sheet(unicode(model_class._meta.verbose_name_plural))

        # Get field names from POST data
        fieldnames = []
        # request.POST reorders the data <img src='http://davidmburke.com/wp-includes/images/smilies/icon_sad.gif' alt=':(' class='wp-smiley' />  There's little reason to go through all
        # the work of reordering it right again when raw data is ordered correctly.
        for value in request.raw_post_data.split('&amp;'):
            if value[:7] == &quot;field__&quot; and value[-3:] == &quot;=on&quot;:
                fieldname = value[7:-3]
                app = fieldname.split('__')[0].split('%3A')[0]
                model = fieldname.split('__')[0].split('%3A')[1]
                # Server side permission check, edit implies view.
                if request.user.has_perm(app + '.view_' + model) or request.user.has_perm(app + '.change_' + model):
                    fieldnames.append(fieldname)

        # Title
        for i, field in enumerate(fieldnames):
            #ex field 'sis%3Astudent__fname'
            field = field.split('__')
            model = get_model(field[0].split('%3A')[0], field[0].split('%3A')[1])
            txt = &quot;&quot;
            for sub_field in field[1:-1]:
                txt += sub_field + &quot; &quot;
            txt += model._meta.get_field_by_name(field[-1])[0].verbose_name
            worksheet.write(0,i, txt)

        # Data
        for ri, row in enumerate(queryset): # For Row iterable, data row in the queryset
            for ci, field in enumerate(fieldnames): # For Cell iterable, field, fields
               try:
                    field = field.split('__')
                    data = getattr(row, field[1])
                    for sub_field in field[2:]:
                        data = getattr(data, sub_field)
                    worksheet.write(ri+1, ci, unicode(data))
                except: # In case there is a None for a referenced field
                    pass 

        # Boring file handeling crap
        fd, fn = tempfile.mkstemp()
        os.close(fd)
        workbook.save(fn)
        fh = open(fn, 'rb')
        resp = fh.read()
        fh.close()
        response = HttpResponse(resp, mimetype='application/ms-excel')
        response['Content-Disposition'] = 'attachment; filename=%s.xls' % \
              (unicode(model_class._meta.verbose_name_plural),)
        return response

    return render_to_response('sis/export_to_xls.html', {
        'model_name': model_class._meta.verbose_name,
        'model': model_class._meta.app_label + &quot;:&quot; +  model_class._meta.module_name,
        'fields': model_fields,
        'get_variables': get_variables,
    }, RequestContext(request, {}),)
</pre>
<p>admin_export_xls does the bulk of the work and actually creates your xls reports. We use a similar syntax to django queries to find out exactly what data we are looking for. Something like &#8220;field__work_study:company__placement__company__id&#8221; where field just makes it a unique html name, followed by the app:model, then each object. So here we know we have a work_study.company model. We take our row in the queryset and say row.placement.company.id. Of course it uses a recursive getattr instead. </p>
<p>get_fields_for_model handles each ajax request for getting more fields. Now for the html I have two files<br />
export_to_xls.html</p>
<pre class="brush: xml; title: ; notranslate">
&lt;style type=&quot;text/css&quot;&gt;
  td, th {
    padding: 2px 0px 3px 3px;
    text-align: left;
  }
&lt;/style&gt;

&lt;script type=&quot;text/javascript&quot;&gt;
  $(document).ready(function()
  {
    $(&quot;#check_all&quot;).click(function()
    {
      var checked_status = this.checked;
      $(&quot;.check_field&quot;).each(function()
      {
        this.checked = checked_status;
      });
    });
  });

  function get_related(name){
    $.post(
      &quot;/sis/export_to_xls_related/?{{ get_variables }}&quot;,
      {get_related: &quot;True&quot;, rel_name: name},
      function(data){
        $(&quot;#field_&quot; + name).hide('fast');
        $(&quot;#field_&quot; + name).html(data);
        $(&quot;#field_&quot; + name).show('slow');
      }
    );
  }
&lt;/script&gt;   

&lt;h2&gt; Export {{ model_name }} &lt;/h2&gt;

&lt;form method=&quot;post&quot; action=&quot;/sis/export_to_xls/?{{ get_variables }}&quot;&gt;
  &lt;table&gt;
    &lt;tr&gt;
      &lt;th&gt;
        &lt;input type=&quot;checkbox&quot; id=&quot;check_all&quot; checked=&quot;checked&quot; /&gt;
      &lt;/th&gt;
      &lt;th&gt;
        Field
      &lt;/th&gt;
    &lt;/tr&gt;

    {% for field in fields %}
      &lt;tr&gt;
        &lt;td&gt;
          &lt;input type=&quot;checkbox&quot; class=&quot;check_field&quot; checked=&quot;checked&quot; name=&quot;field__{{ model }}__{{ field.name }}&quot;/&gt;
        &lt;/td&gt;
        &lt;td&gt;
          {{ field.verbose_name }}
        &lt;/td&gt;
      &lt;/tr&gt;
      {% if field.related %}{% if field.perm %}
        &lt;tr&gt;
          &lt;td&gt;&lt;/td&gt;
          &lt;td&gt;
            &lt;div id=&quot;field_{{ field.name }}&quot;&gt;
              &lt;a href=&quot;javascript:void(0)&quot; onclick=&quot;get_related('{{ field.name }}')&quot;&gt; &gt;&gt; Expand {{ field.verbose_name }}&lt;/a&gt;
            &lt;/div&gt;
          &lt;/td&gt;
        &lt;/tr&gt;
      {% endif %}{% endif %}
    {% endfor %}
  &lt;/table&gt;
  &lt;input type=&quot;submit&quot; name=&quot;xls&quot; onclick='$(&quot;#export_xls_form&quot;).overlay().close();' value=&quot;Submit&quot;/&gt;
&lt;/form&gt;
</pre>
<p>export_to_xls_related.html</p>
<pre class="brush: xml; title: ; notranslate">
&lt;table&gt;
  {% for field in fields %}
    &lt;tr&gt;
      &lt;td&gt;
        &lt;input type=&quot;checkbox&quot; class=&quot;check_field&quot; checked=&quot;checked&quot; name=&quot;field__{{ model }}__{{ previous_fields }}__{{ field.name }}&quot;/&gt;
      &lt;/td&gt;
      &lt;td&gt;
        {{ field.verbose_name }}
      &lt;/td&gt;
    &lt;/tr&gt;
    {% if field.related %}{% if field.perm %}
      &lt;tr&gt;
        &lt;td&gt;&lt;/td&gt;
        &lt;td&gt;
          &lt;div id=&quot;field_{{ previous_fields }}__{{ field.name }}&quot;&gt;
            &lt;a href=&quot;javascript:void(0)&quot; onclick=&quot;get_related('{{ previous_fields }}__{{ field.name }}')&quot;&gt; &gt;&gt; Expand {{ field.verbose_name }}&lt;/a&gt;
          &lt;/div&gt;
        &lt;/td&gt;
      &lt;/tr&gt;
    {% endif %}{% endif %}
  {% endfor %}
&lt;/table&gt;
</pre>
<p>I also use the modified change_list.html from last post. export_to_xls.html gets inserted into export_to_xls_related.html a potentially infinite amount of times which is all displayed on an overlay in the edit list view. When you click Submit you get an XLS file with all checked off fields. Hurray! </p>
<p>TODO: Many to Many fields! This will be much harder. The only way to show them is either by repeating rows or smashing them all in one cell. This is the peril of trying to fit a potentially infinite dimensional object into a 2D spreadsheet.</p>
]]></content:encoded>
			<wfw:commentRss>http://davidmburke.com/2011/05/19/django-admin-export-foreignkey-subfields-to-xls/feed/</wfw:commentRss>
		<slash:comments>8</slash:comments>
		</item>
		<item>
		<title>Django admin: better export to XLS</title>
		<link>http://davidmburke.com/2011/05/17/django-admin-better-export-to-xls/</link>
		<comments>http://davidmburke.com/2011/05/17/django-admin-better-export-to-xls/#comments</comments>
		<pubDate>Tue, 17 May 2011 17:15:27 +0000</pubDate>
		<dc:creator>david</dc:creator>
				<category><![CDATA[Uncategorized]]></category>
		<category><![CDATA[django]]></category>
		<category><![CDATA[export]]></category>
		<category><![CDATA[jquery]]></category>
		<category><![CDATA[ods]]></category>
		<category><![CDATA[spreadsheet]]></category>
		<category><![CDATA[xls]]></category>

		<guid isPermaLink="false">http://davidmburke.com/?p=240</guid>
		<description><![CDATA[The goal here is to make a slick gui for selecting exactly what the user wants to export from Django&#8217;s Change List view. It will be an global action, so lets start there. This adds a global action called Export &#8230; <a href="http://davidmburke.com/2011/05/17/django-admin-better-export-to-xls/">Continue reading <span class="meta-nav">&#8594;</span></a>]]></description>
			<content:encoded><![CDATA[<p><a href="http://davidmburke.com/files/2011/05/screenshot2.png"><img class="alignnone size-full wp-image-248" src="http://davidmburke.com/files/2011/05/screenshot2.png" alt="" width="629" height="422" /></a><br />
The goal here is to make a slick gui for selecting exactly what the user wants to export from Django&#8217;s Change List view. It will be an global action, so lets start there.</p>
<pre class="brush: python; title: ; notranslate">
def export_simple_selected_objects(modeladmin, request, queryset):
  selected_int = queryset.values_list('id', flat=True)
    selected = []
    for s in selected_int:
        selected.append(str(s))
  ct = ContentType.objects.get_for_model(queryset.model)
  return HttpResponseRedirect(&quot;/export_to_xls/?ct=%s&amp;ids=%s&quot; % (ct.pk, &quot;,&quot;.join(selected)))
export_simple_selected_objects.short_description = &quot;Export selected items to XLS&quot;
admin.site.add_action(export_simple_selected_objects)
</pre>
<p>This adds a global action called Export selected items to XLS. I went with xls instead of ods because xlwt is very mature and LibreOffice can open xls just fine. It&#8217;s limited by the max length of get variables because it just lists each id. See this <a href="https://code.djangoproject.com/ticket/15742">bug report.</a> Next is the view.</p>
<pre class="brush: python; title: ; notranslate">
import xlwt
def admin_export_xls(request):
    model_class = ContentType.objects.get(id=request.GET['ct']).model_class()
    queryset = model_class.objects.filter(pk__in=request.GET['ids'].split(','))
    model_fields = model_class._meta.fields

    if 'xls' in request.POST:
        workbook = xlwt.Workbook()
        worksheet = workbook.add_sheet(unicode(model_class._meta.verbose_name_plural))
        fields = []
        # Get selected fields from POST data
        for field in model_fields:
            if 'field__' + field.name in request.POST:
                fields.append(field)
        # Title
        for i, field in enumerate(fields):
            worksheet.write(0,i, field.verbose_name)
        for ri, row in enumerate(queryset): # For Row iterable, data row in the queryset
            for ci, field in enumerate(fields): # For Cell iterable, field, fields
                worksheet.write(ri+1, ci, unicode(getattr(row, field.name)))
        # Boring file handeling crap
        fd, fn = tempfile.mkstemp()
        os.close(fd)
        workbook.save(fn)
        fh = open(fn, 'rb')
        resp = fh.read()
        fh.close()
        response = HttpResponse(resp, mimetype='application/ms-excel')
        response['Content-Disposition'] = 'attachment; filename=%s.xls' % \
              (unicode(model_class._meta.verbose_name_plural),)
        return response

    return render_to_response('export_to_xls.html', {
        'model_name': model_class._meta.verbose_name,
        'fields': model_fields,
    }, RequestContext(request, {}),)
</pre>
<p>Remember to set up your URLs. Next is the HTML. Maybe something like this</p>
<pre class="brush: xml; title: ; notranslate">
&lt;script type=&quot;text/javascript&quot;&gt;
	$(document).ready(function()
	{
		$(&quot;#check_all&quot;).click(function()
		{
			var checked_status = this.checked;
			$(&quot;.check_field&quot;).each(function()
			{
				this.checked = checked_status;
			});
		});
	});
&lt;/script&gt;
&lt;h2&gt; Export {{ model_name }} &lt;/h2&gt;
    &lt;form method=&quot;post&quot; action=&quot;&quot;&gt;
		&lt;table&gt;
			&lt;tr&gt;
				&lt;th&gt;
					&lt;input type=&quot;checkbox&quot; id=&quot;check_all&quot; checked=&quot;checked&quot; /&gt;
				&lt;/th&gt;
				&lt;th&gt;
					Field
				&lt;/th&gt;
			&lt;/tr&gt;
			{% for field in fields %}
				&lt;tr&gt;
					&lt;td&gt;
						&lt;input type=&quot;checkbox&quot; class=&quot;check_field&quot; checked=&quot;checked&quot; name=&quot;field__{{ field.name }}&quot;/&gt;
					&lt;/td&gt;
					&lt;td&gt;
						{{ field.verbose_name }}
					&lt;/td&gt;
				&lt;/tr&gt;
			{% endfor %}
		&lt;/table&gt;
        &lt;input type=&quot;submit&quot; name=&quot;xls&quot; value=&quot;Submit&quot;/&gt;
    &lt;/form&gt;
</pre>
<p>The javascript just makes the check all box work. Note I use jquery, if you don&#8217;t you will need to rewrite it. Very simple but it works. Now users won&#8217;t have to delete unwanted columns from xls reports. Notice how the user is left on the export screen and not happily back to the edit list. Some ajax can solve this. I&#8217;m overriding the global change_list.html which actually isn&#8217;t ideal if you use any plugins that also override it. Here&#8217;s what I added.</p>
<pre class="brush: xml; title: ; notranslate">
&lt;script src=&quot;/static/js/jquery.tools.min.js&quot; type=&quot;text/javascript&quot;&gt;&lt;/script&gt;
&lt;script type=&quot;text/javascript&quot;&gt;
  $(document).ready(function()
  {
    $(&quot;.button&quot;).click(function()
    {
     if (
       $(&quot;option[value=export_simple_selected_objects]:selected&quot;).length
       &amp;&amp; $(&quot;input:checked&quot;).length
     ) {
        $.post(
          &quot;&quot;,
          $(&quot;#changelist-form&quot;).serialize(),
          function(data){
              $(&quot;#export_xls_form&quot;).html(data);
          }
        );
        $(&quot;#export_xls_form&quot;).overlay({
          top: 60
        });
        $(&quot;#export_xls_form&quot;).overlay().load();
        return false;
      }
    });
  });
&lt;/script&gt;
&lt;!-- Overlay, when you edit CSS, make sure this display is set to none initially --&gt;
  &lt;div class=&quot;modal&quot; id=&quot;export_xls_form&quot;&gt;&lt;button class=&quot;close&quot;&gt; Close &lt;/button&gt;&lt;/div&gt;
</pre>
<p>I use <a href="http://flowplayer.org/tools/overlay/index.html">jquery tools overlay</a> to make a nice overlay screen while keeping the user on the change list page. Basically I want a div to appear and then load some stuff from ajax. What&#8217;s cool is that I just post the data to &#8220;&#8221; so the regular Django admin functions work without editing them for AJAX. Well I did add to the submit button onclick=&#8217;$(&#8220;#export_xls_form&#8221;).overlay().close();&#8217; to close the window when submitting. Ok I&#8217;m a complete liar I also added get_variables = request.META['QUERY_STRING'] to the view as a cheap way to keep those GET variables. But hey it&#8217;s still works as a non ajax admin action and that&#8217;s cool.</p>
<p>In the screenshot I added a CSS3 shadow and rounded corners to make it look better.</p>
<p>What&#8217;s next? Well it would be nice if we could access foreign key fields. If this had some type of advanced search and saving mechanism, we&#8217;d have a full generic Django query builder. Hmm.</p>
]]></content:encoded>
			<wfw:commentRss>http://davidmburke.com/2011/05/17/django-admin-better-export-to-xls/feed/</wfw:commentRss>
		<slash:comments>3</slash:comments>
		</item>
	</channel>
</rss>

