changeset 71:e2868ad47a1e

For Django 1.4, using the new manage.py.
author Brian Neal <bgneal@gmail.com>
date Sat, 14 Apr 2012 16:40:29 -0500
parents f26cdda0ad8b
children e882e1c9f0c0
files articles/__init__.py articles/admin.py articles/management/__init__.py articles/management/commands/__init__.py articles/management/commands/import_old_articles.py articles/models.py articles/urls.py articles/views.py band/__init__.py band/admin.py band/management/__init__.py band/management/commands/__init__.py band/management/commands/import_old_band.py band/models.py band/urls.py band/views.py email_list/__init__.py email_list/admin.py email_list/forms.py email_list/management/__init__.py email_list/management/commands/__init__.py email_list/management/commands/import_old_email_list.py email_list/models.py email_list/tests/__init__.py email_list/tests/model_tests.py email_list/tests/view_tests.py email_list/urls.py email_list/views.py gigs/__init__.py gigs/admin.py gigs/models.py gigs/templatetags/__init__.py gigs/templatetags/gig_tags.py gigs/urls.py gigs/views.py madeira/apache/madeira.wsgi madeira/articles/__init__.py madeira/articles/admin.py madeira/articles/management/__init__.py madeira/articles/management/commands/__init__.py madeira/articles/management/commands/import_old_articles.py madeira/articles/models.py madeira/articles/urls.py madeira/articles/views.py madeira/band/__init__.py madeira/band/admin.py madeira/band/management/__init__.py madeira/band/management/commands/__init__.py madeira/band/management/commands/import_old_band.py madeira/band/models.py madeira/band/urls.py madeira/band/views.py madeira/email_list/__init__.py madeira/email_list/admin.py madeira/email_list/forms.py madeira/email_list/management/__init__.py madeira/email_list/management/commands/__init__.py madeira/email_list/management/commands/import_old_email_list.py madeira/email_list/models.py madeira/email_list/tests/__init__.py madeira/email_list/tests/model_tests.py madeira/email_list/tests/view_tests.py madeira/email_list/urls.py madeira/email_list/views.py madeira/gigs/__init__.py madeira/gigs/admin.py madeira/gigs/models.py madeira/gigs/templatetags/__init__.py madeira/gigs/templatetags/gig_tags.py madeira/gigs/urls.py madeira/gigs/views.py madeira/manage.py madeira/mp3/__init__.py madeira/mp3/admin.py madeira/mp3/management/__init__.py madeira/mp3/management/commands/__init__.py madeira/mp3/management/commands/import_old_mp3.py madeira/mp3/models.py madeira/mp3/urls.py madeira/news/__init__.py madeira/news/admin.py madeira/news/management/__init__.py madeira/news/management/commands/__init__.py madeira/news/management/commands/import_old_news.py madeira/news/models.py madeira/news/urls.py madeira/photologue/LICENSE.txt madeira/photologue/README.txt madeira/photologue/__init__.py madeira/photologue/admin.py madeira/photologue/locale/pl/LC_MESSAGES/django.mo madeira/photologue/locale/pl/LC_MESSAGES/django.po madeira/photologue/management/__init__.py madeira/photologue/management/commands/__init__.py madeira/photologue/management/commands/plcache.py madeira/photologue/management/commands/plcreatesize.py madeira/photologue/management/commands/plflush.py madeira/photologue/management/commands/plinit.py madeira/photologue/models.py madeira/photologue/res/sample.jpg madeira/photologue/res/test_landscape.jpg madeira/photologue/res/test_portrait.jpg madeira/photologue/res/test_square.jpg madeira/photologue/templates/photologue/gallery_archive.html madeira/photologue/templates/photologue/gallery_archive_day.html madeira/photologue/templates/photologue/gallery_archive_month.html madeira/photologue/templates/photologue/gallery_archive_year.html madeira/photologue/templates/photologue/gallery_detail.html madeira/photologue/templates/photologue/gallery_list.html madeira/photologue/templates/photologue/photo_archive.html madeira/photologue/templates/photologue/photo_archive_day.html madeira/photologue/templates/photologue/photo_archive_month.html madeira/photologue/templates/photologue/photo_archive_year.html madeira/photologue/templates/photologue/photo_detail.html madeira/photologue/templates/photologue/photo_list.html madeira/photologue/templates/photologue/root.html madeira/photologue/tests.py madeira/photologue/urls.py madeira/photologue/utils/EXIF.py madeira/photologue/utils/__init__.py madeira/photologue/utils/reflection.py madeira/photologue/utils/watermark.py madeira/pl-admin.py madeira/settings/base.py madeira/settings/local.py madeira/settings/production.py madeira/settings/test.py madeira/videos/__init__.py madeira/videos/admin.py madeira/videos/management/__init__.py madeira/videos/management/commands/__init__.py madeira/videos/management/commands/import_old_videos.py madeira/videos/models.py madeira/videos/static/js/videos/videos_admin.js madeira/videos/urls.py manage.py mp3/__init__.py mp3/admin.py mp3/management/__init__.py mp3/management/commands/__init__.py mp3/management/commands/import_old_mp3.py mp3/models.py mp3/urls.py news/__init__.py news/admin.py news/management/__init__.py news/management/commands/__init__.py news/management/commands/import_old_news.py news/models.py news/urls.py photologue/LICENSE.txt photologue/README.txt photologue/__init__.py photologue/admin.py photologue/locale/pl/LC_MESSAGES/django.mo photologue/locale/pl/LC_MESSAGES/django.po photologue/management/__init__.py photologue/management/commands/__init__.py photologue/management/commands/plcache.py photologue/management/commands/plcreatesize.py photologue/management/commands/plflush.py photologue/management/commands/plinit.py photologue/models.py photologue/res/sample.jpg photologue/res/test_landscape.jpg photologue/res/test_portrait.jpg photologue/res/test_square.jpg photologue/templates/photologue/gallery_archive.html photologue/templates/photologue/gallery_archive_day.html photologue/templates/photologue/gallery_archive_month.html photologue/templates/photologue/gallery_archive_year.html photologue/templates/photologue/gallery_detail.html photologue/templates/photologue/gallery_list.html photologue/templates/photologue/photo_archive.html photologue/templates/photologue/photo_archive_day.html photologue/templates/photologue/photo_archive_month.html photologue/templates/photologue/photo_archive_year.html photologue/templates/photologue/photo_detail.html photologue/templates/photologue/photo_list.html photologue/templates/photologue/root.html photologue/tests.py photologue/urls.py photologue/utils/EXIF.py photologue/utils/__init__.py photologue/utils/reflection.py photologue/utils/watermark.py pl-admin.py videos/__init__.py videos/admin.py videos/management/__init__.py videos/management/commands/__init__.py videos/management/commands/import_old_videos.py videos/models.py videos/static/js/videos/videos_admin.js videos/urls.py
diffstat 151 files changed, 5730 insertions(+), 5732 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/articles/admin.py	Sat Apr 14 16:40:29 2012 -0500
@@ -0,0 +1,21 @@
+"""
+Automatic admin definitions for the articles application.
+
+"""
+from django.contrib import admin
+from django.conf import settings
+
+from articles.models import Article
+
+
+class ArticleAdmin(admin.ModelAdmin):
+    save_on_top = True
+    list_filter = ['date']
+    list_display = ['title', 'date']
+    search_fields = ['text', 'title']
+
+    class Media:
+        js = settings.THIRD_PARTY_JS['tiny_mce']
+
+
+admin.site.register(Article, ArticleAdmin)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/articles/management/commands/import_old_articles.py	Sat Apr 14 16:40:29 2012 -0500
@@ -0,0 +1,59 @@
+"""
+import_old_articles.py - For importing articles from the older version of this
+website.
+
+"""
+import datetime
+
+from django.conf import settings
+from django.core.management.base import LabelCommand
+from django.utils import simplejson as json
+from django.utils.html import linebreaks
+import textile
+
+from articles.models import Article
+
+
+class Command(LabelCommand):
+    args = '<filename filename ...>'
+    help = 'Imports older articles in JSON format'
+
+    def handle_label(self, filename, **options):
+        """
+        Process the file of older articles in JSON. Convert to the new model
+        scheme.
+
+        """
+        with open(filename, 'rb') as f:
+            items = json.load(f)
+
+        for item in items:
+            if item['model'] == 'band.article':
+                self.process_item(item)
+
+    def process_item(self, item):
+
+        fields = item['fields']
+
+        content = fields['text'].strip()
+        if fields['markup_enabled']:
+            text = textile.textile(content, encoding='utf-8', output='utf-8')
+        else:
+            text = linebreaks(fields['text'])
+
+        source = linebreaks(fields['source'].strip())
+
+        pdf = fields['pdf'].strip()
+        if pdf:
+            pdf = u"%s%s" % (settings.MEDIA_URL, pdf.replace('\\', '/'))
+
+        article = Article(
+                id=item['pk'],
+                title=fields['title'].strip(),
+                date=datetime.datetime.strptime(fields['date'], '%Y-%m-%d'),
+                text=text,
+                source=source,
+                url=fields['url'].strip(),
+                pdf=pdf)
+
+        article.save()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/articles/models.py	Sat Apr 14 16:40:29 2012 -0500
@@ -0,0 +1,28 @@
+"""
+Models for the articles application.
+
+"""
+from django.db import models
+
+
+class Article(models.Model):
+    title = models.CharField(max_length=64)
+    date = models.DateTimeField(db_index=True)
+    text = models.TextField()
+    source = models.TextField(help_text="Enter the source/author for the "
+            "article, copyright info, etc; it will appear under the article.")
+    url = models.URLField(blank=True,
+            help_text = 'Link to original article; optional')
+    pdf = models.FileField(upload_to = 'pdf/articles/%Y/%m/%d/', blank=True,
+            help_text="If you want to make the original article available as "
+            "a PDF download, you may upload it here.")
+
+    def __unicode__(self):
+        return self.title
+
+    class Meta:
+        ordering = ['-date']
+
+    @models.permalink
+    def get_absolute_url(self):
+        return ('articles-item', [], {'pk': str(self.id)})
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/articles/urls.py	Sat Apr 14 16:40:29 2012 -0500
@@ -0,0 +1,20 @@
+"""
+Urls for the articles application.
+
+"""
+from django.conf.urls import patterns, url
+from django.views.generic import DetailView, ListView
+
+from articles.models import Article
+
+
+urlpatterns = patterns('',
+   url(r'^$',
+       ListView.as_view(
+           model=Article,
+           context_object_name='article_list'),
+       name='articles-index'),
+   url(r'^(?P<pk>\d+)/$',
+       DetailView.as_view(model=Article, context_object_name='article'),
+       name='articles-item')
+)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/articles/views.py	Sat Apr 14 16:40:29 2012 -0500
@@ -0,0 +1,1 @@
+# Create your views here.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/band/admin.py	Sat Apr 14 16:40:29 2012 -0500
@@ -0,0 +1,83 @@
+"""
+Automatic admin definitions for the band models.
+
+"""
+from django.contrib import admin
+
+from band.models import Album
+from band.models import AlbumMerchant
+from band.models import AlbumTrack
+from band.models import Gear
+from band.models import LabelRelease
+from band.models import Member
+from band.models import Merchandise
+from band.models import RecordLabel
+
+
+class GearInline(admin.TabularInline):
+    model = Gear
+
+
+class GearAdmin(admin.ModelAdmin):
+    list_display = ['item', 'member']
+    list_filter = ['member']
+
+
+class MemberAdmin(admin.ModelAdmin):
+    list_display = ['name', 'instrument', 'is_active']
+    inlines = [GearInline]
+
+
+class AlbumTrackInline(admin.TabularInline):
+    model = AlbumTrack
+
+
+class AlbumTrackAdmin(admin.ModelAdmin):
+    list_display = ['track_name', 'album']
+    list_filter = ['album']
+
+
+class LabelReleaseInline(admin.TabularInline):
+    model = LabelRelease
+
+
+class LabelReleaseAdmin(admin.ModelAdmin):
+    list_display = ['catalog_number', 'album', 'record_label', 'release_date']
+    list_filter = ['record_label', 'album']
+
+
+class RecordLabelAdmin(admin.ModelAdmin):
+    inlines = [LabelReleaseInline]
+
+
+class AlbumMerchantInline(admin.TabularInline):
+    model = AlbumMerchant
+
+
+class AlbumMerchantAdmin(admin.ModelAdmin):
+    list_display = ['name', 'album']
+    list_filter = ['album']
+
+
+class AlbumAdmin(admin.ModelAdmin):
+    save_on_top = True
+    inlines = [
+        AlbumTrackInline,
+        LabelReleaseInline,
+        AlbumMerchantInline,
+    ]
+
+
+class MerchandiseAdmin(admin.ModelAdmin):
+    list_display = ['name', 'price', 'in_stock']
+    list_filter = ['in_stock']
+
+
+admin.site.register(Gear, GearAdmin)
+admin.site.register(Member, MemberAdmin)
+admin.site.register(AlbumTrack, AlbumTrackAdmin)
+admin.site.register(LabelRelease, LabelReleaseAdmin)
+admin.site.register(RecordLabel, RecordLabelAdmin)
+admin.site.register(AlbumMerchant, AlbumMerchantAdmin)
+admin.site.register(Album, AlbumAdmin)
+admin.site.register(Merchandise, MerchandiseAdmin)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/band/management/commands/import_old_band.py	Sat Apr 14 16:40:29 2012 -0500
@@ -0,0 +1,149 @@
+"""
+import_old_band.py - For importing band models from the older version of this
+website.
+
+"""
+import datetime
+
+from django.core.management.base import LabelCommand
+from django.utils import simplejson as json
+from django.utils.html import linebreaks
+from photologue.models import Photo
+
+from band.models import (Member, Gear, RecordLabel, Album, AlbumTrack,
+        LabelRelease, AlbumMerchant, Merchandise)
+
+
+class Command(LabelCommand):
+    args = '<filename filename ...>'
+    help = 'Imports older band model objects in JSON format'
+
+    members = {}
+    albums = {}
+    labels = {}
+
+    def handle_label(self, filename, **options):
+        """
+        Process the file of older model objects in JSON. Convert to the new model
+        scheme.
+
+        """
+        with open(filename, 'rb') as f:
+            items = json.load(f)
+
+        for item in items:
+            if item['model'] == 'band.member':
+                self.process_member(item)
+            elif item['model'] == 'band.gear':
+                self.process_gear(item)
+            elif item['model'] == 'band.record_label':
+                self.process_record_label(item)
+            elif item['model'] == 'band.album':
+                self.process_album(item)
+            elif item['model'] == 'band.album_track':
+                self.process_track(item)
+            elif item['model'] == 'band.label_release':
+                self.process_release(item)
+            elif item['model'] == 'band.album_merchant':
+                self.process_merchant(item)
+            elif item['model'] == 'band.merchandise':
+                self.process_merch(item)
+
+    def process_member(self, item):
+
+        fields = item['fields']
+
+        start_date = datetime.datetime.strptime(fields['start_date'], '%Y-%m-%d')
+
+        if fields['end_date'] == u'1985-01-01':
+            end_date = None
+        else:
+            end_date = datetime.datetime.strptime(fields['end_date'], '%Y-%m-%d')
+
+        member = Member(id=item['pk'],
+                name=fields['name'],
+                nickname=fields['nickname'],
+                instrument=fields['instrument'],
+                bio=fields['bio'],
+                photo=fields['photo'],
+                order=int(fields['order']),
+                is_active=fields['is_active'],
+                start_date=start_date,
+                end_date=end_date,
+                email=fields['email'])
+        member.save()
+        self.members[member.id] = member
+
+    def process_gear(self, item):
+        fields = item['fields']
+
+        gear = Gear(id=item['pk'],
+                member=self.members[int(fields['member'])],
+                item=fields['item'])
+        gear.save()
+
+    def process_record_label(self, item):
+        fields = item['fields']
+
+        label = RecordLabel(id=item['pk'],
+                url=fields['url'],
+                name=fields['name'])
+        label.save()
+        self.labels[label.id] = label
+        
+    def process_album(self, item):
+        fields = item['fields']
+
+        photo = Photo.objects.get(id=fields['photo'])
+
+        album = Album(id=item['pk'],
+                title=fields['title'],
+                photo=photo,
+                desc=fields['desc'])
+        album.save()
+        self.albums[album.id] = album
+
+    def process_track(self, item):
+        fields = item['fields']
+
+        track = AlbumTrack(id=item['pk'],
+                album=self.albums[int(fields['album'])],
+                track_number=int(fields['track_number']),
+                track_name=fields['track_name'])
+        track.save()
+
+    def process_release(self, item):
+        fields = item['fields']
+
+        release = LabelRelease(id=item['pk'],
+                record_label=self.labels[int(fields['record_label'])],
+                album=self.albums[int(fields['album'])],
+                catalog_number=fields['catalog_number'],
+                release_date=datetime.datetime.strptime(
+                    fields['release_date'], '%Y-%m-%d'))
+
+        release.save()
+
+    def process_merchant(self, item):
+        fields = item['fields']
+
+        merchant = AlbumMerchant(id=item['pk'],
+                album=self.albums[int(fields['album'])],
+                url=fields['url'],
+                name=fields['name'])
+
+        merchant.save()
+
+    def process_merch(self, item):
+        fields = item['fields']
+
+        photo = Photo.objects.get(id=fields['photo'])
+
+        merch = Merchandise(id=item['pk'],
+                name=fields['name'],
+                desc=fields['desc'],
+                price=fields['price'],
+                in_stock=fields['in_stock'],
+                photo=photo)
+
+        merch.save()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/band/models.py	Sat Apr 14 16:40:29 2012 -0500
@@ -0,0 +1,111 @@
+"""
+Models for the band application.
+
+"""
+from django.db import models
+
+from photologue.models import Photo
+
+
+class Member(models.Model):
+    name = models.CharField(max_length=50)
+    nickname = models.CharField(max_length=50, blank=True)
+    instrument = models.CharField(max_length=255)
+    bio = models.TextField(blank=True)
+    photo = models.ImageField(upload_to='images/bio/', blank=True)
+    order = models.SmallIntegerField(
+        help_text = "Controls order of display; lower numbers displayed first")
+    is_active = models.BooleanField()
+    start_date = models.DateField()
+    end_date = models.DateField(blank=True,
+        null=True,
+        help_text="Only used if the member is not active")
+    email = models.EmailField()
+
+    def __unicode__(self):
+        return self.name
+
+    class Meta:
+        ordering = ['-is_active', 'name']
+
+
+class Gear(models.Model):
+    member = models.ForeignKey(Member)
+    item = models.CharField(max_length=255)
+
+    def __unicode__(self):
+        return self.item
+
+    class Meta:
+        verbose_name_plural = 'Gear List'
+
+
+class RecordLabel(models.Model):
+    name = models.CharField(max_length=64)
+    url = models.URLField(max_length=200)
+
+    def __unicode__(self):
+        return self.name
+
+
+class Album(models.Model):
+    title = models.CharField(max_length=64)
+    photo = models.ForeignKey(Photo)
+    desc = models.TextField(blank=True)
+
+    def __unicode__(self):
+        return self.title
+
+
+class AlbumTrack(models.Model):
+    album = models.ForeignKey(Album, related_name='tracks')
+    track_number = models.SmallIntegerField()
+    track_name = models.CharField(max_length=64)
+
+    def __unicode__(self):
+        return self.track_name
+
+    class Meta:
+        verbose_name = 'Album Track'
+        ordering = ['album', 'track_number']
+
+
+class LabelRelease(models.Model):
+    record_label = models.ForeignKey(RecordLabel)
+    album = models.ForeignKey(Album, related_name='labels')
+    catalog_number = models.CharField(max_length=32)
+    release_date = models.DateField()
+
+    def __unicode__(self):
+        return u'%s %s %s' % (self.record_label.name, self.album.title,
+                self.catalog_number)
+
+    class Meta:
+        verbose_name = 'Label Release'
+
+
+class AlbumMerchant(models.Model):
+    album = models.ForeignKey(Album, related_name='merchants')
+    name = models.CharField(max_length=64)
+    url = models.URLField(max_length=200)
+
+    def __unicode__(self):
+        return u'%s (%s)' % (self.name, self.album.title)
+
+    class Meta:
+        verbose_name = 'Album Merchant'
+        ordering = ['name']
+
+
+class Merchandise(models.Model):
+    name = models.CharField(max_length=64)
+    desc = models.TextField()
+    price = models.DecimalField(max_digits=5, decimal_places=2)
+    in_stock = models.BooleanField()
+    photo = models.ForeignKey(Photo)
+
+    def __unicode__(self):
+        return self.name
+
+    class Meta:
+        verbose_name_plural = "Merchandise"
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/band/urls.py	Sat Apr 14 16:40:29 2012 -0500
@@ -0,0 +1,13 @@
+"""
+Urls for the band application.
+
+"""
+from django.conf.urls import patterns, url
+
+urlpatterns = patterns('band.views',
+   url(r'^bio/$', 'bio', name='band-bio'),
+   url(r'^buy/$', 'buy', name='band-buy'),
+   url(r'^contact/$', 'contact', name='band-contact'),
+   url(r'^photos/$', 'photos_index', name='band-photo_index'),
+   url(r'^photos/(\d+)$', 'photo_detail', name='band-photo_detail'),
+)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/band/views.py	Sat Apr 14 16:40:29 2012 -0500
@@ -0,0 +1,57 @@
+"""
+Views for the band application.
+
+"""
+import random
+
+from django.shortcuts import render
+from django.shortcuts import get_object_or_404
+from photologue.models import Gallery
+from photologue.models import Photo
+
+from band.models import Member, Merchandise, Album
+
+
+def bio(request):
+    members = Member.objects.exclude(is_active=0)
+    return render(request, 'band/bio.html', {'members': members})
+
+
+def photos_index(request):
+    galleries = Gallery.objects.values('title', 'id').order_by('-id')
+
+    photo_ids = Photo.objects.filter(is_public=True).values_list('id',
+            flat=True)
+    photo_ids = random.sample(photo_ids, 4)
+    random_photos = Photo.objects.filter(id__in=photo_ids)
+
+    return render(request, 'band/photos.html', {
+        'galleries': galleries,
+        'random_photos': random_photos,
+    })
+
+
+def photo_detail(request, id):
+    gallery = get_object_or_404(Gallery, pk=id)
+    photos = gallery.photos.order_by('id')
+
+    return render(request, 'band/photo_detail.html', {
+        'gallery': gallery,
+        'photos': photos,
+    })
+
+
+def buy(request):
+    albums = Album.objects.all().order_by('-id')
+    merchandise = Merchandise.objects.all().order_by('-id')
+    return render(request, 'band/buy.html', {
+        'albums': albums, 
+        'merchandise': merchandise, 
+    })
+
+
+def contact(request):
+    band = Member.objects.exclude(is_active=0).order_by('order')
+    return render(request, 'band/contact.html', {
+        'band': band,
+    })
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/email_list/admin.py	Sat Apr 14 16:40:29 2012 -0500
@@ -0,0 +1,41 @@
+"""
+Automatic admin definitions for the email_list application.
+
+"""
+from django.contrib import admin
+from django.conf.urls import patterns, url
+from django.shortcuts import render, redirect
+
+from email_list.models import Subscriber
+from email_list.forms import AdminEmailForm
+
+
+class SubscriberAdmin(admin.ModelAdmin):
+    list_display = ['__unicode__', 'location', 'status']
+    list_filter = ['status']
+    search_fields = ['name', 'email']
+
+    def get_urls(self):
+        urls = super(SubscriberAdmin, self).get_urls()
+        my_urls = patterns('',
+            url(r'^send_mail/$',
+                self.admin_site.admin_view(self.send_mail),
+                name='email_list-admin_mail'),
+        )
+        return my_urls + urls
+
+    def send_mail(self, request):
+        if request.method == 'POST':
+            form = AdminEmailForm(request.POST)
+            if form.is_valid():
+                n = form.save()
+                msg = '%d mailing list emails sent' % n
+                self.message_user(request, msg)
+                return redirect('admin:index')
+        else:
+            form = AdminEmailForm()
+
+        return render(request, 'email_list/admin_mail.html', {'form': form})
+
+
+admin.site.register(Subscriber, SubscriberAdmin)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/email_list/forms.py	Sat Apr 14 16:40:29 2012 -0500
@@ -0,0 +1,155 @@
+"""
+Forms for the email_list application.
+
+"""
+from django import forms
+from django.conf import settings
+from django.core.urlresolvers import reverse
+from django.core.mail import send_mail, send_mass_mail
+from django.template.loader import render_to_string
+
+from email_list.models import Subscriber
+
+
+SUBSCRIBE_OPTS = [('sub', 'Subscribe'), ('unsub', 'Unsubscribe')]
+
+ALREADY_SUBSCRIBED = "This email address is already subscribed."
+NOT_SUBSCRIBED = "This email address is not on our list."
+
+
+class SubscriberForm(forms.Form):
+    name = forms.CharField(max_length=64, required=False)
+    email = forms.EmailField()
+    location = forms.CharField(max_length=64, required=False)
+    option = forms.ChoiceField(choices=SUBSCRIBE_OPTS)
+
+    def clean(self):
+        """
+        This method ensures the appropriate action can be carried out and raises
+        a validation error if not.
+
+        """
+        email = self.cleaned_data['email']
+
+        if self.cleaned_data['option'] == 'sub':
+            # is the user already subscribed (active)?
+            try:
+                subscriber = Subscriber.objects.get(email=email)
+            except Subscriber.DoesNotExist:
+                subscriber = Subscriber(email=email,
+                        name=self.cleaned_data['name'],
+                        location=self.cleaned_data['location'])
+            else:
+                if subscriber.is_active():
+                    raise forms.ValidationError(ALREADY_SUBSCRIBED)
+        else:
+            # is the user already unsubscribed or not subscribed?
+            try:
+                subscriber = Subscriber.objects.get(email=email)
+            except Subscriber.DoesNotExist:
+                raise forms.ValidationError(NOT_SUBSCRIBED)
+
+        # save the subscriber away for a future process() call
+        self.subscriber = subscriber
+
+        return self.cleaned_data
+
+    def is_subscribe(self):
+        """
+        This function can be called after an is_valid() call to determine if the
+        request was for a subscribe or unsubscribe.
+
+        """
+        return self.cleaned_data['option'] == 'sub'
+
+    def process(self):
+        """
+        Call this function if is_valid() returns True. It carries out the user's
+        subscription request. 
+
+        """
+        if self.is_subscribe():
+            self.subscriber.set_pending()
+        else:
+            self.subscriber.set_leaving()
+
+        self.subscriber.save()
+        send_email(self.subscriber)
+
+
+class AdminEmailForm(forms.Form):
+    subject = forms.CharField(max_length=255, required=True, label='Subject:',
+        widget=forms.TextInput(attrs={'class': 'vTextField required',
+            'size': '120'}))
+    message = forms.CharField(label='Message:',
+        widget=forms.Textarea(attrs={'class': 'vLargeTextField required'}))
+
+    def __init__(self, *args, **kwargs):
+        initial = kwargs.pop('initial', {})
+        if 'subject' not in initial:
+            initial['subject'] = '[%s] ' % settings.BAND_CONFIG['BAND_NAME']
+            kwargs['initial'] = initial
+
+        super(AdminEmailForm, self).__init__(*args, **kwargs)
+
+    def save(self):
+        """
+        Call this function if is_valid() to send the mass email.
+        Returns the number of mails sent.
+
+        """
+        subject = self.cleaned_data['subject']
+        message = self.cleaned_data['message']
+        return send_mail_to_subscribers(subject, message)
+
+
+def send_email(subscriber):
+    """
+    This function sends out the appropriate email for the given subscriber.
+
+    """
+    config = settings.BAND_CONFIG
+    band = config['BAND_NAME']
+    from_email = config['BAND_EMAIL']
+
+    url = "http://%s%s" % (config['BAND_DOMAIN'],
+            reverse('email_list-confirm', kwargs={'key': subscriber.key}))
+
+    if subscriber.is_pending():
+        email_template = 'email_list/email_subscribe.txt'
+    else:
+        email_template = 'email_list/email_unsubscribe.txt'
+
+    msg = render_to_string(email_template, {
+        'band': band, 
+        'url': url, 
+        'band_domain': config['BAND_DOMAIN'],
+        })
+
+    subject = "[%s] Mailing List Confirmation" % band
+
+    send_mail(subject, msg, from_email, [subscriber.email])
+
+
+def send_mail_to_subscribers(subject, message):
+    """
+    Send an email to each mailing list subscriber with the given subject and
+    message.
+    Returns the number of messages sent.
+
+    """
+    config = settings.BAND_CONFIG
+    unsubscribe_url = "http://%s%s" % (config['BAND_DOMAIN'],
+              reverse('email_list-main'))
+
+    msg = render_to_string('email_list/mailing_list.txt', {
+        'band': config['BAND_NAME'],
+        'unsubscribe_url': unsubscribe_url,
+        'message': message,
+        })
+
+    mail_data = [(subject, msg, config['BAND_EMAIL'], [subscriber]) for
+            subscriber in Subscriber.objects.values_list('email', flat=True)]
+
+    send_mass_mail(mail_data)
+    return len(mail_data)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/email_list/management/commands/import_old_email_list.py	Sat Apr 14 16:40:29 2012 -0500
@@ -0,0 +1,51 @@
+"""
+import_old_email_list.py - For importing the email list from the older version
+of this website.
+
+"""
+import datetime
+
+from django.core.management.base import LabelCommand
+from django.utils import simplejson as json
+
+from email_list.models import Subscriber
+
+
+class Command(LabelCommand):
+    args = '<filename filename ...>'
+    help = 'Imports older email list data in JSON format'
+
+    def handle_label(self, filename, **options):
+        """
+        Process the file of older email list data in JSON. Convert to the new
+        model scheme.
+
+        """
+        with open(filename, 'rb') as f:
+            items = json.load(f)
+
+        for item in items:
+            if item['model'] == 'band.fan':
+                self.process_item(item)
+
+    def process_item(self, item):
+
+        fields = item['fields']
+
+        # Only process 'active' subscribers because the new application
+        # has changed the key generation scheme.
+
+        if fields['status'] != 'A':
+            return
+
+        subscriber = Subscriber(
+                id=item['pk'],
+                name=fields['name'].strip(),
+                email=fields['email'].strip(),
+                location=fields['location'].strip(),
+                status='A',
+                key='',
+                status_date=datetime.datetime.strptime(
+                    fields['status_date'], '%Y-%m-%d'))
+
+        subscriber.save()                    
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/email_list/models.py	Sat Apr 14 16:40:29 2012 -0500
@@ -0,0 +1,69 @@
+"""
+Models for the email_list application.
+
+"""
+import base64
+import datetime
+import hashlib
+
+from django.db import models
+from django.conf import settings
+
+
+class Subscriber(models.Model):
+    status_codes = [('P', 'Pending'), ('A', 'Active'), ('L', 'Leaving')]
+    key_length = 28
+
+    name = models.CharField(max_length=64, blank=True)
+    email = models.EmailField(db_index=True)
+    location = models.CharField(max_length=64, blank=True)
+    status = models.CharField(max_length=1, choices=status_codes, default='A')
+    key = models.CharField(max_length=key_length, editable=False, blank=True,
+            db_index=True)
+    status_date = models.DateTimeField(editable=False)
+
+    class Meta:
+        ordering = ['name', 'email']
+
+    def __unicode__(self):
+        if self.name:
+            return u'%s <%s>' % (self.name, self.email)
+        return self.email
+
+    def save(self, *args, **kwargs):
+        if not self.pk and not self.status_date:
+            self.status_date = datetime.datetime.now()
+
+        super(Subscriber, self).save(*args, **kwargs)
+
+    def set_pending(self):
+        self.status = 'P'
+        self.status_date = datetime.datetime.now()
+        self.gen_key()
+
+    def set_active(self):
+        self.status = 'A'
+        self.status_date = datetime.datetime.now()
+        self.key = ''
+
+    def set_leaving(self):
+        self.status = 'L'
+        self.status_date = datetime.datetime.now()
+        self.gen_key()
+
+    def is_pending(self):
+        return self.status == 'P'
+
+    def is_leaving(self):
+        return self.status == 'L'
+
+    def is_active(self):
+        return self.status == 'A'
+
+    def gen_key(self):
+        source = (settings.SECRET_KEY + self.email + self.name + self.location +
+                self.status + self.status_date.isoformat())
+
+        sha = hashlib.sha1()
+        sha.update(source)
+        self.key = base64.urlsafe_b64encode(sha.digest())
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/email_list/tests/__init__.py	Sat Apr 14 16:40:29 2012 -0500
@@ -0,0 +1,2 @@
+from model_tests import *
+from view_tests import *
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/email_list/tests/model_tests.py	Sat Apr 14 16:40:29 2012 -0500
@@ -0,0 +1,76 @@
+"""
+Model tests for the email_list application.
+
+"""
+import datetime
+
+from django.test import TestCase
+
+from email_list.models import Subscriber
+
+
+class SubscriberTestCase(TestCase):
+
+    def test_auto_save(self):
+
+        sub = Subscriber(name='', location='', email='test@example.com')
+        sub.save()
+
+        now = datetime.datetime.now()
+        self.assertTrue(now - sub.status_date < datetime.timedelta(seconds=2))
+
+        self.assertTrue(sub.status == 'A')
+        self.assertTrue(sub.is_active())
+        self.failIf(sub.is_pending())
+        self.failIf(sub.is_leaving())
+
+    def test_set_pending(self):
+
+        sub = Subscriber(name='', location='', email='test@example.com')
+        sub.set_pending()
+
+        now = datetime.datetime.now()
+        self.assertTrue(now - sub.status_date < datetime.timedelta(seconds=2))
+
+        self.assertTrue(sub.status == 'P')
+        self.failIf(sub.is_active())
+        self.assertTrue(sub.is_pending())
+        self.failIf(sub.is_leaving())
+
+        self.assertTrue(len(sub.key) == sub.key_length)
+
+    def test_set_active(self):
+
+        sub = Subscriber(name='', location='', email='test@example.com')
+        sub.set_active()
+
+        now = datetime.datetime.now()
+        self.assertTrue(now - sub.status_date < datetime.timedelta(seconds=2))
+
+        self.assertTrue(sub.status == 'A')
+        self.assertTrue(sub.is_active())
+        self.failIf(sub.is_pending())
+        self.failIf(sub.is_leaving())
+
+    def test_set_leaving(self):
+
+        sub = Subscriber(name='', location='', email='test@example.com')
+        sub.set_leaving()
+
+        now = datetime.datetime.now()
+        self.assertTrue(now - sub.status_date < datetime.timedelta(seconds=2))
+
+        self.assertTrue(sub.status == 'L')
+        self.failIf(sub.is_active())
+        self.failIf(sub.is_pending())
+        self.assertTrue(sub.is_leaving())
+
+        self.assertTrue(len(sub.key) == sub.key_length)
+
+    def test_gen_key(self):
+
+        sub = Subscriber(name='', location='', email='test@example.com')
+        sub.status_date = datetime.datetime.now()
+        sub.gen_key()
+        self.assertTrue(len(sub.key) == sub.key_length)
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/email_list/tests/view_tests.py	Sat Apr 14 16:40:29 2012 -0500
@@ -0,0 +1,213 @@
+"""
+View tests for the email_list application.
+
+"""
+from django.test import TestCase
+from django.core.urlresolvers import reverse
+from django.core import mail
+
+from email_list.models import Subscriber
+import email_list.forms
+
+
+SUB_PARAMS = {
+    'name': 'John Doe',
+    'email': 'j.doe@example.com',
+    'location': 'USA',
+    'option': 'sub'
+}
+
+UNSUB_PARAMS = {
+    'name': '',
+    'email': SUB_PARAMS['email'],
+    'location': '',
+    'option': 'unsub'
+}
+
+class EmailListTestCase(TestCase):
+
+    def test_already_subscribed(self):
+        """
+        Test that subscribing twice fails with a form error.
+
+        """
+        sub = Subscriber(email=SUB_PARAMS['email'])
+        sub.set_active()
+        sub.save()
+
+        # Post a subscribe request
+
+        response = self.client.post(
+                reverse('email_list-main'),
+                SUB_PARAMS,
+                follow=True)
+
+        self.assertTrue(response.status_code, 200)
+        self.assertEqual(len(response.redirect_chain), 0)
+        self.assertContains(response, email_list.forms.ALREADY_SUBSCRIBED)
+
+    def test_not_subscribed(self):
+        """
+        Test that unsubscribing without being subscribed fails with a form error.
+
+        """
+        # Post a unsubscribe request
+
+        response = self.client.post(
+                reverse('email_list-main'),
+                UNSUB_PARAMS,
+                follow=True)
+
+        self.assertTrue(response.status_code, 200)
+        self.assertEqual(len(response.redirect_chain), 0)
+        self.assertContains(response, email_list.forms.NOT_SUBSCRIBED)
+
+    def test_normal_cycle(self):
+        """
+        Test a normal subscribe and unsubscribe cycle.
+
+        """
+        self.do_test_subscribe()
+        self.do_test_unsubscribe()
+
+    def test_subscribe_if_pending(self):
+        """
+        Ensure you can subscribe if you are already pending.
+
+        """
+        sub = Subscriber(email=SUB_PARAMS['email'])
+        sub.set_pending()
+        sub.save()
+        self.do_test_subscribe()
+
+    def test_subscribe_if_leaving(self):
+        """
+        Ensure you can subscribe if you are leaving.
+
+        """
+        sub = Subscriber(email=SUB_PARAMS['email'])
+        sub.set_leaving()
+        sub.save()
+        self.do_test_subscribe()
+
+    def test_unsubscribe_if_leaving(self):
+        """
+        Ensure you can unsubscribe if you are already leaving.
+
+        """
+        sub = Subscriber(email=SUB_PARAMS['email'])
+        sub.set_leaving()
+        sub.save()
+        self.do_test_unsubscribe()
+
+    def do_test_subscribe(self):
+        # Get the form view
+        response = self.client.get(reverse('email_list-main'))
+        self.assertEqual(response.status_code, 200)
+        self.assertTemplateUsed(response, 'email_list/subscribe_form.html')
+
+        # Post a subscribe request
+
+        response = self.client.post(
+                reverse('email_list-main'),
+                SUB_PARAMS,
+                follow=True)
+
+        self.assertTrue(response.status_code, 200)
+        self.assertEqual(len(response.redirect_chain), 1)
+        self.assertEqual(response.redirect_chain[0][0],
+                'http://testserver' + reverse('email_list-request_subscribe'))
+        self.assertEqual(response.redirect_chain[0][1], 302)
+
+        # verify subscriber is in pending state
+
+        try:
+            subscriber = Subscriber.objects.get(email=SUB_PARAMS['email'])
+        except Subscriber.DoesNotExist:
+            self.fail("No pending subscriber")
+
+        self.assertTrue(subscriber.is_pending())
+
+        # test email sent
+        self.do_test_email(subscriber)
+
+        # simulate a confirm click
+
+        response = self.client.get(
+                reverse('email_list-confirm', kwargs={'key': subscriber.key}),
+                follow=True)
+
+        self.assertTrue(response.status_code, 200)
+        self.assertEqual(len(response.redirect_chain), 1)
+        self.assertEqual(response.redirect_chain[0][0],
+                'http://testserver' + reverse('email_list-subscribed'))
+        self.assertEqual(response.redirect_chain[0][1], 302)
+
+        # verify active user
+        try:
+            subscriber = Subscriber.objects.get(email=SUB_PARAMS['email'])
+        except Subscriber.DoesNotExist:
+            self.fail("No active subscriber")
+
+        self.assertTrue(subscriber.is_active())
+
+    def do_test_unsubscribe(self):
+        # Get the form view
+        response = self.client.get(reverse('email_list-main'))
+        self.assertEqual(response.status_code, 200)
+        self.assertTemplateUsed(response, 'email_list/subscribe_form.html')
+
+        # Post a unsubscribe request
+
+        response = self.client.post(
+                reverse('email_list-main'),
+                UNSUB_PARAMS,
+                follow=True)
+
+        self.assertTrue(response.status_code, 200)
+        self.assertEqual(len(response.redirect_chain), 1)
+        self.assertEqual(response.redirect_chain[0][0],
+                'http://testserver' + reverse('email_list-request_unsubscribe'))
+        self.assertEqual(response.redirect_chain[0][1], 302)
+
+        # verify subscriber is in leaving state
+
+        try:
+            subscriber = Subscriber.objects.get(email=UNSUB_PARAMS['email'])
+        except Subscriber.DoesNotExist:
+            self.fail("No pending subscriber")
+
+        self.assertTrue(subscriber.is_leaving())
+
+        # test email sent
+        self.do_test_email(subscriber)
+
+        # simulate a click to unsubscribe
+
+        response = self.client.get(
+                reverse('email_list-confirm', kwargs={'key': subscriber.key}),
+                follow=True)
+
+        self.assertTrue(response.status_code, 200)
+        self.assertEqual(len(response.redirect_chain), 1)
+        self.assertEqual(response.redirect_chain[0][0],
+                'http://testserver' + reverse('email_list-unsubscribed'))
+        self.assertEqual(response.redirect_chain[0][1], 302)
+
+        # verify subscription has been removed
+
+        self.assertRaises(Subscriber.DoesNotExist, Subscriber.objects.get,
+                email=UNSUB_PARAMS['email'])
+
+    def do_test_email(self, subscriber):
+        """
+        Tests to see if the confirmation email was sent.
+
+        """
+        self.assertEqual(len(mail.outbox), 1)
+        self.assertEqual(len(mail.outbox[0].to), 1)
+        self.assertEqual(mail.outbox[0].to[0], subscriber.email)
+        self.assertTrue(str(mail.outbox[0].message()).count(subscriber.key) > 0)
+
+        # clear outbox
+        mail.outbox = []
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/email_list/urls.py	Sat Apr 14 16:40:29 2012 -0500
@@ -0,0 +1,28 @@
+"""
+Urls for the email_list application.
+
+"""
+from django.conf.urls import patterns, url
+from django.views.generic import TemplateView
+
+
+urlpatterns = patterns('email_list.views',
+   url(r'^$',
+       'mailing_list',
+       name='email_list-main'),
+   url(r'^confirm/(?P<key>[-a-zA-Z0-9_=]{28})/$',
+       'confirm',
+       name='email_list-confirm'),
+   url(r'^request/subscribe/$',
+       TemplateView.as_view(template_name='email_list/subscribe_request.html'),
+       name='email_list-request_subscribe'),
+   url(r'^request/unsubscribe/$',
+       TemplateView.as_view(template_name='email_list/unsubscribe_request.html'),
+       name='email_list-request_unsubscribe'),
+   url(r'^subscribed/$',
+       TemplateView.as_view(template_name='email_list/subscribed.html'),
+       name='email_list-subscribed'),
+   url(r'^unsubscribed/$',
+       TemplateView.as_view(template_name='email_list/unsubscribed.html'),
+       name='email_list-unsubscribed'),
+)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/email_list/views.py	Sat Apr 14 16:40:29 2012 -0500
@@ -0,0 +1,56 @@
+"""
+Views for the email_list application.
+
+"""
+import logging
+
+from django.http import HttpResponseServerError
+from django.shortcuts import render, redirect, get_object_or_404
+
+from email_list.forms import SubscriberForm
+from email_list.models import Subscriber
+
+
+logger = logging.getLogger(__name__)
+
+
+def mailing_list(request):
+    """
+    The main view for handling email list actions (subscribe or unsubscribe).
+
+    """
+    if request.method == 'POST':
+        form = SubscriberForm(request.POST)
+        if form.is_valid():
+            form.process()
+
+            if form.is_subscribe():
+                return redirect('email_list-request_subscribe')
+            else:
+                return redirect('email_list-request_unsubscribe')
+
+    else:
+        form = SubscriberForm()
+
+    return render(request, 'email_list/subscribe_form.html', {'form': form})
+
+
+def confirm(request, key):
+    """
+    This view handles the confirmation of a subscribe or unsubscribe action.
+
+    """
+    subscriber = get_object_or_404(Subscriber, key=key)
+
+    if subscriber.is_pending():
+        subscriber.set_active()
+        subscriber.save()
+        return redirect('email_list-subscribed')
+    elif subscriber.is_leaving():
+        subscriber.delete()
+        return redirect('email_list-unsubscribed')
+
+    # This should not happen
+    logger.error("Trying to confirm subscriber %d, but status is %s",
+            subscriber.pk, subscriber.status)
+    return HttpResponseServerError()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gigs/admin.py	Sat Apr 14 16:40:29 2012 -0500
@@ -0,0 +1,57 @@
+"""
+Automatic admin definitions for the gigs application.
+
+"""
+from django.contrib import admin
+
+from gigs.models import Band, City, Country, Gig, State, Venue
+
+
+class CityInline(admin.TabularInline):
+    model = City
+
+
+class CityAdmin(admin.ModelAdmin):
+    list_display = ['name', 'state', 'country']
+    list_filter = ['state']
+    search_fields = ['name']
+
+    def queryset(self, request):
+        qs = super(CityAdmin, self).queryset(request)
+        return qs.select_related('state', 'country')
+
+
+class StateAdmin(admin.ModelAdmin):
+    inlines = [CityInline]
+
+
+class VenueAdmin(admin.ModelAdmin):
+    list_filter = ['city__name']
+    list_display = ['name', 'city']
+    search_fields = ['name']
+
+    def queryset(self, request):
+        qs = super(VenueAdmin, self).queryset(request)
+        return qs.select_related('city', 'city__state', 'city__country')
+
+
+class BandAdmin(admin.ModelAdmin):
+    search_fields = ['name']
+
+
+class GigAdmin(admin.ModelAdmin):
+    list_filter = ['date']
+    save_on_top = True
+    filter_horizontal = ['bands']
+
+    def queryset(self, request):
+        qs = super(GigAdmin, self).queryset(request)
+        return qs.select_related('venue')
+
+
+admin.site.register(Band, BandAdmin)
+admin.site.register(City, CityAdmin)
+admin.site.register(Country)
+admin.site.register(Gig, GigAdmin)
+admin.site.register(State, StateAdmin)
+admin.site.register(Venue, VenueAdmin)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gigs/models.py	Sat Apr 14 16:40:29 2012 -0500
@@ -0,0 +1,97 @@
+"""
+Models for the gigs application.
+
+"""
+from django.db import models
+from django.contrib.localflavor.us.models import USStateField
+from django.contrib.localflavor.us.models import PhoneNumberField
+
+from photologue.models import Photo
+
+
+class Country(models.Model):
+    name = models.CharField(max_length=64)
+
+    class Meta:
+        ordering = ['name']
+        verbose_name_plural = 'Countries'
+
+    def __unicode__(self):
+        return self.name
+
+
+class State(models.Model):
+    name = models.CharField(max_length=16)
+    abbrev = USStateField()
+
+    class Meta:
+        ordering = ['name']
+
+    def __unicode__(self):
+        return self.name
+
+
+class City(models.Model):
+    name = models.CharField(max_length=50)
+    state = models.ForeignKey(State, null=True, blank=True)
+    country = models.ForeignKey(Country, null=True, blank=True)
+
+    class Meta:
+        verbose_name_plural = 'Cities'
+        ordering = ['name']
+
+    def __unicode__(self):
+        if self.state:
+            return u'%s, %s' % (self.name, self.state.abbrev)
+        
+        return self.name
+
+
+class Venue(models.Model):
+    name = models.CharField(max_length=50)
+    url = models.URLField(blank=True)
+    address = models.CharField(max_length=255, blank=True)
+    phone = PhoneNumberField(help_text="Format: XXX-XXX-XXXX", blank=True)
+    city = models.ForeignKey(City)
+
+    class Meta:
+        ordering = ['name']
+
+    def __unicode__(self):
+        return self.name
+
+
+class Band(models.Model):
+    name = models.CharField(max_length=64)
+    url = models.URLField(blank=True)
+
+    class Meta:
+        ordering = ['name']
+
+    def __unicode__(self):
+        return self.name
+
+
+class Gig(models.Model):
+    title = models.CharField(max_length=50, blank=True,
+            help_text="Optional; e.g. Some Festival")
+    url = models.URLField(blank=True,
+            help_text="Optional; e.g. Some Festival's Website")
+    date = models.DateField(db_index=True)
+    time = models.TimeField(null=True, blank=True)
+    venue = models.ForeignKey(Venue, null=True, blank=True)
+    notes = models.TextField(blank=True)
+    bands = models.ManyToManyField(Band, blank=True)
+    flyer = models.ForeignKey(Photo, null=True, blank=True,
+            related_name='gig_flyers')
+
+    def __unicode__(self):
+        if self.title:
+            return u'%s %s %s' % (self.date.strftime('%m/%d/%Y'), self.title, self.venue.name)
+        elif self.venue:
+            return u'%s %s' % (self.date.strftime('%m/%d/%Y'), self.venue.name)
+        else:
+            return self.date.strftime('%m/%d/%Y')
+
+    class Meta:
+        ordering = ['-date', 'time']
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gigs/templatetags/gig_tags.py	Sat Apr 14 16:40:29 2012 -0500
@@ -0,0 +1,22 @@
+"""
+Template tags for the gigs application.
+
+"""
+import datetime
+
+from django import template
+
+from gigs.models import Gig
+
+
+register = template.Library()
+
+
+@register.inclusion_tag('gigs/upcoming_gigs_tag.html')
+def upcoming_gigs():
+
+    gigs = Gig.objects.filter(date__gte=datetime.date.today).select_related(
+        'flyer', 'venue', 'venue__city', 'venue__city__state',
+        'venue__city__country').order_by('date')[:10]
+
+    return {'gigs': gigs}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gigs/urls.py	Sat Apr 14 16:40:29 2012 -0500
@@ -0,0 +1,19 @@
+"""
+Urls for the gigs application.
+
+"""
+from django.conf.urls import patterns, url
+from django.views.generic import ListView
+
+from gigs.models import Gig
+
+
+urlpatterns = patterns('',
+   url(r'^$', 'gigs.views.gigs', name='gigs-index'),
+   url(r'^flyers/$',
+       ListView.as_view(
+           queryset=Gig.objects.exclude(flyer__isnull=True).select_related('flyer'),
+           template_name='gigs/flyers.html',
+           context_object_name='gig_list'),
+       name='gigs-flyers')
+)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gigs/views.py	Sat Apr 14 16:40:29 2012 -0500
@@ -0,0 +1,76 @@
+"""
+Views for the gigs application.
+
+"""
+import collections
+import datetime
+
+from django.db import connection
+from django.shortcuts import render
+
+from gigs.models import Band, Gig
+
+
+def gigs(request):
+    """
+    This view function renders the main gigs view, showing upcoming and past
+    gigs as well as some statistics.
+
+    """
+    today = datetime.date.today()
+    gigs = Gig.objects.select_related('venue', 'flyer', 'venue__city',
+              'venue__city__state', 'venue__city__country')
+    upcoming = []
+    previous = []
+
+    # To avoid many, many database hits in the template, we get all the
+    # bands out at once. We also get the many-to-many intermediate table
+    # that Django generated for us so we can associate bands to gigs.
+    # Since we don't know about this table we drop into raw SQL to get
+    # the contents.
+
+    bands = dict((band.id, band) for band in Band.objects.all())
+    cursor = connection.cursor()
+    cursor.execute('SELECT * FROM gigs_gig_bands')
+    gig_bands = collections.defaultdict(list)
+    for row in cursor.fetchall():
+        gig_bands[row[1]].append(bands[row[2]])
+
+    for gig in gigs:
+        gig.bands_ = gig_bands[gig.id]
+        if gig.date >= today:
+            upcoming.append(gig)
+        else:
+            previous.append(gig)
+
+    upcoming.reverse()
+
+    stats = {}
+    venues = set()
+    cities = set()
+    states = set()
+    countries = set()
+    for gig in previous:
+        venues.add(gig.venue.id)
+        cities.add(gig.venue.city.id)
+        if gig.venue.city.state:
+            states.add(gig.venue.city.state.id)
+        if gig.venue.city.country:
+            countries.add(gig.venue.city.country.id)
+
+    stats['count'] = len(previous)
+    stats['venues'] = len(venues)
+    stats['cities'] = len(cities)
+    stats['states'] = len(states)
+    stats['countries'] = len(countries)
+    stats['bands'] = len(bands)
+
+    flyer_gigs = Gig.objects.exclude(flyer__isnull = True).select_related(
+         'venue', 'flyer').order_by('-date')
+
+    return render(request, 'gigs/gigs.html', {
+         'upcoming' : upcoming,
+         'previous' : previous,
+         'stats' : stats,
+         'flyer_gigs' : flyer_gigs,
+         })
--- a/madeira/apache/madeira.wsgi	Sat Apr 14 15:49:28 2012 -0500
+++ b/madeira/apache/madeira.wsgi	Sat Apr 14 16:40:29 2012 -0500
@@ -7,7 +7,6 @@
 site.addsitedir('/home/var/django-sites/virtualenvs/madeira/lib/python2.5/site-packages')
 
 sys.path.append('/home/var/django-sites/virtualenvs/madeira/madeira')
-sys.path.append('/home/var/django-sites/virtualenvs/madeira/madeira/madeira')
 os.environ['PYTHON_EGG_CACHE'] = '/home/var/django-sites/virtualenvs/madeira/eggs/'
 
 
--- a/madeira/articles/admin.py	Sat Apr 14 15:49:28 2012 -0500
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,21 +0,0 @@
-"""
-Automatic admin definitions for the articles application.
-
-"""
-from django.contrib import admin
-from django.conf import settings
-
-from articles.models import Article
-
-
-class ArticleAdmin(admin.ModelAdmin):
-    save_on_top = True
-    list_filter = ['date']
-    list_display = ['title', 'date']
-    search_fields = ['text', 'title']
-
-    class Media:
-        js = settings.THIRD_PARTY_JS['tiny_mce']
-
-
-admin.site.register(Article, ArticleAdmin)
--- a/madeira/articles/management/commands/import_old_articles.py	Sat Apr 14 15:49:28 2012 -0500
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,59 +0,0 @@
-"""
-import_old_articles.py - For importing articles from the older version of this
-website.
-
-"""
-import datetime
-
-from django.conf import settings
-from django.core.management.base import LabelCommand
-from django.utils import simplejson as json
-from django.utils.html import linebreaks
-import textile
-
-from articles.models import Article
-
-
-class Command(LabelCommand):
-    args = '<filename filename ...>'
-    help = 'Imports older articles in JSON format'
-
-    def handle_label(self, filename, **options):
-        """
-        Process the file of older articles in JSON. Convert to the new model
-        scheme.
-
-        """
-        with open(filename, 'rb') as f:
-            items = json.load(f)
-
-        for item in items:
-            if item['model'] == 'band.article':
-                self.process_item(item)
-
-    def process_item(self, item):
-
-        fields = item['fields']
-
-        content = fields['text'].strip()
-        if fields['markup_enabled']:
-            text = textile.textile(content, encoding='utf-8', output='utf-8')
-        else:
-            text = linebreaks(fields['text'])
-
-        source = linebreaks(fields['source'].strip())
-
-        pdf = fields['pdf'].strip()
-        if pdf:
-            pdf = u"%s%s" % (settings.MEDIA_URL, pdf.replace('\\', '/'))
-
-        article = Article(
-                id=item['pk'],
-                title=fields['title'].strip(),
-                date=datetime.datetime.strptime(fields['date'], '%Y-%m-%d'),
-                text=text,
-                source=source,
-                url=fields['url'].strip(),
-                pdf=pdf)
-
-        article.save()
--- a/madeira/articles/models.py	Sat Apr 14 15:49:28 2012 -0500
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,28 +0,0 @@
-"""
-Models for the articles application.
-
-"""
-from django.db import models
-
-
-class Article(models.Model):
-    title = models.CharField(max_length=64)
-    date = models.DateTimeField(db_index=True)
-    text = models.TextField()
-    source = models.TextField(help_text="Enter the source/author for the "
-            "article, copyright info, etc; it will appear under the article.")
-    url = models.URLField(blank=True,
-            help_text = 'Link to original article; optional')
-    pdf = models.FileField(upload_to = 'pdf/articles/%Y/%m/%d/', blank=True,
-            help_text="If you want to make the original article available as "
-            "a PDF download, you may upload it here.")
-
-    def __unicode__(self):
-        return self.title
-
-    class Meta:
-        ordering = ['-date']
-
-    @models.permalink
-    def get_absolute_url(self):
-        return ('articles-item', [], {'pk': str(self.id)})
--- a/madeira/articles/urls.py	Sat Apr 14 15:49:28 2012 -0500
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,20 +0,0 @@
-"""
-Urls for the articles application.
-
-"""
-from django.conf.urls import patterns, url
-from django.views.generic import DetailView, ListView
-
-from articles.models import Article
-
-
-urlpatterns = patterns('',
-   url(r'^$',
-       ListView.as_view(
-           model=Article,
-           context_object_name='article_list'),
-       name='articles-index'),
-   url(r'^(?P<pk>\d+)/$',
-       DetailView.as_view(model=Article, context_object_name='article'),
-       name='articles-item')
-)
--- a/madeira/articles/views.py	Sat Apr 14 15:49:28 2012 -0500
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,1 +0,0 @@
-# Create your views here.
--- a/madeira/band/admin.py	Sat Apr 14 15:49:28 2012 -0500
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,83 +0,0 @@
-"""
-Automatic admin definitions for the band models.
-
-"""
-from django.contrib import admin
-
-from band.models import Album
-from band.models import AlbumMerchant
-from band.models import AlbumTrack
-from band.models import Gear
-from band.models import LabelRelease
-from band.models import Member
-from band.models import Merchandise
-from band.models import RecordLabel
-
-
-class GearInline(admin.TabularInline):
-    model = Gear
-
-
-class GearAdmin(admin.ModelAdmin):
-    list_display = ['item', 'member']
-    list_filter = ['member']
-
-
-class MemberAdmin(admin.ModelAdmin):
-    list_display = ['name', 'instrument', 'is_active']
-    inlines = [GearInline]
-
-
-class AlbumTrackInline(admin.TabularInline):
-    model = AlbumTrack
-
-
-class AlbumTrackAdmin(admin.ModelAdmin):
-    list_display = ['track_name', 'album']
-    list_filter = ['album']
-
-
-class LabelReleaseInline(admin.TabularInline):
-    model = LabelRelease
-
-
-class LabelReleaseAdmin(admin.ModelAdmin):
-    list_display = ['catalog_number', 'album', 'record_label', 'release_date']
-    list_filter = ['record_label', 'album']
-
-
-class RecordLabelAdmin(admin.ModelAdmin):
-    inlines = [LabelReleaseInline]
-
-
-class AlbumMerchantInline(admin.TabularInline):
-    model = AlbumMerchant
-
-
-class AlbumMerchantAdmin(admin.ModelAdmin):
-    list_display = ['name', 'album']
-    list_filter = ['album']
-
-
-class AlbumAdmin(admin.ModelAdmin):
-    save_on_top = True
-    inlines = [
-        AlbumTrackInline,
-        LabelReleaseInline,
-        AlbumMerchantInline,
-    ]
-
-
-class MerchandiseAdmin(admin.ModelAdmin):
-    list_display = ['name', 'price', 'in_stock']
-    list_filter = ['in_stock']
-
-
-admin.site.register(Gear, GearAdmin)
-admin.site.register(Member, MemberAdmin)
-admin.site.register(AlbumTrack, AlbumTrackAdmin)
-admin.site.register(LabelRelease, LabelReleaseAdmin)
-admin.site.register(RecordLabel, RecordLabelAdmin)
-admin.site.register(AlbumMerchant, AlbumMerchantAdmin)
-admin.site.register(Album, AlbumAdmin)
-admin.site.register(Merchandise, MerchandiseAdmin)
--- a/madeira/band/management/commands/import_old_band.py	Sat Apr 14 15:49:28 2012 -0500
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,149 +0,0 @@
-"""
-import_old_band.py - For importing band models from the older version of this
-website.
-
-"""
-import datetime
-
-from django.core.management.base import LabelCommand
-from django.utils import simplejson as json
-from django.utils.html import linebreaks
-from photologue.models import Photo
-
-from band.models import (Member, Gear, RecordLabel, Album, AlbumTrack,
-        LabelRelease, AlbumMerchant, Merchandise)
-
-
-class Command(LabelCommand):
-    args = '<filename filename ...>'
-    help = 'Imports older band model objects in JSON format'
-
-    members = {}
-    albums = {}
-    labels = {}
-
-    def handle_label(self, filename, **options):
-        """
-        Process the file of older model objects in JSON. Convert to the new model
-        scheme.
-
-        """
-        with open(filename, 'rb') as f:
-            items = json.load(f)
-
-        for item in items:
-            if item['model'] == 'band.member':
-                self.process_member(item)
-            elif item['model'] == 'band.gear':
-                self.process_gear(item)
-            elif item['model'] == 'band.record_label':
-                self.process_record_label(item)
-            elif item['model'] == 'band.album':
-                self.process_album(item)
-            elif item['model'] == 'band.album_track':
-                self.process_track(item)
-            elif item['model'] == 'band.label_release':
-                self.process_release(item)
-            elif item['model'] == 'band.album_merchant':
-                self.process_merchant(item)
-            elif item['model'] == 'band.merchandise':
-                self.process_merch(item)
-
-    def process_member(self, item):
-
-        fields = item['fields']
-
-        start_date = datetime.datetime.strptime(fields['start_date'], '%Y-%m-%d')
-
-        if fields['end_date'] == u'1985-01-01':
-            end_date = None
-        else:
-            end_date = datetime.datetime.strptime(fields['end_date'], '%Y-%m-%d')
-
-        member = Member(id=item['pk'],
-                name=fields['name'],
-                nickname=fields['nickname'],
-                instrument=fields['instrument'],
-                bio=fields['bio'],
-                photo=fields['photo'],
-                order=int(fields['order']),
-                is_active=fields['is_active'],
-                start_date=start_date,
-                end_date=end_date,
-                email=fields['email'])
-        member.save()
-        self.members[member.id] = member
-
-    def process_gear(self, item):
-        fields = item['fields']
-
-        gear = Gear(id=item['pk'],
-                member=self.members[int(fields['member'])],
-                item=fields['item'])
-        gear.save()
-
-    def process_record_label(self, item):
-        fields = item['fields']
-
-        label = RecordLabel(id=item['pk'],
-                url=fields['url'],
-                name=fields['name'])
-        label.save()
-        self.labels[label.id] = label
-        
-    def process_album(self, item):
-        fields = item['fields']
-
-        photo = Photo.objects.get(id=fields['photo'])
-
-        album = Album(id=item['pk'],
-                title=fields['title'],
-                photo=photo,
-                desc=fields['desc'])
-        album.save()
-        self.albums[album.id] = album
-
-    def process_track(self, item):
-        fields = item['fields']
-
-        track = AlbumTrack(id=item['pk'],
-                album=self.albums[int(fields['album'])],
-                track_number=int(fields['track_number']),
-                track_name=fields['track_name'])
-        track.save()
-
-    def process_release(self, item):
-        fields = item['fields']
-
-        release = LabelRelease(id=item['pk'],
-                record_label=self.labels[int(fields['record_label'])],
-                album=self.albums[int(fields['album'])],
-                catalog_number=fields['catalog_number'],
-                release_date=datetime.datetime.strptime(
-                    fields['release_date'], '%Y-%m-%d'))
-
-        release.save()
-
-    def process_merchant(self, item):
-        fields = item['fields']
-
-        merchant = AlbumMerchant(id=item['pk'],
-                album=self.albums[int(fields['album'])],
-                url=fields['url'],
-                name=fields['name'])
-
-        merchant.save()
-
-    def process_merch(self, item):
-        fields = item['fields']
-
-        photo = Photo.objects.get(id=fields['photo'])
-
-        merch = Merchandise(id=item['pk'],
-                name=fields['name'],
-                desc=fields['desc'],
-                price=fields['price'],
-                in_stock=fields['in_stock'],
-                photo=photo)
-
-        merch.save()
--- a/madeira/band/models.py	Sat Apr 14 15:49:28 2012 -0500
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,111 +0,0 @@
-"""
-Models for the band application.
-
-"""
-from django.db import models
-
-from photologue.models import Photo
-
-
-class Member(models.Model):
-    name = models.CharField(max_length=50)
-    nickname = models.CharField(max_length=50, blank=True)
-    instrument = models.CharField(max_length=255)
-    bio = models.TextField(blank=True)
-    photo = models.ImageField(upload_to='images/bio/', blank=True)
-    order = models.SmallIntegerField(
-        help_text = "Controls order of display; lower numbers displayed first")
-    is_active = models.BooleanField()
-    start_date = models.DateField()
-    end_date = models.DateField(blank=True,
-        null=True,
-        help_text="Only used if the member is not active")
-    email = models.EmailField()
-
-    def __unicode__(self):
-        return self.name
-
-    class Meta:
-        ordering = ['-is_active', 'name']
-
-
-class Gear(models.Model):
-    member = models.ForeignKey(Member)
-    item = models.CharField(max_length=255)
-
-    def __unicode__(self):
-        return self.item
-
-    class Meta:
-        verbose_name_plural = 'Gear List'
-
-
-class RecordLabel(models.Model):
-    name = models.CharField(max_length=64)
-    url = models.URLField(max_length=200)
-
-    def __unicode__(self):
-        return self.name
-
-
-class Album(models.Model):
-    title = models.CharField(max_length=64)
-    photo = models.ForeignKey(Photo)
-    desc = models.TextField(blank=True)
-
-    def __unicode__(self):
-        return self.title
-
-
-class AlbumTrack(models.Model):
-    album = models.ForeignKey(Album, related_name='tracks')
-    track_number = models.SmallIntegerField()
-    track_name = models.CharField(max_length=64)
-
-    def __unicode__(self):
-        return self.track_name
-
-    class Meta:
-        verbose_name = 'Album Track'
-        ordering = ['album', 'track_number']
-
-
-class LabelRelease(models.Model):
-    record_label = models.ForeignKey(RecordLabel)
-    album = models.ForeignKey(Album, related_name='labels')
-    catalog_number = models.CharField(max_length=32)
-    release_date = models.DateField()
-
-    def __unicode__(self):
-        return u'%s %s %s' % (self.record_label.name, self.album.title,
-                self.catalog_number)
-
-    class Meta:
-        verbose_name = 'Label Release'
-
-
-class AlbumMerchant(models.Model):
-    album = models.ForeignKey(Album, related_name='merchants')
-    name = models.CharField(max_length=64)
-    url = models.URLField(max_length=200)
-
-    def __unicode__(self):
-        return u'%s (%s)' % (self.name, self.album.title)
-
-    class Meta:
-        verbose_name = 'Album Merchant'
-        ordering = ['name']
-
-
-class Merchandise(models.Model):
-    name = models.CharField(max_length=64)
-    desc = models.TextField()
-    price = models.DecimalField(max_digits=5, decimal_places=2)
-    in_stock = models.BooleanField()
-    photo = models.ForeignKey(Photo)
-
-    def __unicode__(self):
-        return self.name
-
-    class Meta:
-        verbose_name_plural = "Merchandise"
--- a/madeira/band/urls.py	Sat Apr 14 15:49:28 2012 -0500
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,13 +0,0 @@
-"""
-Urls for the band application.
-
-"""
-from django.conf.urls import patterns, url
-
-urlpatterns = patterns('band.views',
-   url(r'^bio/$', 'bio', name='band-bio'),
-   url(r'^buy/$', 'buy', name='band-buy'),
-   url(r'^contact/$', 'contact', name='band-contact'),
-   url(r'^photos/$', 'photos_index', name='band-photo_index'),
-   url(r'^photos/(\d+)$', 'photo_detail', name='band-photo_detail'),
-)
--- a/madeira/band/views.py	Sat Apr 14 15:49:28 2012 -0500
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,57 +0,0 @@
-"""
-Views for the band application.
-
-"""
-import random
-
-from django.shortcuts import render
-from django.shortcuts import get_object_or_404
-from photologue.models import Gallery
-from photologue.models import Photo
-
-from band.models import Member, Merchandise, Album
-
-
-def bio(request):
-    members = Member.objects.exclude(is_active=0)
-    return render(request, 'band/bio.html', {'members': members})
-
-
-def photos_index(request):
-    galleries = Gallery.objects.values('title', 'id').order_by('-id')
-
-    photo_ids = Photo.objects.filter(is_public=True).values_list('id',
-            flat=True)
-    photo_ids = random.sample(photo_ids, 4)
-    random_photos = Photo.objects.filter(id__in=photo_ids)
-
-    return render(request, 'band/photos.html', {
-        'galleries': galleries,
-        'random_photos': random_photos,
-    })
-
-
-def photo_detail(request, id):
-    gallery = get_object_or_404(Gallery, pk=id)
-    photos = gallery.photos.order_by('id')
-
-    return render(request, 'band/photo_detail.html', {
-        'gallery': gallery,
-        'photos': photos,
-    })
-
-
-def buy(request):
-    albums = Album.objects.all().order_by('-id')
-    merchandise = Merchandise.objects.all().order_by('-id')
-    return render(request, 'band/buy.html', {
-        'albums': albums, 
-        'merchandise': merchandise, 
-    })
-
-
-def contact(request):
-    band = Member.objects.exclude(is_active=0).order_by('order')
-    return render(request, 'band/contact.html', {
-        'band': band,
-    })
--- a/madeira/email_list/admin.py	Sat Apr 14 15:49:28 2012 -0500
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,41 +0,0 @@
-"""
-Automatic admin definitions for the email_list application.
-
-"""
-from django.contrib import admin
-from django.conf.urls import patterns, url
-from django.shortcuts import render, redirect
-
-from email_list.models import Subscriber
-from email_list.forms import AdminEmailForm
-
-
-class SubscriberAdmin(admin.ModelAdmin):
-    list_display = ['__unicode__', 'location', 'status']
-    list_filter = ['status']
-    search_fields = ['name', 'email']
-
-    def get_urls(self):
-        urls = super(SubscriberAdmin, self).get_urls()
-        my_urls = patterns('',
-            url(r'^send_mail/$',
-                self.admin_site.admin_view(self.send_mail),
-                name='email_list-admin_mail'),
-        )
-        return my_urls + urls
-
-    def send_mail(self, request):
-        if request.method == 'POST':
-            form = AdminEmailForm(request.POST)
-            if form.is_valid():
-                n = form.save()
-                msg = '%d mailing list emails sent' % n
-                self.message_user(request, msg)
-                return redirect('admin:index')
-        else:
-            form = AdminEmailForm()
-
-        return render(request, 'email_list/admin_mail.html', {'form': form})
-
-
-admin.site.register(Subscriber, SubscriberAdmin)
--- a/madeira/email_list/forms.py	Sat Apr 14 15:49:28 2012 -0500
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,155 +0,0 @@
-"""
-Forms for the email_list application.
-
-"""
-from django import forms
-from django.conf import settings
-from django.core.urlresolvers import reverse
-from django.core.mail import send_mail, send_mass_mail
-from django.template.loader import render_to_string
-
-from email_list.models import Subscriber
-
-
-SUBSCRIBE_OPTS = [('sub', 'Subscribe'), ('unsub', 'Unsubscribe')]
-
-ALREADY_SUBSCRIBED = "This email address is already subscribed."
-NOT_SUBSCRIBED = "This email address is not on our list."
-
-
-class SubscriberForm(forms.Form):
-    name = forms.CharField(max_length=64, required=False)
-    email = forms.EmailField()
-    location = forms.CharField(max_length=64, required=False)
-    option = forms.ChoiceField(choices=SUBSCRIBE_OPTS)
-
-    def clean(self):
-        """
-        This method ensures the appropriate action can be carried out and raises
-        a validation error if not.
-
-        """
-        email = self.cleaned_data['email']
-
-        if self.cleaned_data['option'] == 'sub':
-            # is the user already subscribed (active)?
-            try:
-                subscriber = Subscriber.objects.get(email=email)
-            except Subscriber.DoesNotExist:
-                subscriber = Subscriber(email=email,
-                        name=self.cleaned_data['name'],
-                        location=self.cleaned_data['location'])
-            else:
-                if subscriber.is_active():
-                    raise forms.ValidationError(ALREADY_SUBSCRIBED)
-        else:
-            # is the user already unsubscribed or not subscribed?
-            try:
-                subscriber = Subscriber.objects.get(email=email)
-            except Subscriber.DoesNotExist:
-                raise forms.ValidationError(NOT_SUBSCRIBED)
-
-        # save the subscriber away for a future process() call
-        self.subscriber = subscriber
-
-        return self.cleaned_data
-
-    def is_subscribe(self):
-        """
-        This function can be called after an is_valid() call to determine if the
-        request was for a subscribe or unsubscribe.
-
-        """
-        return self.cleaned_data['option'] == 'sub'
-
-    def process(self):
-        """
-        Call this function if is_valid() returns True. It carries out the user's
-        subscription request. 
-
-        """
-        if self.is_subscribe():
-            self.subscriber.set_pending()
-        else:
-            self.subscriber.set_leaving()
-
-        self.subscriber.save()
-        send_email(self.subscriber)
-
-
-class AdminEmailForm(forms.Form):
-    subject = forms.CharField(max_length=255, required=True, label='Subject:',
-        widget=forms.TextInput(attrs={'class': 'vTextField required',
-            'size': '120'}))
-    message = forms.CharField(label='Message:',
-        widget=forms.Textarea(attrs={'class': 'vLargeTextField required'}))
-
-    def __init__(self, *args, **kwargs):
-        initial = kwargs.pop('initial', {})
-        if 'subject' not in initial:
-            initial['subject'] = '[%s] ' % settings.BAND_CONFIG['BAND_NAME']
-            kwargs['initial'] = initial
-
-        super(AdminEmailForm, self).__init__(*args, **kwargs)
-
-    def save(self):
-        """
-        Call this function if is_valid() to send the mass email.
-        Returns the number of mails sent.
-
-        """
-        subject = self.cleaned_data['subject']
-        message = self.cleaned_data['message']
-        return send_mail_to_subscribers(subject, message)
-
-
-def send_email(subscriber):
-    """
-    This function sends out the appropriate email for the given subscriber.
-
-    """
-    config = settings.BAND_CONFIG
-    band = config['BAND_NAME']
-    from_email = config['BAND_EMAIL']
-
-    url = "http://%s%s" % (config['BAND_DOMAIN'],
-            reverse('email_list-confirm', kwargs={'key': subscriber.key}))
-
-    if subscriber.is_pending():
-        email_template = 'email_list/email_subscribe.txt'
-    else:
-        email_template = 'email_list/email_unsubscribe.txt'
-
-    msg = render_to_string(email_template, {
-        'band': band, 
-        'url': url, 
-        'band_domain': config['BAND_DOMAIN'],
-        })
-
-    subject = "[%s] Mailing List Confirmation" % band
-
-    send_mail(subject, msg, from_email, [subscriber.email])
-
-
-def send_mail_to_subscribers(subject, message):
-    """
-    Send an email to each mailing list subscriber with the given subject and
-    message.
-    Returns the number of messages sent.
-
-    """
-    config = settings.BAND_CONFIG
-    unsubscribe_url = "http://%s%s" % (config['BAND_DOMAIN'],
-              reverse('email_list-main'))
-
-    msg = render_to_string('email_list/mailing_list.txt', {
-        'band': config['BAND_NAME'],
-        'unsubscribe_url': unsubscribe_url,
-        'message': message,
-        })
-
-    mail_data = [(subject, msg, config['BAND_EMAIL'], [subscriber]) for
-            subscriber in Subscriber.objects.values_list('email', flat=True)]
-
-    send_mass_mail(mail_data)
-    return len(mail_data)
--- a/madeira/email_list/management/commands/import_old_email_list.py	Sat Apr 14 15:49:28 2012 -0500
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,51 +0,0 @@
-"""
-import_old_email_list.py - For importing the email list from the older version
-of this website.
-
-"""
-import datetime
-
-from django.core.management.base import LabelCommand
-from django.utils import simplejson as json
-
-from email_list.models import Subscriber
-
-
-class Command(LabelCommand):
-    args = '<filename filename ...>'
-    help = 'Imports older email list data in JSON format'
-
-    def handle_label(self, filename, **options):
-        """
-        Process the file of older email list data in JSON. Convert to the new
-        model scheme.
-
-        """
-        with open(filename, 'rb') as f:
-            items = json.load(f)
-
-        for item in items:
-            if item['model'] == 'band.fan':
-                self.process_item(item)
-
-    def process_item(self, item):
-
-        fields = item['fields']
-
-        # Only process 'active' subscribers because the new application
-        # has changed the key generation scheme.
-
-        if fields['status'] != 'A':
-            return
-
-        subscriber = Subscriber(
-                id=item['pk'],
-                name=fields['name'].strip(),
-                email=fields['email'].strip(),
-                location=fields['location'].strip(),
-                status='A',
-                key='',
-                status_date=datetime.datetime.strptime(
-                    fields['status_date'], '%Y-%m-%d'))
-
-        subscriber.save()                    
--- a/madeira/email_list/models.py	Sat Apr 14 15:49:28 2012 -0500
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,69 +0,0 @@
-"""
-Models for the email_list application.
-
-"""
-import base64
-import datetime
-import hashlib
-
-from django.db import models
-from django.conf import settings
-
-
-class Subscriber(models.Model):
-    status_codes = [('P', 'Pending'), ('A', 'Active'), ('L', 'Leaving')]
-    key_length = 28
-
-    name = models.CharField(max_length=64, blank=True)
-    email = models.EmailField(db_index=True)
-    location = models.CharField(max_length=64, blank=True)
-    status = models.CharField(max_length=1, choices=status_codes, default='A')
-    key = models.CharField(max_length=key_length, editable=False, blank=True,
-            db_index=True)
-    status_date = models.DateTimeField(editable=False)
-
-    class Meta:
-        ordering = ['name', 'email']
-
-    def __unicode__(self):
-        if self.name:
-            return u'%s <%s>' % (self.name, self.email)
-        return self.email
-
-    def save(self, *args, **kwargs):
-        if not self.pk and not self.status_date:
-            self.status_date = datetime.datetime.now()
-
-        super(Subscriber, self).save(*args, **kwargs)
-
-    def set_pending(self):
-        self.status = 'P'
-        self.status_date = datetime.datetime.now()
-        self.gen_key()
-
-    def set_active(self):
-        self.status = 'A'
-        self.status_date = datetime.datetime.now()
-        self.key = ''
-
-    def set_leaving(self):
-        self.status = 'L'
-        self.status_date = datetime.datetime.now()
-        self.gen_key()
-
-    def is_pending(self):
-        return self.status == 'P'
-
-    def is_leaving(self):
-        return self.status == 'L'
-
-    def is_active(self):
-        return self.status == 'A'
-
-    def gen_key(self):
-        source = (settings.SECRET_KEY + self.email + self.name + self.location +
-                self.status + self.status_date.isoformat())
-
-        sha = hashlib.sha1()
-        sha.update(source)
-        self.key = base64.urlsafe_b64encode(sha.digest())
--- a/madeira/email_list/tests/__init__.py	Sat Apr 14 15:49:28 2012 -0500
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,2 +0,0 @@
-from model_tests import *
-from view_tests import *
--- a/madeira/email_list/tests/model_tests.py	Sat Apr 14 15:49:28 2012 -0500
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,76 +0,0 @@
-"""
-Model tests for the email_list application.
-
-"""
-import datetime
-
-from django.test import TestCase
-
-from email_list.models import Subscriber
-
-
-class SubscriberTestCase(TestCase):
-
-    def test_auto_save(self):
-
-        sub = Subscriber(name='', location='', email='test@example.com')
-        sub.save()
-
-        now = datetime.datetime.now()
-        self.assertTrue(now - sub.status_date < datetime.timedelta(seconds=2))
-
-        self.assertTrue(sub.status == 'A')
-        self.assertTrue(sub.is_active())
-        self.failIf(sub.is_pending())
-        self.failIf(sub.is_leaving())
-
-    def test_set_pending(self):
-
-        sub = Subscriber(name='', location='', email='test@example.com')
-        sub.set_pending()
-
-        now = datetime.datetime.now()
-        self.assertTrue(now - sub.status_date < datetime.timedelta(seconds=2))
-
-        self.assertTrue(sub.status == 'P')
-        self.failIf(sub.is_active())
-        self.assertTrue(sub.is_pending())
-        self.failIf(sub.is_leaving())
-
-        self.assertTrue(len(sub.key) == sub.key_length)
-
-    def test_set_active(self):
-
-        sub = Subscriber(name='', location='', email='test@example.com')
-        sub.set_active()
-
-        now = datetime.datetime.now()
-        self.assertTrue(now - sub.status_date < datetime.timedelta(seconds=2))
-
-        self.assertTrue(sub.status == 'A')
-        self.assertTrue(sub.is_active())
-        self.failIf(sub.is_pending())
-        self.failIf(sub.is_leaving())
-
-    def test_set_leaving(self):
-
-        sub = Subscriber(name='', location='', email='test@example.com')
-        sub.set_leaving()
-
-        now = datetime.datetime.now()
-        self.assertTrue(now - sub.status_date < datetime.timedelta(seconds=2))
-
-        self.assertTrue(sub.status == 'L')
-        self.failIf(sub.is_active())
-        self.failIf(sub.is_pending())
-        self.assertTrue(sub.is_leaving())
-
-        self.assertTrue(len(sub.key) == sub.key_length)
-
-    def test_gen_key(self):
-
-        sub = Subscriber(name='', location='', email='test@example.com')
-        sub.status_date = datetime.datetime.now()
-        sub.gen_key()
-        self.assertTrue(len(sub.key) == sub.key_length)
-
--- a/madeira/email_list/tests/view_tests.py	Sat Apr 14 15:49:28 2012 -0500
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,213 +0,0 @@
-"""
-View tests for the email_list application.
-
-"""
-from django.test import TestCase
-from django.core.urlresolvers import reverse
-from django.core import mail
-
-from email_list.models import Subscriber
-import email_list.forms
-
-
-SUB_PARAMS = {
-    'name': 'John Doe',
-    'email': 'j.doe@example.com',
-    'location': 'USA',
-    'option': 'sub'
-}
-
-UNSUB_PARAMS = {
-    'name': '',
-    'email': SUB_PARAMS['email'],
-    'location': '',
-    'option': 'unsub'
-}
-
-class EmailListTestCase(TestCase):
-
-    def test_already_subscribed(self):
-        """
-        Test that subscribing twice fails with a form error.
-
-        """
-        sub = Subscriber(email=SUB_PARAMS['email'])
-        sub.set_active()
-        sub.save()
-
-        # Post a subscribe request
-
-        response = self.client.post(
-                reverse('email_list-main'),
-                SUB_PARAMS,
-                follow=True)
-
-        self.assertTrue(response.status_code, 200)
-        self.assertEqual(len(response.redirect_chain), 0)
-        self.assertContains(response, email_list.forms.ALREADY_SUBSCRIBED)
-
-    def test_not_subscribed(self):
-        """
-        Test that unsubscribing without being subscribed fails with a form error.
-
-        """
-        # Post a unsubscribe request
-
-        response = self.client.post(
-                reverse('email_list-main'),
-                UNSUB_PARAMS,
-                follow=True)
-
-        self.assertTrue(response.status_code, 200)
-        self.assertEqual(len(response.redirect_chain), 0)
-        self.assertContains(response, email_list.forms.NOT_SUBSCRIBED)
-
-    def test_normal_cycle(self):
-        """
-        Test a normal subscribe and unsubscribe cycle.
-
-        """
-        self.do_test_subscribe()
-        self.do_test_unsubscribe()
-
-    def test_subscribe_if_pending(self):
-        """
-        Ensure you can subscribe if you are already pending.
-
-        """
-        sub = Subscriber(email=SUB_PARAMS['email'])
-        sub.set_pending()
-        sub.save()
-        self.do_test_subscribe()
-
-    def test_subscribe_if_leaving(self):
-        """
-        Ensure you can subscribe if you are leaving.
-
-        """
-        sub = Subscriber(email=SUB_PARAMS['email'])
-        sub.set_leaving()
-        sub.save()
-        self.do_test_subscribe()
-
-    def test_unsubscribe_if_leaving(self):
-        """
-        Ensure you can unsubscribe if you are already leaving.
-
-        """
-        sub = Subscriber(email=SUB_PARAMS['email'])
-        sub.set_leaving()
-        sub.save()
-        self.do_test_unsubscribe()
-
-    def do_test_subscribe(self):
-        # Get the form view
-        response = self.client.get(reverse('email_list-main'))
-        self.assertEqual(response.status_code, 200)
-        self.assertTemplateUsed(response, 'email_list/subscribe_form.html')
-
-        # Post a subscribe request
-
-        response = self.client.post(
-                reverse('email_list-main'),
-                SUB_PARAMS,
-                follow=True)
-
-        self.assertTrue(response.status_code, 200)
-        self.assertEqual(len(response.redirect_chain), 1)
-        self.assertEqual(response.redirect_chain[0][0],
-                'http://testserver' + reverse('email_list-request_subscribe'))
-        self.assertEqual(response.redirect_chain[0][1], 302)
-
-        # verify subscriber is in pending state
-
-        try:
-            subscriber = Subscriber.objects.get(email=SUB_PARAMS['email'])
-        except Subscriber.DoesNotExist:
-            self.fail("No pending subscriber")
-
-        self.assertTrue(subscriber.is_pending())
-
-        # test email sent
-        self.do_test_email(subscriber)
-
-        # simulate a confirm click
-
-        response = self.client.get(
-                reverse('email_list-confirm', kwargs={'key': subscriber.key}),
-                follow=True)
-
-        self.assertTrue(response.status_code, 200)
-        self.assertEqual(len(response.redirect_chain), 1)
-        self.assertEqual(response.redirect_chain[0][0],
-                'http://testserver' + reverse('email_list-subscribed'))
-        self.assertEqual(response.redirect_chain[0][1], 302)
-
-        # verify active user
-        try:
-            subscriber = Subscriber.objects.get(email=SUB_PARAMS['email'])
-        except Subscriber.DoesNotExist:
-            self.fail("No active subscriber")
-
-        self.assertTrue(subscriber.is_active())
-
-    def do_test_unsubscribe(self):
-        # Get the form view
-        response = self.client.get(reverse('email_list-main'))
-        self.assertEqual(response.status_code, 200)
-        self.assertTemplateUsed(response, 'email_list/subscribe_form.html')
-
-        # Post a unsubscribe request
-
-        response = self.client.post(
-                reverse('email_list-main'),
-                UNSUB_PARAMS,
-                follow=True)
-
-        self.assertTrue(response.status_code, 200)
-        self.assertEqual(len(response.redirect_chain), 1)
-        self.assertEqual(response.redirect_chain[0][0],
-                'http://testserver' + reverse('email_list-request_unsubscribe'))
-        self.assertEqual(response.redirect_chain[0][1], 302)
-
-        # verify subscriber is in leaving state
-
-        try:
-            subscriber = Subscriber.objects.get(email=UNSUB_PARAMS['email'])
-        except Subscriber.DoesNotExist:
-            self.fail("No pending subscriber")
-
-        self.assertTrue(subscriber.is_leaving())
-
-        # test email sent
-        self.do_test_email(subscriber)
-
-        # simulate a click to unsubscribe
-
-        response = self.client.get(
-                reverse('email_list-confirm', kwargs={'key': subscriber.key}),
-                follow=True)
-
-        self.assertTrue(response.status_code, 200)
-        self.assertEqual(len(response.redirect_chain), 1)
-        self.assertEqual(response.redirect_chain[0][0],
-                'http://testserver' + reverse('email_list-unsubscribed'))
-        self.assertEqual(response.redirect_chain[0][1], 302)
-
-        # verify subscription has been removed
-
-        self.assertRaises(Subscriber.DoesNotExist, Subscriber.objects.get,
-                email=UNSUB_PARAMS['email'])
-
-    def do_test_email(self, subscriber):
-        """
-        Tests to see if the confirmation email was sent.
-
-        """
-        self.assertEqual(len(mail.outbox), 1)
-        self.assertEqual(len(mail.outbox[0].to), 1)
-        self.assertEqual(mail.outbox[0].to[0], subscriber.email)
-        self.assertTrue(str(mail.outbox[0].message()).count(subscriber.key) > 0)
-
-        # clear outbox
-        mail.outbox = []
--- a/madeira/email_list/urls.py	Sat Apr 14 15:49:28 2012 -0500
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,28 +0,0 @@
-"""
-Urls for the email_list application.
-
-"""
-from django.conf.urls import patterns, url
-from django.views.generic import TemplateView
-
-
-urlpatterns = patterns('email_list.views',
-   url(r'^$',
-       'mailing_list',
-       name='email_list-main'),
-   url(r'^confirm/(?P<key>[-a-zA-Z0-9_=]{28})/$',
-       'confirm',
-       name='email_list-confirm'),
-   url(r'^request/subscribe/$',
-       TemplateView.as_view(template_name='email_list/subscribe_request.html'),
-       name='email_list-request_subscribe'),
-   url(r'^request/unsubscribe/$',
-       TemplateView.as_view(template_name='email_list/unsubscribe_request.html'),
-       name='email_list-request_unsubscribe'),
-   url(r'^subscribed/$',
-       TemplateView.as_view(template_name='email_list/subscribed.html'),
-       name='email_list-subscribed'),
-   url(r'^unsubscribed/$',
-       TemplateView.as_view(template_name='email_list/unsubscribed.html'),
-       name='email_list-unsubscribed'),
-)
--- a/madeira/email_list/views.py	Sat Apr 14 15:49:28 2012 -0500
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,56 +0,0 @@
-"""
-Views for the email_list application.
-
-"""
-import logging
-
-from django.http import HttpResponseServerError
-from django.shortcuts import render, redirect, get_object_or_404
-
-from email_list.forms import SubscriberForm
-from email_list.models import Subscriber
-
-
-logger = logging.getLogger(__name__)
-
-
-def mailing_list(request):
-    """
-    The main view for handling email list actions (subscribe or unsubscribe).
-
-    """
-    if request.method == 'POST':
-        form = SubscriberForm(request.POST)
-        if form.is_valid():
-            form.process()
-
-            if form.is_subscribe():
-                return redirect('email_list-request_subscribe')
-            else:
-                return redirect('email_list-request_unsubscribe')
-
-    else:
-        form = SubscriberForm()
-
-    return render(request, 'email_list/subscribe_form.html', {'form': form})
-
-
-def confirm(request, key):
-    """
-    This view handles the confirmation of a subscribe or unsubscribe action.
-
-    """
-    subscriber = get_object_or_404(Subscriber, key=key)
-
-    if subscriber.is_pending():
-        subscriber.set_active()
-        subscriber.save()
-        return redirect('email_list-subscribed')
-    elif subscriber.is_leaving():
-        subscriber.delete()
-        return redirect('email_list-unsubscribed')
-
-    # This should not happen
-    logger.error("Trying to confirm subscriber %d, but status is %s",
-            subscriber.pk, subscriber.status)
-    return HttpResponseServerError()
--- a/madeira/gigs/admin.py	Sat Apr 14 15:49:28 2012 -0500
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,57 +0,0 @@
-"""
-Automatic admin definitions for the gigs application.
-
-"""
-from django.contrib import admin
-
-from gigs.models import Band, City, Country, Gig, State, Venue
-
-
-class CityInline(admin.TabularInline):
-    model = City
-
-
-class CityAdmin(admin.ModelAdmin):
-    list_display = ['name', 'state', 'country']
-    list_filter = ['state']
-    search_fields = ['name']
-
-    def queryset(self, request):
-        qs = super(CityAdmin, self).queryset(request)
-        return qs.select_related('state', 'country')
-
-
-class StateAdmin(admin.ModelAdmin):
-    inlines = [CityInline]
-
-
-class VenueAdmin(admin.ModelAdmin):
-    list_filter = ['city__name']
-    list_display = ['name', 'city']
-    search_fields = ['name']
-
-    def queryset(self, request):
-        qs = super(VenueAdmin, self).queryset(request)
-        return qs.select_related('city', 'city__state', 'city__country')
-
-
-class BandAdmin(admin.ModelAdmin):
-    search_fields = ['name']
-
-
-class GigAdmin(admin.ModelAdmin):
-    list_filter = ['date']
-    save_on_top = True
-    filter_horizontal = ['bands']
-
-    def queryset(self, request):
-        qs = super(GigAdmin, self).queryset(request)
-        return qs.select_related('venue')
-
-
-admin.site.register(Band, BandAdmin)
-admin.site.register(City, CityAdmin)
-admin.site.register(Country)
-admin.site.register(Gig, GigAdmin)
-admin.site.register(State, StateAdmin)
-admin.site.register(Venue, VenueAdmin)
--- a/madeira/gigs/models.py	Sat Apr 14 15:49:28 2012 -0500
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,97 +0,0 @@
-"""
-Models for the gigs application.
-
-"""
-from django.db import models
-from django.contrib.localflavor.us.models import USStateField
-from django.contrib.localflavor.us.models import PhoneNumberField
-
-from photologue.models import Photo
-
-
-class Country(models.Model):
-    name = models.CharField(max_length=64)
-
-    class Meta:
-        ordering = ['name']
-        verbose_name_plural = 'Countries'
-
-    def __unicode__(self):
-        return self.name
-
-
-class State(models.Model):
-    name = models.CharField(max_length=16)
-    abbrev = USStateField()
-
-    class Meta:
-        ordering = ['name']
-
-    def __unicode__(self):
-        return self.name
-
-
-class City(models.Model):
-    name = models.CharField(max_length=50)
-    state = models.ForeignKey(State, null=True, blank=True)
-    country = models.ForeignKey(Country, null=True, blank=True)
-
-    class Meta:
-        verbose_name_plural = 'Cities'
-        ordering = ['name']
-
-    def __unicode__(self):
-        if self.state:
-            return u'%s, %s' % (self.name, self.state.abbrev)
-        
-        return self.name
-
-
-class Venue(models.Model):
-    name = models.CharField(max_length=50)
-    url = models.URLField(blank=True)
-    address = models.CharField(max_length=255, blank=True)
-    phone = PhoneNumberField(help_text="Format: XXX-XXX-XXXX", blank=True)
-    city = models.ForeignKey(City)
-
-    class Meta:
-        ordering = ['name']
-
-    def __unicode__(self):
-        return self.name
-
-
-class Band(models.Model):
-    name = models.CharField(max_length=64)
-    url = models.URLField(blank=True)
-
-    class Meta:
-        ordering = ['name']
-
-    def __unicode__(self):
-        return self.name
-
-
-class Gig(models.Model):
-    title = models.CharField(max_length=50, blank=True,
-            help_text="Optional; e.g. Some Festival")
-    url = models.URLField(blank=True,
-            help_text="Optional; e.g. Some Festival's Website")
-    date = models.DateField(db_index=True)
-    time = models.TimeField(null=True, blank=True)
-    venue = models.ForeignKey(Venue, null=True, blank=True)
-    notes = models.TextField(blank=True)
-    bands = models.ManyToManyField(Band, blank=True)
-    flyer = models.ForeignKey(Photo, null=True, blank=True,
-            related_name='gig_flyers')
-
-    def __unicode__(self):
-        if self.title:
-            return u'%s %s %s' % (self.date.strftime('%m/%d/%Y'), self.title, self.venue.name)
-        elif self.venue:
-            return u'%s %s' % (self.date.strftime('%m/%d/%Y'), self.venue.name)
-        else:
-            return self.date.strftime('%m/%d/%Y')
-
-    class Meta:
-        ordering = ['-date', 'time']
--- a/madeira/gigs/templatetags/gig_tags.py	Sat Apr 14 15:49:28 2012 -0500
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,22 +0,0 @@
-"""
-Template tags for the gigs application.
-
-"""
-import datetime
-
-from django import template
-
-from gigs.models import Gig
-
-
-register = template.Library()
-
-
-@register.inclusion_tag('gigs/upcoming_gigs_tag.html')
-def upcoming_gigs():
-
-    gigs = Gig.objects.filter(date__gte=datetime.date.today).select_related(
-        'flyer', 'venue', 'venue__city', 'venue__city__state',
-        'venue__city__country').order_by('date')[:10]
-
-    return {'gigs': gigs}
--- a/madeira/gigs/urls.py	Sat Apr 14 15:49:28 2012 -0500
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,19 +0,0 @@
-"""
-Urls for the gigs application.
-
-"""
-from django.conf.urls import patterns, url
-from django.views.generic import ListView
-
-from gigs.models import Gig
-
-
-urlpatterns = patterns('',
-   url(r'^$', 'gigs.views.gigs', name='gigs-index'),
-   url(r'^flyers/$',
-       ListView.as_view(
-           queryset=Gig.objects.exclude(flyer__isnull=True).select_related('flyer'),
-           template_name='gigs/flyers.html',
-           context_object_name='gig_list'),
-       name='gigs-flyers')
-)
--- a/madeira/gigs/views.py	Sat Apr 14 15:49:28 2012 -0500
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,76 +0,0 @@
-"""
-Views for the gigs application.
-
-"""
-import collections
-import datetime
-
-from django.db import connection
-from django.shortcuts import render
-
-from gigs.models import Band, Gig
-
-
-def gigs(request):
-    """
-    This view function renders the main gigs view, showing upcoming and past
-    gigs as well as some statistics.
-
-    """
-    today = datetime.date.today()
-    gigs = Gig.objects.select_related('venue', 'flyer', 'venue__city',
-              'venue__city__state', 'venue__city__country')
-    upcoming = []
-    previous = []
-
-    # To avoid many, many database hits in the template, we get all the
-    # bands out at once. We also get the many-to-many intermediate table
-    # that Django generated for us so we can associate bands to gigs.
-    # Since we don't know about this table we drop into raw SQL to get
-    # the contents.
-
-    bands = dict((band.id, band) for band in Band.objects.all())
-    cursor = connection.cursor()
-    cursor.execute('SELECT * FROM gigs_gig_bands')
-    gig_bands = collections.defaultdict(list)
-    for row in cursor.fetchall():
-        gig_bands[row[1]].append(bands[row[2]])
-
-    for gig in gigs:
-        gig.bands_ = gig_bands[gig.id]
-        if gig.date >= today:
-            upcoming.append(gig)
-        else:
-            previous.append(gig)
-
-    upcoming.reverse()
-
-    stats = {}
-    venues = set()
-    cities = set()
-    states = set()
-    countries = set()
-    for gig in previous:
-        venues.add(gig.venue.id)
-        cities.add(gig.venue.city.id)
-        if gig.venue.city.state:
-            states.add(gig.venue.city.state.id)
-        if gig.venue.city.country:
-            countries.add(gig.venue.city.country.id)
-
-    stats['count'] = len(previous)
-    stats['venues'] = len(venues)
-    stats['cities'] = len(cities)
-    stats['states'] = len(states)
-    stats['countries'] = len(countries)
-    stats['bands'] = len(bands)
-
-    flyer_gigs = Gig.objects.exclude(flyer__isnull = True).select_related(
-         'venue', 'flyer').order_by('-date')
-
-    return render(request, 'gigs/gigs.html', {
-         'upcoming' : upcoming,
-         'previous' : previous,
-         'stats' : stats,
-         'flyer_gigs' : flyer_gigs,
-         })
--- a/madeira/manage.py	Sat Apr 14 15:49:28 2012 -0500
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,11 +0,0 @@
-#!/usr/bin/env python
-from django.core.management import execute_manager
-try:
-    import settings # Assumed to be in the same directory.
-except ImportError:
-    import sys
-    sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n(If the file settings.py does indeed exist, it's causing an ImportError somehow.)\n" % __file__)
-    sys.exit(1)
-
-if __name__ == "__main__":
-    execute_manager(settings)
--- a/madeira/mp3/admin.py	Sat Apr 14 15:49:28 2012 -0500
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,29 +0,0 @@
-"""
-Automatic admin definitions for the models in the mp3 application.
-
-"""
-from django.contrib import admin
-from django.conf import settings
-
-from mp3.models import Collection, Song
-
-
-class SongInline(admin.TabularInline):
-   model = Song
-
-
-class CollectionAdmin(admin.ModelAdmin):
-    list_filter = ['date_added']
-    list_display = ['title', 'date_added']
-    inlines = [SongInline]
-
-    class Media:
-        js = settings.THIRD_PARTY_JS['tiny_mce']
-
-
-class SongAdmin(admin.ModelAdmin):
-    list_display = ['title', 'collection']
-
-
-admin.site.register(Collection, CollectionAdmin)
-admin.site.register(Song, SongAdmin)
--- a/madeira/mp3/management/commands/import_old_mp3.py	Sat Apr 14 15:49:28 2012 -0500
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,64 +0,0 @@
-"""
-import_old_mp3.py - For importing mp3 data from the older version of this
-website.
-
-"""
-import datetime
-
-from django.core.management.base import LabelCommand
-from django.utils import simplejson as json
-from django.utils.html import linebreaks
-
-from mp3.models import Collection, Song
-
-
-class Command(LabelCommand):
-    args = '<filename filename ...>'
-    help = 'Imports older mp3 & mp3 sets in JSON format'
-
-    collections = {}
-
-    def handle_label(self, filename, **options):
-        """
-        Process the file of older mp3 & mp3 sets in JSON. Convert to the new
-        model scheme.
-
-        """
-        with open(filename, 'rb') as f:
-            items = json.load(f)
-
-        for item in items:
-            if item['model'] == 'band.mp3_set':
-                self.process_mp3_set(item)
-
-        for item in items:
-            if item['model'] == 'band.mp3':
-                self.process_mp3(item)
-
-    def process_mp3_set(self, item):
-
-        fields = item['fields']
-
-        description = linebreaks(fields['text'].strip())
-
-        coll = Collection(
-                id=item['pk'],
-                title=fields['title'].strip(),
-                date_added=datetime.datetime.strptime(fields['date'], '%Y-%m-%d'),
-                description=description)
-
-        coll.save()
-        self.collections[coll.pk] = coll
-
-    def process_mp3(self, item):
-
-        fields = item['fields']
-
-        song = Song(
-                id=item['pk'],
-                title=fields['title'].strip(),
-                description=fields['desc'].strip(),
-                file=fields['file'],
-                collection=self.collections[fields['mp3_set']],
-                )
-        song.save()
--- a/madeira/mp3/models.py	Sat Apr 14 15:49:28 2012 -0500
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,39 +0,0 @@
-"""
-Models for the mp3 application.
-
-"""
-from django.db import models
-
-
-class Collection(models.Model):
-    """
-    This model represents a collection of songs.
-
-    """
-    title = models.CharField(max_length=64)
-    description = models.TextField()
-    date_added = models.DateTimeField()
-
-    class Meta:
-        ordering = ['-date_added']
-
-    def __unicode__(self):
-        return self.title
-
-
-class Song(models.Model):
-    """
-    This model represents an uploaded song file.
-
-    """
-    title = models.CharField(max_length=64)
-    description = models.CharField(max_length=255, blank=True)
-    file = models.FileField(upload_to='mp3s/%Y/%m/%d/')
-    collection = models.ForeignKey(Collection)
-
-    class Meta:
-        ordering = ['title']
-
-    def __unicode__(self):
-        return self.title
-
--- a/madeira/mp3/urls.py	Sat Apr 14 15:49:28 2012 -0500
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,18 +0,0 @@
-"""
-Urls for the mp3 application.
-
-"""
-from django.conf.urls import patterns, url
-from django.views.generic import ListView
-
-from mp3.models import Collection
-
-
-urlpatterns = patterns('',
-   url(r'^$',
-       ListView.as_view(
-           model=Collection,
-           paginate_by=10,
-           context_object_name='collection_list'),
-       name='mp3-index'),
-)
--- a/madeira/news/admin.py	Sat Apr 14 15:49:28 2012 -0500
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,19 +0,0 @@
-"""
-Automatic admin definitions for the models in the news application.
-
-"""
-from django.contrib import admin
-from django.conf import settings
-
-from news.models import News
-
-
-class NewsAdmin(admin.ModelAdmin):
-    list_filter = ['date']
-    search_fields = ['title', 'content']
-    save_on_top = True
-
-    class Media:
-        js = settings.THIRD_PARTY_JS['tiny_mce']
-
-admin.site.register(News, NewsAdmin)
--- a/madeira/news/management/commands/import_old_news.py	Sat Apr 14 15:49:28 2012 -0500
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,65 +0,0 @@
-"""
-import_old_news.py - For importing news stories from the older version of this
-website.
-
-"""
-import datetime
-
-from django.conf import settings
-from django.core.management.base import LabelCommand
-from django.utils import simplejson as json
-from django.utils.html import linebreaks
-import textile
-
-from news.models import News
-
-IMG_TAG = u"""\
-<img src="{src}" class="floatLeftBox" alt="{title}" title="{title}" border="0" />"""
-
-
-class Command(LabelCommand):
-    args = '<filename filename ...>'
-    help = 'Imports older news stories in JSON format'
-
-    def handle_label(self, filename, **options):
-        """
-        Process the file of older news stories in JSON. Convert to the new model
-        scheme.
-
-        """
-        with open(filename, 'rb') as f:
-            items = json.load(f)
-
-        for item in items:
-            if item['model'] == 'band.news':
-                self.process_item(item)
-
-    def process_item(self, item):
-
-        fields = item['fields']
-
-        content = fields['text'].strip()
-        if fields['markup_enabled']:
-            content = textile.textile(content, encoding='utf-8', output='utf-8')
-        else:
-            content = linebreaks(fields['text'])
-
-        author = fields['author'].strip()
-        if author:
-            content += u"<p>\u2013&nbsp;%s" % author
-
-        image = fields['photo'].strip()
-        if image:
-            caption = fields['photo_caption'].strip()
-            caption = caption if caption else 'Image'
-
-            src = u"%s%s" % (settings.MEDIA_URL, image)
-
-            content = IMG_TAG.format(src=src, title=caption) + content
-
-        news = News(id=item['pk'],
-                title=fields['title'].strip(),
-                date=datetime.datetime.strptime(fields['date'], '%Y-%m-%d'),
-                content=content)
-
-        news.save()
--- a/madeira/news/models.py	Sat Apr 14 15:49:28 2012 -0500
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,29 +0,0 @@
-"""
-Models for the news application.
-
-"""
-from django.db import models
-
-
-class News(models.Model):
-    """
-    This model represents all the info we store about each news entry.
-
-    """
-    title = models.CharField(max_length=128, blank=True)
-    date = models.DateTimeField(db_index=True)
-    content = models.TextField()
-
-    def __unicode__(self):
-        date_str = self.date.strftime('%m/%d/%Y')
-        if self.title:
-            return u'%s %s' % (date_str, self.title)
-        return u'%s' % date_str
-
-    @models.permalink
-    def get_absolute_url(self):
-        return ('news-item', [], {'pk': str(self.id)})
-
-    class Meta:
-        verbose_name_plural = 'News'
-        ordering = ['-date']
--- a/madeira/news/urls.py	Sat Apr 14 15:49:28 2012 -0500
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,21 +0,0 @@
-"""
-Urls for the news application.
-
-"""
-from django.conf.urls import patterns, url
-from django.views.generic import DetailView, ListView
-
-from news.models import News
-
-
-urlpatterns = patterns('',
-   url(r'^$',
-       ListView.as_view(
-           model=News,
-           paginate_by=10,
-           context_object_name='news_list'),
-       name='news-index'),
-   url(r'^story/(?P<pk>\d+)/$',
-       DetailView.as_view(model=News, context_object_name='story'),
-       name='news-item')
-)
--- a/madeira/photologue/LICENSE.txt	Sat Apr 14 15:49:28 2012 -0500
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,27 +0,0 @@
-Copyright (c) 2007-2008, Justin C. Driscoll
-All rights reserved.
-
-Redistribution and use in source and binary forms, with or without modification,
-are permitted provided that the following conditions are met:
-
-    1. Redistributions of source code must retain the above copyright notice,
-       this list of conditions and the following disclaimer.
-
-    2. Redistributions in binary form must reproduce the above copyright
-       notice, this list of conditions and the following disclaimer in the
-       documentation and/or other materials provided with the distribution.
-
-    3. Neither the name of django-photologue nor the names of its contributors may be used
-       to endorse or promote products derived from this software without
-       specific prior written permission.
-
-THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
-ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
-WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
-ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
-(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
-LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
-ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
-(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
-SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
--- a/madeira/photologue/README.txt	Sat Apr 14 15:49:28 2012 -0500
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,38 +0,0 @@
-Installation
-
-Step 1 - Download Photologue
-
-Photologue can be downloaded below or from the project page. Older versions are also available from the project page and users who like to live on the edge can checkout a copy of the latest trunk revision.
-
-Step 2 - Add Photologue To Your Project
-
-Copy the entire Photologue application folder (the folder named 'photologue' that contains 'models.py') to a location on your Python path such as your project root. Your project root is typically the directory where your 'settings.py' is found.
-
-Step 3 - Configure Your Settings
-
-Add 'photologue' to your INSTALLED_APPS setting:
-
-    INSTALLED_APPS = (
-         # ...other installed applications,
-         'photologue',
-    )
-
-Confirm that your MEDIA_ROOT and MEDIA_URL settings are correct.
-
-If you want to tweak things even more you can also over-ride a few default settings (optional, see documentation for more information on the available settings).
-
-Step 4 - Register Photologue with the Django Admin
-
-Add the following to your projects urls.py file:
-
-    from django.contrib import admin
-    
-    admin.autodiscover()
-
-Step 4 - Sync Your Database
-
-Run the 'manage.py syndb' command to create the appropriate tables. After the database in initialized, Photologue will walk you through creating some default models.
-
-Additional documentation available here:
-
-http://code.google.com/p/django-photologue/w/list
--- a/madeira/photologue/admin.py	Sat Apr 14 15:49:28 2012 -0500
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,60 +0,0 @@
-""" Newforms Admin configuration for Photologue
-
-"""
-from django.contrib import admin
-from models import *
-
-class GalleryAdmin(admin.ModelAdmin):
-    list_display = ('title', 'date_added', 'photo_count', 'is_public')
-    list_filter = ['date_added', 'is_public']
-    date_hierarchy = 'date_added'
-    prepopulated_fields = {'title_slug': ('title',)}
-    filter_horizontal = ('photos',)    
-    
-class PhotoAdmin(admin.ModelAdmin):
-    list_display = ('title', 'date_taken', 'date_added', 'is_public', 'tags', 'view_count', 'admin_thumbnail')
-    list_filter = ['date_added', 'is_public']
-    list_per_page = 10
-    prepopulated_fields = {'title_slug': ('title',)}
-    
-class PhotoEffectAdmin(admin.ModelAdmin):
-    list_display = ('name', 'description', 'admin_sample')
-    fieldsets = (
-        (None, {
-            'fields': ('name', 'description')
-        }),
-        ('Adjustments', {
-            'fields': ('color', 'brightness', 'contrast', 'sharpness')
-        }),
-        ('Filters', {
-            'fields': ('filters',)
-        }),
-        ('Reflection', {
-            'fields': ('reflection_size', 'reflection_strength', 'background_color')
-        }),
-    )
-
-class PhotoSizeAdmin(admin.ModelAdmin):
-    list_display = ('name', 'width', 'height', 'crop', 'pre_cache', 'effect', 'increment_count')
-    fieldsets = (
-        (None, {
-            'fields': ('name', 'width', 'height', 'quality')
-        }),
-        ('Options', {
-            'fields': ('upscale', 'crop', 'pre_cache', 'increment_count')
-        }),
-        ('Enhancements', {
-            'fields': ('effect', 'watermark',)
-        }),
-    )
-    
-class WatermarkAdmin(admin.ModelAdmin):
-    list_display = ('name', 'opacity', 'style')
-
-
-admin.site.register(Gallery, GalleryAdmin)
-admin.site.register(GalleryUpload)
-admin.site.register(Photo, PhotoAdmin)
-admin.site.register(PhotoEffect, PhotoEffectAdmin)
-admin.site.register(PhotoSize, PhotoSizeAdmin)
-admin.site.register(Watermark, WatermarkAdmin)
\ No newline at end of file
Binary file madeira/photologue/locale/pl/LC_MESSAGES/django.mo has changed
--- a/madeira/photologue/locale/pl/LC_MESSAGES/django.po	Sat Apr 14 15:49:28 2012 -0500
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,419 +0,0 @@
-# SOME DESCRIPTIVE TITLE.
-# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
-# This file is distributed under the same license as the PACKAGE package.
-# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
-#
-msgid ""
-msgstr ""
-"Project-Id-Version: Photologue Preview 2\n"
-"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2008-07-22 23:05+0200\n"
-"PO-Revision-Date: 2008-07-22 23:08+0100\n"
-"Last-Translator: Jakub Wiśniowski <restless.being@gmail.com>\n"
-"Language-Team: Jakub Wiśniowski <restless.being@gmail.com>\n"
-"MIME-Version: 1.0\n"
-"Content-Type: text/plain; charset=UTF-8\n"
-"Content-Transfer-Encoding: 8bit\n"
-"Plural-Forms: nplurals=3; plural=n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\\n\n"
-"X-Poedit-Language: Polish\n"
-"X-Poedit-Country: POLAND\n"
-"X-Poedit-SourceCharset: utf-8\n"
-
-#: models.py:32
-msgid "Photologue was unable to import the Python Imaging Library. Please confirm it`s installed and available on your current Python path."
-msgstr "Photologue nie był w stanie zaimportować Python Imaging Library. Upewnij się, że pakiet ten jest zainstalowany i znajduje się w ścieżce dostępnej dla Pythona."
-
-#: models.py:38
-msgid "Separate tags with spaces, put quotes around multiple-word tags."
-msgstr "Rozdziel tagi spacjami, ujmij w cudzysłowy tagi złożone z wielu słów."
-
-#: models.py:47
-msgid "Django-tagging was not found, tags will be treated as plain text."
-msgstr "Django-tagging nie zostało znalezione. Tagi będą traktowane jako czysty tekst."
-
-#: models.py:64
-msgid "Very Low"
-msgstr "Bardzo niska"
-
-#: models.py:65
-msgid "Low"
-msgstr "Niska"
-
-#: models.py:66
-msgid "Medium-Low"
-msgstr "Niższa średnia"
-
-#: models.py:67
-msgid "Medium"
-msgstr "Åšrednia"
-
-#: models.py:68
-msgid "Medium-High"
-msgstr "Wyższa średnia"
-
-#: models.py:69
-msgid "High"
-msgstr "Wysoka"
-
-#: models.py:70
-msgid "Very High"
-msgstr "Bardzo wysoka"
-
-#: models.py:75
-msgid "Top"
-msgstr "Góra"
-
-#: models.py:76
-msgid "Right"
-msgstr "Prawo"
-
-#: models.py:77
-msgid "Bottom"
-msgstr "Dół"
-
-#: models.py:78
-msgid "Left"
-msgstr "Lewo"
-
-#: models.py:79
-msgid "Center (Default)"
-msgstr "Środek (Domyślnie)"
-
-#: models.py:83
-msgid "Flip left to right"
-msgstr "Odbij w poziomie"
-
-#: models.py:84
-msgid "Flip top to bottom"
-msgstr "Odbij w pionie"
-
-#: models.py:85
-msgid "Rotate 90 degrees counter-clockwise"
-msgstr "Odwróć 90 stopni w lewo"
-
-#: models.py:86
-msgid "Rotate 90 degrees clockwise"
-msgstr "Odwróć 90 stopni w prawo"
-
-#: models.py:87
-msgid "Rotate 180 degrees"
-msgstr "Obróć o 180 stopni"
-
-#: models.py:91
-msgid "Tile"
-msgstr "Kafelki"
-
-#: models.py:92
-msgid "Scale"
-msgstr "Skaluj"
-
-#: models.py:102
-#, python-format
-msgid "Chain multiple filters using the following pattern \"FILTER_ONE->FILTER_TWO->FILTER_THREE\". Image filters will be applied in order. The following filter are available: %s."
-msgstr "Połącz wiele filtrów używając następującego wzorca: \"FILTR_PIERWSZY->FILTR_DRUGI->FILTR_TRZECI\". Filtry obrazów będą zastosowane w kolejności. Dostępne są następujące filtry: %s."
-
-#: models.py:107
-msgid "date published"
-msgstr "data publikacji"
-
-#: models.py:108
-#: models.py:164
-#: models.py:448
-msgid "title"
-msgstr "tytuł"
-
-#: models.py:109
-msgid "title slug"
-msgstr "tytuł - slug "
-
-#: models.py:110
-msgid "A \"slug\" is a unique URL-friendly title for an object."
-msgstr "\"Slug\" jest unikalnym, zgodnym z formatem dla URL-i tytułem obiektu."
-
-#: models.py:111
-#: models.py:166
-#: models.py:483
-msgid "description"
-msgstr "opis"
-
-#: models.py:112
-#: models.py:167
-#: models.py:453
-msgid "is public"
-msgstr "jest publiczna"
-
-#: models.py:113
-msgid "Public galleries will be displayed in the default views."
-msgstr "Galerie publiczne będą wyświetlana w domyślnych widokach."
-
-#: models.py:114
-#: models.py:460
-msgid "photos"
-msgstr "zdjęcia"
-
-#: models.py:116
-#: models.py:168
-#: models.py:454
-msgid "tags"
-msgstr "tagi"
-
-#: models.py:121
-msgid "gallery"
-msgstr "galeria"
-
-#: models.py:122
-msgid "galleries"
-msgstr "galerie"
-
-#: models.py:155
-msgid "count"
-msgstr "ilość"
-
-#: models.py:162
-msgid "images file (.zip)"
-msgstr "plik z obrazami (.zip)"
-
-#: models.py:163
-msgid "Select a .zip file of images to upload into a new Gallery."
-msgstr "Wybierz plik .zip zawierający zdjęcia które chcesz załadować do nowej Galerii."
-
-#: models.py:164
-msgid "All photos in the gallery will be given a title made up of the gallery title + a sequential number."
-msgstr "Wszystkie "
-
-#: models.py:165
-#: models.py:451
-msgid "caption"
-msgstr "podpis"
-
-#: models.py:165
-msgid "Caption will be added to all photos."
-msgstr "Podpis będzie dodany do wszystkich zdjęć."
-
-#: models.py:166
-msgid "A description of this Gallery."
-msgstr "Opis tej Galerii."
-
-#: models.py:167
-msgid "Uncheck this to make the uploaded gallery and included photographs private."
-msgstr "Odznacz aby uczynić wrzucaną galerię oraz zawarte w niej zdjęcia prywatnymi."
-
-#: models.py:171
-msgid "gallery upload"
-msgstr "wrzucona galeria"
-
-#: models.py:172
-msgid "gallery uploads"
-msgstr "wrzucone galerie"
-
-#: models.py:228
-#: models.py:594
-msgid "image"
-msgstr "obraz"
-
-#: models.py:229
-msgid "date taken"
-msgstr "data wykonania"
-
-#: models.py:231
-msgid "crop from"
-msgstr "obetnij z"
-
-#: models.py:232
-msgid "effect"
-msgstr "efekt"
-
-#: models.py:250
-msgid "An \"admin_thumbnail\" photo size has not been defined."
-msgstr "Rozmiar zdjęcia \"admin_thumbnail\" nie został zdefiniowany."
-
-#: models.py:258
-msgid "Thumbnail"
-msgstr "Miniaturka"
-
-#: models.py:449
-msgid "slug"
-msgstr "slug"
-
-#: models.py:452
-msgid "date added"
-msgstr "data dodania"
-
-#: models.py:453
-msgid "Public photographs will be displayed in the default views."
-msgstr "Publiczne zdjęcia będą wyświetlane w domyślnych widokach."
-
-#: models.py:459
-msgid "photo"
-msgstr "zdjęcie"
-
-#: models.py:482
-#: models.py:608
-msgid "name"
-msgstr "nazwa"
-
-#: models.py:554
-msgid "rotate or flip"
-msgstr "obróć lub odbij"
-
-#: models.py:555
-#: models.py:562
-msgid "color"
-msgstr "kolor"
-
-#: models.py:555
-msgid "A factor of 0.0 gives a black and white image, a factor of 1.0 gives the original image."
-msgstr "Współczynnik 0.0 daje czarno-biały obraz, współczynnik 1.0 daje obraz oryginalny."
-
-#: models.py:556
-msgid "brightness"
-msgstr "jasność"
-
-#: models.py:556
-msgid "A factor of 0.0 gives a black image, a factor of 1.0 gives the original image."
-msgstr "Współczynnik 0.0 daje czarny obraz, współczynnik 1.0 daje obraz oryginalny."
-
-#: models.py:557
-msgid "contrast"
-msgstr "kontrast"
-
-#: models.py:557
-msgid "A factor of 0.0 gives a solid grey image, a factor of 1.0 gives the original image."
-msgstr "Współczynnik 0.0 daje jednolity szary obraz, współczynnik 1.0 daje obraz oryginalny."
-
-#: models.py:558
-msgid "sharpness"
-msgstr "ostrość"
-
-#: models.py:558
-msgid "A factor of 0.0 gives a blurred image, a factor of 1.0 gives the original image."
-msgstr "Współczynnik 0.0 daje rozmazany obraz, współczynnik 1.0 daje obraz oryginalny."
-
-#: models.py:559
-msgid "filters"
-msgstr "filtry"
-
-#: models.py:560
-msgid "size"
-msgstr "rozmiar"
-
-#: models.py:560
-msgid "The height of the reflection as a percentage of the orignal image. A factor of 0.0 adds no reflection, a factor of 1.0 adds a reflection equal to the height of the orignal image."
-msgstr "Wysokość odbicia jako procent oryginalnego obrazu. Współczynnik 0.0 nie dodaje odbicia, współczynnik 1.0 dodaje odbicie równe wysokości oryginalnego obrazu."
-
-#: models.py:561
-msgid "strength"
-msgstr "intensywność"
-
-#: models.py:565
-#: models.py:616
-msgid "photo effect"
-msgstr "efekt zdjęcia"
-
-#: models.py:566
-msgid "photo effects"
-msgstr "efekty zdjęć"
-
-#: models.py:595
-msgid "style"
-msgstr "styl"
-
-#: models.py:596
-msgid "opacity"
-msgstr "przeźroczystość"
-
-#: models.py:596
-msgid "The opacity of the overlay."
-msgstr "Poziom przezroczystości"
-
-#: models.py:599
-msgid "watermark"
-msgstr "znak wodny"
-
-#: models.py:600
-msgid "watermarks"
-msgstr "znaki wodne"
-
-#: models.py:608
-msgid "Photo size name should contain only letters, numbers and underscores. Examples: \"thumbnail\", \"display\", \"small\", \"main_page_widget\"."
-msgstr "Nazwa rozmiaru zdjęcia powinna zawierać tylko litery, cyfry i podkreślenia. Przykłady: \"miniatura\", \"wystawa\", \"male\", \"widget_strony_glownej\"."
-
-#: models.py:609
-msgid "width"
-msgstr "szerokość"
-
-#: models.py:609
-msgid "If width is set to \"0\" the image will be scaled to the supplied height."
-msgstr "Jeśli szerokość jest ustawiona na \"0\" to obraz będzie skalowany do podanej wysokości."
-
-#: models.py:610
-msgid "height"
-msgstr "wysokość"
-
-#: models.py:610
-msgid "If height is set to \"0\" the image will be scaled to the supplied width"
-msgstr "Jeśli wysokość jest ustawiona na \"0\" to obraz będzie skalowany do podanej szerokości."
-
-#: models.py:611
-msgid "quality"
-msgstr "jakość"
-
-#: models.py:611
-msgid "JPEG image quality."
-msgstr "Jakość obrazu JPEG"
-
-#: models.py:612
-msgid "upscale images?"
-msgstr "skalować obrazy w górę?"
-
-#: models.py:612
-msgid "If selected the image will be scaled up if necessary to fit the supplied dimensions. Cropped sizes will be upscaled regardless of this setting."
-msgstr "Jeśli zaznaczone to obraz będzie skalowany w górę tak aby pasował do podanych wymiarów. Obcinane rozmiary będą skalowane niezależnie od tego ustawienia."
-
-#: models.py:613
-msgid "crop to fit?"
-msgstr "przyciąć aby pasował?"
-
-#: models.py:613
-msgid "If selected the image will be scaled and cropped to fit the supplied dimensions."
-msgstr "Jeśli zaznaczone to obraz będzie skalowany i przycinany tak aby pasował do podanych wymiarów."
-
-#: models.py:614
-msgid "pre-cache?"
-msgstr "wstępnie cachować?"
-
-#: models.py:614
-msgid "If selected this photo size will be pre-cached as photos are added."
-msgstr "Jesli zaznaczone to ten rozmiar zdjęć będzie wstępnie cachowany przy dodawaniu zdjęć."
-
-#: models.py:615
-msgid "increment view count?"
-msgstr "zwiększyć licznik odsłon?"
-
-#: models.py:615
-msgid "If selected the image's \"view_count\" will be incremented when this photo size is displayed."
-msgstr "Jeśli zaznaczone to \"licznik_odslon\" będzie zwiększany gdy ten rozmiar zdjęcia będzie wyświetlany."
-
-#: models.py:617
-msgid "watermark image"
-msgstr "oznacz kluczem wodnym"
-
-#: models.py:621
-msgid "photo size"
-msgstr "rozmiar zdjęcia"
-
-#: models.py:622
-msgid "photo sizes"
-msgstr "rozmiary zdjęć"
-
-#: models.py:640
-msgid "A PhotoSize must have a positive height or width."
-msgstr "PhotoSize musi mieć dodatnią wysokość i szerokość."
-
-#~ msgid "Leave to size the image to the set height"
-#~ msgstr "Ustaw aby przeskalować obraz do wybranej wysokości"
-#~ msgid "Leave to size the image to the set width"
-#~ msgstr "Ustaw aby przeskalować obraz do wybranej szerokości"
-#~ msgid "original image"
-#~ msgstr "oryginalny obraz"
-
--- a/madeira/photologue/management/__init__.py	Sat Apr 14 15:49:28 2012 -0500
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,1 +0,0 @@
-
--- a/madeira/photologue/management/commands/__init__.py	Sat Apr 14 15:49:28 2012 -0500
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,37 +0,0 @@
-from photologue.models import PhotoSize
-
-def get_response(msg, func=int, default=None):
-    while True:
-        resp = raw_input(msg)
-        if not resp and default is not None:
-            return default            
-        try:
-            return func(resp)
-        except:
-            print 'Invalid input.'
-            
-def create_photosize(name, width=0, height=0, crop=False, pre_cache=False, increment_count=False):
-    try:
-        size = PhotoSize.objects.get(name=name)
-        exists = True
-    except PhotoSize.DoesNotExist:
-        size = PhotoSize(name=name)
-        exists = False
-    if exists:
-        msg = 'A "%s" photo size already exists. Do you want to replace it? (yes, no):' % name
-        if not get_response(msg, lambda inp: inp == 'yes', False):
-            return
-    print '\nWe will now define the "%s" photo size:\n' % size
-    w = get_response('Width (in pixels):', lambda inp: int(inp), width)
-    h = get_response('Height (in pixels):', lambda inp: int(inp), height)
-    c = get_response('Crop to fit? (yes, no):', lambda inp: inp == 'yes', crop)
-    p = get_response('Pre-cache? (yes, no):', lambda inp: inp == 'yes', pre_cache)
-    i = get_response('Increment count? (yes, no):', lambda inp: inp == 'yes', increment_count)
-    size.width = w
-    size.height = h
-    size.crop = c
-    size.pre_cache = p
-    size.increment_count = i
-    size.save()
-    print '\nA "%s" photo size has been created.\n' % name
-    return size
\ No newline at end of file
--- a/madeira/photologue/management/commands/plcache.py	Sat Apr 14 15:49:28 2012 -0500
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,43 +0,0 @@
-from django.core.management.base import BaseCommand, CommandError
-from optparse import make_option
-from photologue.models import PhotoSize, ImageModel
-
-class Command(BaseCommand):
-    option_list = BaseCommand.option_list + (
-        make_option('--reset', '-r', action='store_true', dest='reset', help='Reset photo cache before generating'),
-    )
-
-    help = ('Manages Photologue cache file for the given sizes.')
-    args = '[sizes]'
-
-    requires_model_validation = True
-    can_import_settings = True
-
-    def handle(self, *args, **options):
-        return create_cache(args, options)
-
-def create_cache(sizes, options):
-    """
-    Creates the cache for the given files
-    """
-    reset = options.get('reset', None)
-    
-    size_list = [size.strip(' ,') for size in sizes]
-    
-    if len(size_list) < 1:
-        sizes = PhotoSize.objects.filter(pre_cache=True)
-    else:
-        sizes = PhotoSize.objects.filter(name__in=size_list)
-        
-    if not len(sizes):
-        raise CommandError('No photo sizes were found.')
-        
-    print 'Caching photos, this may take a while...'
-    
-    for cls in ImageModel.__subclasses__():
-        for photosize in sizes:
-            print 'Cacheing %s size images' % photosize.name
-            for obj in cls.objects.all():
-                if reset:
-                    obj.remove_size(photosize)
-                obj.create_size(photosize)
--- a/madeira/photologue/management/commands/plcreatesize.py	Sat Apr 14 15:49:28 2012 -0500
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,13 +0,0 @@
-from django.core.management.base import BaseCommand, CommandError
-from photologue.management.commands import create_photosize
-
-class Command(BaseCommand):  
-    help = ('Creates a new Photologue photo size interactively.')
-    requires_model_validation = True
-    can_import_settings = True
-
-    def handle(self, *args, **options):
-        create_size(args[0])
-
-def create_size(size):
-    create_photosize(size)
\ No newline at end of file
--- a/madeira/photologue/management/commands/plflush.py	Sat Apr 14 15:49:28 2012 -0500
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,35 +0,0 @@
-from django.core.management.base import BaseCommand, CommandError
-from optparse import make_option
-from photologue.models import PhotoSize, ImageModel
-
-class Command(BaseCommand):
-    help = ('Clears the Photologue cache for the given sizes.')
-    args = '[sizes]'
-
-    requires_model_validation = True
-    can_import_settings = True
-
-    def handle(self, *args, **options):
-        return create_cache(args, options)
-
-def create_cache(sizes, options):
-    """
-    Clears the cache for the given files
-    """    
-    size_list = [size.strip(' ,') for size in sizes]
-    
-    if len(size_list) < 1:
-        sizes = PhotoSize.objects.all()
-    else:
-        sizes = PhotoSize.objects.filter(name__in=size_list)
-        
-    if not len(sizes):
-        raise CommandError('No photo sizes were found.')
-        
-    print 'Flushing cache...'
-    
-    for cls in ImageModel.__subclasses__():
-        for photosize in sizes:
-            print 'Flushing %s size images' % photosize.name
-            for obj in cls.objects.all():
-                obj.remove_size(photosize)
--- a/madeira/photologue/management/commands/plinit.py	Sat Apr 14 15:49:28 2012 -0500
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,30 +0,0 @@
-from django.core.management.base import BaseCommand, CommandError
-from photologue.management.commands import get_response, create_photosize
-from photologue.models import PhotoEffect
-
-class Command(BaseCommand):  
-    help = ('Prompts the user to set up the default photo sizes required by Photologue.')
-    requires_model_validation = True
-    can_import_settings = True
-
-    def handle(self, *args, **kwargs):
-        return init(*args, **kwargs)
-
-def init(*args, **kwargs):
-    msg = '\nPhotologue requires a specific photo size to display thumbnail previews in the Django admin application.\nWould you like to generate this size now? (yes, no):'
-    if get_response(msg, lambda inp: inp == 'yes', False):
-        admin_thumbnail = create_photosize('admin_thumbnail', width=100, height=75, crop=True, pre_cache=True)
-        msg = 'Would you like to apply a sample enhancement effect to your admin thumbnails? (yes, no):'
-        if get_response(msg, lambda inp: inp == 'yes', False):
-            effect, created = PhotoEffect.objects.get_or_create(name='Enhance Thumbnail', description="Increases sharpness and contrast. Works well for smaller image sizes such as thumbnails.", contrast=1.2, sharpness=1.3)
-            admin_thumbnail.effect = effect
-            admin_thumbnail.save()                
-    msg = '\nPhotologue comes with a set of templates for setting up a complete photo gallery. These templates require you to define both a "thumbnail" and "display" size.\nWould you like to define them now? (yes, no):'
-    if get_response(msg, lambda inp: inp == 'yes', False):
-        thumbnail = create_photosize('thumbnail', width=100, height=75)
-        display = create_photosize('display', width=400, increment_count=True)
-        msg = 'Would you like to apply a sample reflection effect to your display images? (yes, no):'
-        if get_response(msg, lambda inp: inp == 'yes', False):
-            effect, created = PhotoEffect.objects.get_or_create(name='Display Reflection', description="Generates a reflection with a white background", reflection_size=0.4)
-            display.effect = effect
-            display.save()
\ No newline at end of file
--- a/madeira/photologue/models.py	Sat Apr 14 15:49:28 2012 -0500
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,700 +0,0 @@
-import os
-import random
-import shutil
-import zipfile
-
-from datetime import datetime
-from inspect import isclass
-
-from django.db import models
-from django.db.models.signals import post_init
-from django.conf import settings
-from django.core.files.base import ContentFile
-from django.core.urlresolvers import reverse
-from django.template.defaultfilters import slugify
-from django.utils.functional import curry
-from django.utils.translation import ugettext_lazy as _
-
-# Required PIL classes may or may not be available from the root namespace
-# depending on the installation method used.
-try:
-    import Image
-    import ImageFile
-    import ImageFilter
-    import ImageEnhance
-except ImportError:
-    try:
-        from PIL import Image
-        from PIL import ImageFile
-        from PIL import ImageFilter
-        from PIL import ImageEnhance
-    except ImportError:
-        raise ImportError(_('Photologue was unable to import the Python Imaging Library. Please confirm it`s installed and available on your current Python path.'))
-
-# attempt to load the django-tagging TagField from default location,
-# otherwise we substitude a dummy TagField.
-try:
-    from tagging.fields import TagField
-    tagfield_help_text = _('Separate tags with spaces, put quotes around multiple-word tags.')
-except ImportError:
-    class TagField(models.CharField):
-        def __init__(self, **kwargs):
-            default_kwargs = {'max_length': 255, 'blank': True}
-            default_kwargs.update(kwargs)
-            super(TagField, self).__init__(**default_kwargs)
-        def get_internal_type(self):
-            return 'CharField'
-    tagfield_help_text = _('Django-tagging was not found, tags will be treated as plain text.')
-
-from utils import EXIF
-from utils.reflection import add_reflection
-from utils.watermark import apply_watermark
-
-# Path to sample image
-SAMPLE_IMAGE_PATH = getattr(settings, 'SAMPLE_IMAGE_PATH', os.path.join(os.path.dirname(__file__), 'res', 'sample.jpg')) # os.path.join(settings.PROJECT_PATH, 'photologue', 'res', 'sample.jpg'
-
-# Modify image file buffer size.
-ImageFile.MAXBLOCK = getattr(settings, 'PHOTOLOGUE_MAXBLOCK', 256 * 2 ** 10)
-
-# Photologue image path relative to media root
-PHOTOLOGUE_DIR = getattr(settings, 'PHOTOLOGUE_DIR', 'photologue')
-
-# Look for user function to define file paths
-PHOTOLOGUE_PATH = getattr(settings, 'PHOTOLOGUE_PATH', None)
-if PHOTOLOGUE_PATH is not None:
-    if callable(PHOTOLOGUE_PATH):
-        get_storage_path = PHOTOLOGUE_PATH
-    else:
-        parts = PHOTOLOGUE_PATH.split('.')
-        module_name = '.'.join(parts[:-1])
-        module = __import__(module_name)
-        get_storage_path = getattr(module, parts[-1])
-else:
-    def get_storage_path(instance, filename):
-        return os.path.join(PHOTOLOGUE_DIR, 'photos', filename)
-
-# Quality options for JPEG images
-JPEG_QUALITY_CHOICES = (
-    (30, _('Very Low')),
-    (40, _('Low')),
-    (50, _('Medium-Low')),
-    (60, _('Medium')),
-    (70, _('Medium-High')),
-    (80, _('High')),
-    (90, _('Very High')),
-)
-
-# choices for new crop_anchor field in Photo
-CROP_ANCHOR_CHOICES = (
-    ('top', _('Top')),
-    ('right', _('Right')),
-    ('bottom', _('Bottom')),
-    ('left', _('Left')),
-    ('center', _('Center (Default)')),
-)
-
-IMAGE_TRANSPOSE_CHOICES = (
-    ('FLIP_LEFT_RIGHT', _('Flip left to right')),
-    ('FLIP_TOP_BOTTOM', _('Flip top to bottom')),
-    ('ROTATE_90', _('Rotate 90 degrees counter-clockwise')),
-    ('ROTATE_270', _('Rotate 90 degrees clockwise')),
-    ('ROTATE_180', _('Rotate 180 degrees')),
-)
-
-WATERMARK_STYLE_CHOICES = (
-    ('tile', _('Tile')),
-    ('scale', _('Scale')),
-)
-
-# Prepare a list of image filters
-filter_names = []
-for n in dir(ImageFilter):
-    klass = getattr(ImageFilter, n)
-    if isclass(klass) and issubclass(klass, ImageFilter.BuiltinFilter) and \
-        hasattr(klass, 'name'):
-            filter_names.append(klass.__name__)
-IMAGE_FILTERS_HELP_TEXT = _('Chain multiple filters using the following pattern "FILTER_ONE->FILTER_TWO->FILTER_THREE". Image filters will be applied in order. The following filters are available: %s.' % (', '.join(filter_names)))
-
-
-class Gallery(models.Model):
-    date_added = models.DateTimeField(_('date published'), default=datetime.now)
-    title = models.CharField(_('title'), max_length=100, unique=True)
-    title_slug = models.SlugField(_('title slug'), unique=True,
-                                  help_text=_('A "slug" is a unique URL-friendly title for an object.'))
-    description = models.TextField(_('description'), blank=True)
-    is_public = models.BooleanField(_('is public'), default=True,
-                                    help_text=_('Public galleries will be displayed in the default views.'))
-    photos = models.ManyToManyField('Photo', related_name='galleries', verbose_name=_('photos'),
-                                    null=True, blank=True)
-    tags = TagField(help_text=tagfield_help_text, verbose_name=_('tags'))
-
-    class Meta:
-        ordering = ['-date_added']
-        get_latest_by = 'date_added'
-        verbose_name = _('gallery')
-        verbose_name_plural = _('galleries')
-
-    def __unicode__(self):
-        return self.title
-
-    def __str__(self):
-        return self.__unicode__()
-
-    def get_absolute_url(self):
-        return reverse('pl-gallery', args=[self.title_slug])
-
-    def latest(self, limit=0, public=True):
-        if limit == 0:
-            limit = self.photo_count()
-        if public:
-            return self.public()[:limit]
-        else:
-            return self.photos.all()[:limit]
-
-    def sample(self, count=0, public=True):
-        if count == 0 or count > self.photo_count():
-            count = self.photo_count()
-        if public:
-            photo_set = self.public()
-        else:
-            photo_set = self.photos.all()
-        return random.sample(photo_set, count)
-
-    def photo_count(self, public=True):
-        if public:
-            return self.public().count()
-        else:
-            return self.photos.all().count()
-    photo_count.short_description = _('count')
-
-    def public(self):
-        return self.photos.filter(is_public=True)
-
-
-class GalleryUpload(models.Model):
-    zip_file = models.FileField(_('images file (.zip)'), upload_to=PHOTOLOGUE_DIR+"/temp",
-                                help_text=_('Select a .zip file of images to upload into a new Gallery.'))
-    gallery = models.ForeignKey(Gallery, null=True, blank=True, help_text=_('Select a gallery to add these images to. leave this empty to create a new gallery from the supplied title.'))
-    title = models.CharField(_('title'), max_length=75, help_text=_('All photos in the gallery will be given a title made up of the gallery title + a sequential number.'))
-    caption = models.TextField(_('caption'), blank=True, help_text=_('Caption will be added to all photos.'))
-    description = models.TextField(_('description'), blank=True, help_text=_('A description of this Gallery.'))
-    is_public = models.BooleanField(_('is public'), default=True, help_text=_('Uncheck this to make the uploaded gallery and included photographs private.'))
-    tags = models.CharField(max_length=255, blank=True, help_text=tagfield_help_text, verbose_name=_('tags'))
-
-    class Meta:
-        verbose_name = _('gallery upload')
-        verbose_name_plural = _('gallery uploads')
-
-    def save(self):
-        super(GalleryUpload, self).save()
-        self.process_zipfile()
-        super(GalleryUpload, self).delete()
-
-    def process_zipfile(self):
-        if os.path.isfile(self.zip_file.path):
-            # TODO: implement try-except here
-            zip = zipfile.ZipFile(self.zip_file.path)
-            bad_file = zip.testzip()
-            if bad_file:
-                raise Exception('"%s" in the .zip archive is corrupt.' % bad_file)
-            count = 1
-            if self.gallery:
-                gallery = self.gallery
-            else:
-                gallery = Gallery.objects.create(title=self.title,
-                                                 title_slug=slugify(self.title),
-                                                 description=self.description,
-                                                 is_public=self.is_public,
-                                                 tags=self.tags)
-            from cStringIO import StringIO
-            for filename in zip.namelist():
-                if filename.startswith('__'): # do not process meta files
-                    continue
-                data = zip.read(filename)
-                if len(data):
-                    try:
-                        # the following is taken from django.newforms.fields.ImageField:
-                        #  load() is the only method that can spot a truncated JPEG,
-                        #  but it cannot be called sanely after verify()
-                        trial_image = Image.open(StringIO(data))
-                        trial_image.load()
-                        # verify() is the only method that can spot a corrupt PNG,
-                        #  but it must be called immediately after the constructor
-                        trial_image = Image.open(StringIO(data))
-                        trial_image.verify()
-                    except Exception:
-                        # if a "bad" file is found we just skip it.
-                        continue
-                    while 1:
-                        title = ' '.join([self.title, str(count)])
-                        slug = slugify(title)
-                        try:
-                            p = Photo.objects.get(title_slug=slug)
-                        except Photo.DoesNotExist:                            
-                            photo = Photo(title=title, title_slug=slug,
-                                          caption=self.caption,
-                                          is_public=self.is_public,
-                                          tags=self.tags)
-                            photo.image.save(filename, ContentFile(data))
-                            gallery.photos.add(photo)
-                            count = count + 1
-                            break
-                        count = count + 1
-            zip.close()
-
-
-class ImageModel(models.Model):
-    image = models.ImageField(_('image'), upload_to=get_storage_path)
-    date_taken = models.DateTimeField(_('date taken'), null=True, blank=True, editable=False)
-    view_count = models.PositiveIntegerField(default=0, editable=False)
-    crop_from = models.CharField(_('crop from'), blank=True, max_length=10, default='center', choices=CROP_ANCHOR_CHOICES)
-    effect = models.ForeignKey('PhotoEffect', null=True, blank=True, related_name="%(class)s_related", verbose_name=_('effect'))
-
-    class Meta:
-        abstract = True
-
-    @property
-    def EXIF(self):
-        try:
-            return EXIF.process_file(open(self.image.path, 'rb'))
-        except:
-            try:
-                return EXIF.process_file(open(self.image.path, 'rb'), details=False)
-            except:
-                return {}
-
-    def admin_thumbnail(self):
-        func = getattr(self, 'get_admin_thumbnail_url', None)
-        if func is None:
-            return _('An "admin_thumbnail" photo size has not been defined.')
-        else:
-            if hasattr(self, 'get_absolute_url'):
-                return u'<a href="%s"><img src="%s"></a>' % \
-                    (self.get_absolute_url(), func())
-            else:
-                return u'<a href="%s"><img src="%s"></a>' % \
-                    (self.image.url, func())
-    admin_thumbnail.short_description = _('Thumbnail')
-    admin_thumbnail.allow_tags = True
-
-    def cache_path(self):
-        return os.path.join(os.path.dirname(self.image.path), "cache")
-
-    def cache_url(self):
-        return '/'.join([os.path.dirname(self.image.url), "cache"])
-
-    def image_filename(self):
-        return os.path.basename(self.image.path)
-
-    def _get_filename_for_size(self, size):
-        size = getattr(size, 'name', size)
-        base, ext = os.path.splitext(self.image_filename())
-        return ''.join([base, '_', size, ext])
-
-    def _get_SIZE_photosize(self, size):
-        return PhotoSizeCache().sizes.get(size)
-
-    def _get_SIZE_size(self, size):
-        photosize = PhotoSizeCache().sizes.get(size)
-        if not self.size_exists(photosize):
-            self.create_size(photosize)
-        return Image.open(self._get_SIZE_filename(size)).size
-
-    def _get_SIZE_url(self, size):
-        photosize = PhotoSizeCache().sizes.get(size)
-        if not self.size_exists(photosize):
-            self.create_size(photosize)
-        if photosize.increment_count:
-            self.view_count += 1
-            self.save(update=True)
-        return '/'.join([self.cache_url(), self._get_filename_for_size(photosize.name)])
-
-    def _get_SIZE_filename(self, size):
-        photosize = PhotoSizeCache().sizes.get(size)
-        return os.path.join(self.cache_path(),
-                            self._get_filename_for_size(photosize.name))
-
-    def add_accessor_methods(self, *args, **kwargs):
-        for size in PhotoSizeCache().sizes.keys():
-            setattr(self, 'get_%s_size' % size,
-                    curry(self._get_SIZE_size, size=size))
-            setattr(self, 'get_%s_photosize' % size,
-                    curry(self._get_SIZE_photosize, size=size))
-            setattr(self, 'get_%s_url' % size,
-                    curry(self._get_SIZE_url, size=size))
-            setattr(self, 'get_%s_filename' % size,
-                    curry(self._get_SIZE_filename, size=size))
-
-    def size_exists(self, photosize):
-        func = getattr(self, "get_%s_filename" % photosize.name, None)
-        if func is not None:
-            if os.path.isfile(func()):
-                return True
-        return False
-
-    def resize_image(self, im, photosize):
-        cur_width, cur_height = im.size
-        new_width, new_height = photosize.size
-        if photosize.crop:
-            ratio = max(float(new_width)/cur_width,float(new_height)/cur_height)
-            x = (cur_width * ratio)
-            y = (cur_height * ratio)
-            xd = abs(new_width - x)
-            yd = abs(new_height - y)
-            x_diff = int(xd / 2)
-            y_diff = int(yd / 2)
-            if self.crop_from == 'top':
-                box = (int(x_diff), 0, int(x_diff+new_width), new_height)
-            elif self.crop_from == 'left':
-                box = (0, int(y_diff), new_width, int(y_diff+new_height))
-            elif self.crop_from == 'bottom':
-                box = (int(x_diff), int(yd), int(x_diff+new_width), int(y)) # y - yd = new_height
-            elif self.crop_from == 'right':
-                box = (int(xd), int(y_diff), int(x), int(y_diff+new_height)) # x - xd = new_width
-            else:
-                box = (int(x_diff), int(y_diff), int(x_diff+new_width), int(y_diff+new_height))
-            im = im.resize((int(x), int(y)), Image.ANTIALIAS).crop(box)
-        else:
-            if not new_width == 0 and not new_height == 0:
-                ratio = min(float(new_width)/cur_width,
-                            float(new_height)/cur_height)
-            else:
-                if new_width == 0:
-                    ratio = float(new_height)/cur_height
-                else:
-                    ratio = float(new_width)/cur_width
-            new_dimensions = (int(round(cur_width*ratio)),
-                              int(round(cur_height*ratio)))
-            if new_dimensions[0] > cur_width or \
-               new_dimensions[1] > cur_height:
-                if not photosize.upscale:
-                    return im
-            im = im.resize(new_dimensions, Image.ANTIALIAS)
-        return im
-
-    def create_size(self, photosize):
-        if self.size_exists(photosize):
-            return
-        if not os.path.isdir(self.cache_path()):
-            os.makedirs(self.cache_path())
-        try:
-            im = Image.open(self.image.path)
-        except IOError:
-            return
-        # Apply effect if found
-        if self.effect is not None:
-            im = self.effect.pre_process(im)
-        elif photosize.effect is not None:
-            im = photosize.effect.pre_process(im)
-        # Resize/crop image
-        if im.size != photosize.size:
-            im = self.resize_image(im, photosize)
-        # Apply watermark if found
-        if photosize.watermark is not None:
-            im = photosize.watermark.post_process(im)
-        # Apply effect if found
-        if self.effect is not None:
-            im = self.effect.post_process(im)
-        elif photosize.effect is not None:
-            im = photosize.effect.post_process(im)
-        # Save file
-        im_filename = getattr(self, "get_%s_filename" % photosize.name)()
-        try:
-            if im.format == 'JPEG':
-                im.save(im_filename, 'JPEG', quality=int(photosize.quality), optimize=True)
-            else:
-                im.save(im_filename)
-        except IOError, e:
-            if os.path.isfile(im_filename):
-                os.unlink(im_filename)
-            raise e
-
-    def remove_size(self, photosize, remove_dirs=True):
-        if not self.size_exists(photosize):
-            return
-        filename = getattr(self, "get_%s_filename" % photosize.name)()
-        if os.path.isfile(filename):
-            os.remove(filename)
-        if remove_dirs:
-            self.remove_cache_dirs()
-
-    def clear_cache(self):
-        cache = PhotoSizeCache()
-        for photosize in cache.sizes.values():
-            self.remove_size(photosize, False)
-        self.remove_cache_dirs()
-
-    def pre_cache(self):
-        cache = PhotoSizeCache()
-        for photosize in cache.sizes.values():
-            if photosize.pre_cache:
-                self.create_size(photosize)
-
-    def remove_cache_dirs(self):
-        try:
-            os.removedirs(self.cache_path())
-        except:
-            pass
-
-    def save(self, update=False):
-        if update:
-            models.Model.save(self)
-            return
-        if self.date_taken is None:
-            try:
-                exif_date = self.EXIF.get('EXIF DateTimeOriginal', None)
-                if exif_date is not None:
-                    d, t = str.split(exif_date.values)
-                    year, month, day = d.split(':')
-                    hour, minute, second = t.split(':')
-                    self.date_taken = datetime(int(year), int(month), int(day),
-                                               int(hour), int(minute), int(second))
-            except:
-                pass
-        if self.date_taken is None:
-            self.date_taken = datetime.now()
-        if self._get_pk_val():
-            self.clear_cache()
-        super(ImageModel, self).save()
-        self.pre_cache()
-
-    def delete(self):
-        self.clear_cache()
-        super(ImageModel, self).delete()
-
-
-class Photo(ImageModel):
-    title = models.CharField(_('title'), max_length=100, unique=True)
-    title_slug = models.SlugField(_('slug'), unique=True,
-                                  help_text=('A "slug" is a unique URL-friendly title for an object.'))
-    caption = models.TextField(_('caption'), blank=True)
-    date_added = models.DateTimeField(_('date added'), default=datetime.now, editable=False)
-    is_public = models.BooleanField(_('is public'), default=True, help_text=_('Public photographs will be displayed in the default views.'))
-    tags = TagField(help_text=tagfield_help_text, verbose_name=_('tags'))
-
-    class Meta:
-        ordering = ['-date_added']
-        get_latest_by = 'date_added'
-        verbose_name = _("photo")
-        verbose_name_plural = _("photos")
-
-    def __unicode__(self):
-        return self.title
-
-    def __str__(self):
-        return self.__unicode__()
-
-    def save(self, update=False):
-        if self.title_slug is None:
-            self.title_slug = slugify(self.title)
-        super(Photo, self).save(update)
-
-    def get_absolute_url(self):
-        return reverse('pl-photo', args=[self.title_slug])
-
-    def public_galleries(self):
-        """Return the public galleries to which this photo belongs."""
-        return self.galleries.filter(is_public=True)
-
-
-class BaseEffect(models.Model):
-    name = models.CharField(_('name'), max_length=30, unique=True)
-    description = models.TextField(_('description'), blank=True)
-
-    class Meta:
-        abstract = True
-
-    def sample_dir(self):
-        return os.path.join(settings.MEDIA_ROOT, PHOTOLOGUE_DIR, 'samples')
-
-    def sample_url(self):
-        return settings.MEDIA_URL + '/'.join([PHOTOLOGUE_DIR, 'samples', '%s %s.jpg' % (self.name.lower(), 'sample')])
-
-    def sample_filename(self):
-        return os.path.join(self.sample_dir(), '%s %s.jpg' % (self.name.lower(), 'sample'))
-
-    def create_sample(self):
-        if not os.path.isdir(self.sample_dir()):
-            os.makedirs(self.sample_dir())
-        try:
-            im = Image.open(SAMPLE_IMAGE_PATH)
-        except IOError:
-            raise IOError('Photologue was unable to open the sample image: %s.' % SAMPLE_IMAGE_PATH)
-        im = self.process(im)
-        im.save(self.sample_filename(), 'JPEG', quality=90, optimize=True)
-
-    def admin_sample(self):
-        return u'<img src="%s">' % self.sample_url()
-    admin_sample.short_description = 'Sample'
-    admin_sample.allow_tags = True
-
-    def pre_process(self, im):
-        return im
-
-    def post_process(self, im):
-        return im
-
-    def process(self, im):
-        im = self.pre_process(im)
-        im = self.post_process(im)
-        return im
-
-    def __unicode__(self):
-        return self.name
-
-    def __str__(self):
-        return self.__unicode__()
-
-    def save(self):
-        try:
-            os.remove(self.sample_filename())
-        except:
-            pass
-        models.Model.save(self)
-        self.create_sample()
-        for size in self.photo_sizes.all():
-            size.clear_cache()
-        # try to clear all related subclasses of ImageModel
-        for prop in [prop for prop in dir(self) if prop[-8:] == '_related']:
-            for obj in getattr(self, prop).all():
-                obj.clear_cache()
-                obj.pre_cache()
-
-    def delete(self):
-        try:
-            os.remove(self.sample_filename())
-        except:
-            pass
-        super(PhotoEffect, self).delete()
-
-
-class PhotoEffect(BaseEffect):
-    """ A pre-defined effect to apply to photos """
-    transpose_method = models.CharField(_('rotate or flip'), max_length=15, blank=True, choices=IMAGE_TRANSPOSE_CHOICES)
-    color = models.FloatField(_('color'), default=1.0, help_text=_("A factor of 0.0 gives a black and white image, a factor of 1.0 gives the original image."))
-    brightness = models.FloatField(_('brightness'), default=1.0, help_text=_("A factor of 0.0 gives a black image, a factor of 1.0 gives the original image."))
-    contrast = models.FloatField(_('contrast'), default=1.0, help_text=_("A factor of 0.0 gives a solid grey image, a factor of 1.0 gives the original image."))
-    sharpness = models.FloatField(_('sharpness'), default=1.0, help_text=_("A factor of 0.0 gives a blurred image, a factor of 1.0 gives the original image."))
-    filters = models.CharField(_('filters'), max_length=200, blank=True, help_text=_(IMAGE_FILTERS_HELP_TEXT))
-    reflection_size = models.FloatField(_('size'), default=0, help_text=_("The height of the reflection as a percentage of the orignal image. A factor of 0.0 adds no reflection, a factor of 1.0 adds a reflection equal to the height of the orignal image."))
-    reflection_strength = models.FloatField(_('strength'), default=0.6, help_text="The initial opacity of the reflection gradient.")
-    background_color = models.CharField(_('color'), max_length=7, default="#FFFFFF", help_text="The background color of the reflection gradient. Set this to match the background color of your page.")
-
-    class Meta:
-        verbose_name = _("photo effect")
-        verbose_name_plural = _("photo effects")
-
-    def pre_process(self, im):
-        if self.transpose_method != '':
-            method = getattr(Image, self.transpose_method)
-            im = im.transpose(method)
-        if im.mode != 'RGB' and im.mode != 'RGBA':
-            return im
-        for name in ['Color', 'Brightness', 'Contrast', 'Sharpness']:
-            factor = getattr(self, name.lower())
-            if factor != 1.0:
-                im = getattr(ImageEnhance, name)(im).enhance(factor)
-        for name in self.filters.split('->'):
-            image_filter = getattr(ImageFilter, name.upper(), None)
-            if image_filter is not None:
-                try:
-                    im = im.filter(image_filter)
-                except ValueError:
-                    pass
-        return im
-
-    def post_process(self, im):
-        if self.reflection_size != 0.0:
-            im = add_reflection(im, bgcolor=self.background_color, amount=self.reflection_size, opacity=self.reflection_strength)
-        return im
-
-
-class Watermark(BaseEffect):
-    image = models.ImageField(_('image'), upload_to=PHOTOLOGUE_DIR+"/watermarks")
-    style = models.CharField(_('style'), max_length=5, choices=WATERMARK_STYLE_CHOICES, default='scale')
-    opacity = models.FloatField(_('opacity'), default=1, help_text=_("The opacity of the overlay."))
-
-    class Meta:
-        verbose_name = _('watermark')
-        verbose_name_plural = _('watermarks')
-
-    def post_process(self, im):
-        mark = Image.open(self.image.path)
-        return apply_watermark(im, mark, self.style, self.opacity)
-
-
-class PhotoSize(models.Model):
-    name = models.CharField(_('name'), max_length=20, unique=True, help_text=_('Photo size name should contain only letters, numbers and underscores. Examples: "thumbnail", "display", "small", "main_page_widget".'))
-    width = models.PositiveIntegerField(_('width'), default=0, help_text=_('If width is set to "0" the image will be scaled to the supplied height.'))
-    height = models.PositiveIntegerField(_('height'), default=0, help_text=_('If height is set to "0" the image will be scaled to the supplied width'))
-    quality = models.PositiveIntegerField(_('quality'), choices=JPEG_QUALITY_CHOICES, default=70, help_text=_('JPEG image quality.'))
-    upscale = models.BooleanField(_('upscale images?'), default=False, help_text=_('If selected the image will be scaled up if necessary to fit the supplied dimensions. Cropped sizes will be upscaled regardless of this setting.'))
-    crop = models.BooleanField(_('crop to fit?'), default=False, help_text=_('If selected the image will be scaled and cropped to fit the supplied dimensions.'))
-    pre_cache = models.BooleanField(_('pre-cache?'), default=False, help_text=_('If selected this photo size will be pre-cached as photos are added.'))
-    increment_count = models.BooleanField(_('increment view count?'), default=False, help_text=_('If selected the image\'s "view_count" will be incremented when this photo size is displayed.'))
-    effect = models.ForeignKey('PhotoEffect', null=True, blank=True, related_name='photo_sizes', verbose_name=_('photo effect'))
-    watermark = models.ForeignKey('Watermark', null=True, blank=True, related_name='photo_sizes', verbose_name=_('watermark image'))
-
-    class Meta:
-        ordering = ['width', 'height']
-        verbose_name = _('photo size')
-        verbose_name_plural = _('photo sizes')
-
-    def __unicode__(self):
-        return self.name
-
-    def __str__(self):
-        return self.__unicode__()
-
-    def clear_cache(self):
-        for cls in ImageModel.__subclasses__():
-            for obj in cls.objects.all():
-                obj.remove_size(self)
-                if self.pre_cache:
-                    obj.create_size(self)
-        PhotoSizeCache().reset()
-
-    def save(self):
-        if self.width + self.height <= 0:
-            raise ValueError(_('A PhotoSize must have a positive height or width.'))
-        super(PhotoSize, self).save()
-        PhotoSizeCache().reset()
-        self.clear_cache()
-
-    def delete(self):
-        self.clear_cache()
-        super(PhotoSize, self).delete()
-
-    def _get_size(self):
-        return (self.width, self.height)
-    def _set_size(self, value):
-        self.width, self.height = value
-    size = property(_get_size, _set_size)
-
-
-class PhotoSizeCache(object):
-    __state = {"sizes": {}}
-
-    def __init__(self):
-        self.__dict__ = self.__state
-        if not len(self.sizes):
-            sizes = PhotoSize.objects.all()
-            for size in sizes:
-                self.sizes[size.name] = size
-
-    def reset(self):
-        self.sizes = {}
-
-
-# Set up the accessor methods
-def add_methods(sender, instance, signal, *args, **kwargs):
-    """ Adds methods to access sized images (urls, paths)
-
-    after the Photo model's __init__ function completes,
-    this method calls "add_accessor_methods" on each instance.
-    """
-    if hasattr(instance, 'add_accessor_methods'):
-        instance.add_accessor_methods()
-
-# connect the add_accessor_methods function to the post_init signal
-post_init.connect(add_methods)
Binary file madeira/photologue/res/sample.jpg has changed
Binary file madeira/photologue/res/test_landscape.jpg has changed
Binary file madeira/photologue/res/test_portrait.jpg has changed
Binary file madeira/photologue/res/test_square.jpg has changed
--- a/madeira/photologue/templates/photologue/gallery_archive.html	Sat Apr 14 15:49:28 2012 -0500
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,26 +0,0 @@
-{% extends "photologue/root.html" %}
-
-{% block title %}Latest Photo Galleries{% endblock %}
-
-{% block content %}
-
-<h1>Latest Photo Galleries</h1>
-
-{% if latest %}
-    {% for gallery in latest %}
-    <div class="photo-gallery">
-        <h2><a href="{{ gallery.get_absolute_url }}">{{ gallery.title }}</a></h2>
-        {% for photo in gallery.sample|slice:sample_size %}
-        <div class="gallery-photo">
-            <a href="{{ photo.get_absolute_url }}"><img src="{{ photo.get_thumbnail_url }}" alt="{{ photo.title }}"/></a>
-        </div>
-        {% endfor %}
-    </div>
-    {% endfor %}
-{% else %}
-    <p>No galleries were found.</p>
-{% endif %}
-
-<p><a href="{% url pl-gallery-list 1 %}">View all galleries.</a></p>
-
-{% endblock %}
--- a/madeira/photologue/templates/photologue/gallery_archive_day.html	Sat Apr 14 15:49:28 2012 -0500
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,26 +0,0 @@
-{% extends "photologue/root.html" %}
-
-{% block title %}Galleries for {{ day|date }}{% endblock %}
-
-{% block content %}
-
-<h1>Galleries for {{ day|date }}</h1>
-
-{% if object_list %}
-    {% for gallery in object_list %}
-    <div class="photo-gallery">
-        <h2>{{ gallery.title }}</h2>
-        {% for photo in gallery.sample|slice:sample_size %}
-        <div class="gallery-photo">
-            <a href="{{ photo.get_absolute_url }}"><img src="{{ photo.get_thumbnail_url }}" alt="{{ photo.title }}"/></a>
-        </div>
-        {% endfor %}
-    </div>
-    {% endfor %}
-{% else %}
-    <p>No galleries were found.</p>
-{% endif %}
-
-<p><a href="{% url pl-gallery-list 1 %}">View all galleries.</a></p>
-
-{% endblock %}
--- a/madeira/photologue/templates/photologue/gallery_archive_month.html	Sat Apr 14 15:49:28 2012 -0500
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,26 +0,0 @@
-{% extends "photologue/root.html" %}
-
-{% block title %}Galleries for {{ month|date:"F Y" }}{% endblock %}
-
-{% block content %}
-
-<h1>Galleries for {{ month|date:"F Y" }}</h1>
-
-{% if object_list %}
-    {% for gallery in object_list %}
-    <div class="photo-gallery">
-        <h2>{{ gallery.title }}</h2>
-        {% for photo in gallery.sample|slice:sample_size %}
-        <div class="gallery-photo">
-            <a href="{{ photo.get_absolute_url }}"><img src="{{ photo.get_thumbnail_url }}" alt="{{ photo.title }}"/></a>
-        </div>
-        {% endfor %}
-    </div>
-    {% endfor %}
-{% else %}
-    <p>No galleries were found.</p>
-{% endif %}
-
-<p><a href="{% url pl-gallery-list 1 %}">View all galleries.</a></p>
-
-{% endblock %}
--- a/madeira/photologue/templates/photologue/gallery_archive_year.html	Sat Apr 14 15:49:28 2012 -0500
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,16 +0,0 @@
-{% extends "photologue/root.html" %}
-
-{% block title %}Galleries for {{ year }}{% endblock %}
-
-{% block content %}
-
-<h1>Galleries for {{ year }}</h1>
-<ul>
-{% for date in date_list %}
-<li><a href="{{ date|date:"M"|lower }}/">{{ date|date:"F" }}</a></li>
-{% endfor %}
-</ul>
-
-<p><a href="{% url pl-gallery-list 1 %}">View all galleries.</a></p>
-
-{% endblock %}
--- a/madeira/photologue/templates/photologue/gallery_detail.html	Sat Apr 14 15:49:28 2012 -0500
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,19 +0,0 @@
-{% extends "photologue/root.html" %}
-
-{% block title %}{{ object.title }}{% endblock %}
-
-{% block content %}
-
-<h1>{{ object.title }}</h1>
-<h2>Originally published {{ object.date_added|date:"l, F jS, Y" }}</h2>
-{% if object.description %}<p>{{ object.description }}</p>{% endif %}
-<div class="photo-gallery">
-    {% for photo in object.public %}
-    <div class="gallery-photo">
-        <a href="{{ photo.get_absolute_url }}"><img src="{{ photo.get_thumbnail_url }}" alt="{{ photo.title }}"/></a>
-    </div>
-    {% endfor %}
-</div>
-<p><a href="{% url pl-gallery-list 1 %}">View all galleries</a></p>
-
-{% endblock %}
--- a/madeira/photologue/templates/photologue/gallery_list.html	Sat Apr 14 15:49:28 2012 -0500
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,31 +0,0 @@
-{% extends "photologue/root.html" %}
-
-{% block title %}All Galleries{% endblock %}
-
-{% block content %}
-
-<h1>All galleries</h1>
-
-{% if object_list %}
-    {% for gallery in object_list %}
-    <div class="photo-gallery">
-        <h2><a href="{{ gallery.get_absolute_url }}">{{ gallery.title }}</a></h2>
-        {% for photo in gallery.sample|slice:sample_size %}
-        <div class="gallery-photo">
-            <a href="{{ photo.get_absolute_url }}"><img src="{{ photo.get_thumbnail_url }}" alt="{{ photo.title }}"/></a>
-        </div>
-        {% endfor %}
-    </div>
-    {% endfor %}
-{% else %}
-    <p>No galleries were found.</p>
-{% endif %}
-
-{% if is_paginated %}
-<p>{{ hits }} galleries total.</p>
-<div id="page_controls">
-    <p>{% if has_previous %}<a href="{% url pl-gallery-list previous %}">Previous</a> | {% endif %} page {{ page }} of {{ pages }} {% if has_next %}| <a href="{% url pl-gallery-list next %}">Next</a>{% endif %}</p>
-</div>
-{% endif %}
-
-{% endblock %}
--- a/madeira/photologue/templates/photologue/photo_archive.html	Sat Apr 14 15:49:28 2012 -0500
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,20 +0,0 @@
-{% extends "photologue/root.html" %}
-
-{% block title %}Latest Photos{% endblock %}
-
-{% block content %}
-
-<h1>Latest Photos</h1>
-
-{% if latest %}
-    {% for photo in latest %}
-    <div class="gallery-photo">
-        <a href="{{ photo.get_absolute_url }}"><img src="{{ photo.get_thumbnail_url }}" alt="{{ photo.title }}"/></a>
-    </div>
-    {% endfor %}
-{% else %}
-<p>No photos were found.</p>
-{% endif %}
-<p><a href="{% url pl-photo-list 1 %}">View all photographs</a></p>
-
-{% endblock %}
--- a/madeira/photologue/templates/photologue/photo_archive_day.html	Sat Apr 14 15:49:28 2012 -0500
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,20 +0,0 @@
-{% extends "photologue/root.html" %}
-
-{% block title %}Photos for {{ day|date }}{% endblock %}
-
-{% block content %}
-
-<h1>Photos for {{ day|date }}</h1>
-
-{% if object_list %}
-    {% for photo in object_list %}
-    <div class="gallery-photo">
-        <a href="{{ photo.get_absolute_url }}"><img src="{{ photo.get_thumbnail_url }}" alt="{{ photo.title }}"/></a>
-    </div>
-    {% endfor %}
-{% else %}
-<p>No photos were found.</p>
-{% endif %}
-<p><a href="{% url pl-photo-list 1 %}">View all photographs</a></p>
-
-{% endblock %}
--- a/madeira/photologue/templates/photologue/photo_archive_month.html	Sat Apr 14 15:49:28 2012 -0500
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,20 +0,0 @@
-{% extends "photologue/root.html" %}
-
-{% block title %}Photos for {{ month|date:"F Y" }}{% endblock %}
-
-{% block content %}
-
-<h1>Photos for {{ month|date:"F Y" }}</h1>
-
-{% if object_list %}
-    {% for photo in object_list %}
-    <div class="gallery-photo">
-        <a href="{{ photo.get_absolute_url }}"><img src="{{ photo.get_thumbnail_url }}" alt="{{ photo.title }}"/></a>
-    </div>
-    {% endfor %}
-{% else %}
-<p>No photos were found.</p>
-{% endif %}
-<p><a href="{% url pl-photo-list 1 %}">View all photographs</a></p>
-
-{% endblock %}
--- a/madeira/photologue/templates/photologue/photo_archive_year.html	Sat Apr 14 15:49:28 2012 -0500
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,14 +0,0 @@
-{% extends "photologue/root.html" %}
-
-{% block title %}Galleries for {{ year }}{% endblock %}
-
-{% block content %}
-
-<h1>Photos for {{ year }}</h1>
-<ul>
-{% for date in date_list %}
-<li><a href="{{ date|date:"M"|lower }}/">{{ date|date:"F" }}</a></li>
-{% endfor %}
-</ul>
-
-{% endblock %}
--- a/madeira/photologue/templates/photologue/photo_detail.html	Sat Apr 14 15:49:28 2012 -0500
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,21 +0,0 @@
-{% extends "photologue/root.html" %}
-
-{% block title %}{{ object.title }}{% endblock %}
-
-{% block content %}
-
-<h1>{{ object.title }}</h1>
-<div class="gallery-photo">
-    <a href="{{ object.image.url }}"><img src="{{ object.get_display_url }}" alt="{{ object.title }}"/></a>
-    {% if object.caption %}<p>{{ object.caption }}</p>{% endif %}
-</div>
-{% if object.public_galleries %}
-<h2>This photo is found in the following galleries:</h2>
-<ol>
-{% for gallery in object.public_galleries %}
-    <li><a href="{{ gallery.get_absolute_url }}">{{ gallery.title }}</a></li>
-{% endfor %}
-</ol>
-{% endif %}
-
-{% endblock %}
--- a/madeira/photologue/templates/photologue/photo_list.html	Sat Apr 14 15:49:28 2012 -0500
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,26 +0,0 @@
-{% extends "photologue/root.html" %}
-
-{% block title %}All Photos{% endblock %}
-
-{% block content %}
-
-<h1>All Photos</h1>
-
-{% if object_list %}
-    {% for photo in object_list %}
-    <div class="gallery-photo">
-        <a href="{{ photo.get_absolute_url }}"><img src="{{ photo.get_thumbnail_url }}" alt="{{ photo.title }}"/></a>
-    </div>
-    {% endfor %}
-{% else %}
-<p>No photos were found.</p>
-{% endif %}
-
-{% if is_paginated %}
-<p>{{ hits }} photos total.</p>
-<div id="page_controls">
-    <p>{% if has_previous %}<a href="{% url pl-photo-list previous %}">Previous</a> | {% endif %} page {{ page }} of {{ pages }} {% if has_next %}| <a href="{% url pl-photo-list next %}">Next</a>{% endif %}</p>
-</div>
-{% endif %}
-
-{% endblock %}
--- a/madeira/photologue/templates/photologue/root.html	Sat Apr 14 15:49:28 2012 -0500
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,1 +0,0 @@
-{% extends "base.html" %}
\ No newline at end of file
--- a/madeira/photologue/tests.py	Sat Apr 14 15:49:28 2012 -0500
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,218 +0,0 @@
-import os
-import unittest
-from django.conf import settings
-from django.core.files.base import ContentFile
-from django.test import TestCase
-
-from models import *
-
-# Path to sample image
-RES_DIR = os.path.join(os.path.dirname(__file__), 'res')
-LANDSCAPE_IMAGE_PATH = os.path.join(RES_DIR, 'test_landscape.jpg')
-PORTRAIT_IMAGE_PATH = os.path.join(RES_DIR, 'test_portrait.jpg')
-SQUARE_IMAGE_PATH = os.path.join(RES_DIR, 'test_square.jpg')
-        
-        
-class TestPhoto(ImageModel):
-    """ Minimal ImageModel class for testing """
-    name = models.CharField(max_length=30)
-    
-    
-class PLTest(TestCase):
-    """ Base TestCase class """
-    def setUp(self):
-        self.s = PhotoSize(name='test', width=100, height=100)
-        self.s.save()
-        self.pl = TestPhoto(name='landscape')
-        self.pl.image.save(os.path.basename(LANDSCAPE_IMAGE_PATH),
-                           ContentFile(open(LANDSCAPE_IMAGE_PATH, 'rb').read()))
-        self.pl.save()
-
-    def tearDown(self):
-        path = self.pl.image.path
-        self.pl.delete()
-        self.failIf(os.path.isfile(path))
-        self.s.delete()
-
-
-class PhotoTest(PLTest):    
-    def test_new_photo(self):
-        self.assertEqual(TestPhoto.objects.count(), 1)
-        self.failUnless(os.path.isfile(self.pl.image.path))
-        self.assertEqual(os.path.getsize(self.pl.image.path),
-                         os.path.getsize(LANDSCAPE_IMAGE_PATH))
-                                         
-    def test_exif(self):
-        self.assert_(len(self.pl.EXIF.keys()) > 0)
-
-    def test_paths(self):
-        self.assertEqual(os.path.normpath(str(self.pl.cache_path())).lower(),
-                         os.path.normpath(os.path.join(settings.MEDIA_ROOT,
-                                      PHOTOLOGUE_DIR,
-                                      'photos',
-                                      'cache')).lower())
-        self.assertEqual(self.pl.cache_url(),
-                         settings.MEDIA_URL + PHOTOLOGUE_DIR + '/photos/cache')
-
-    def test_count(self):
-        for i in range(5):
-            self.pl.get_test_url()
-        self.assertEquals(self.pl.view_count, 0)
-        self.s.increment_count = True
-        self.s.save()
-        for i in range(5):
-            self.pl.get_test_url()
-        self.assertEquals(self.pl.view_count, 5)
-        
-    def test_precache(self):
-        # set the thumbnail photo size to pre-cache
-        self.s.pre_cache = True
-        self.s.save()
-        # make sure it created the file
-        self.failUnless(os.path.isfile(self.pl.get_test_filename()))
-        self.s.pre_cache = False
-        self.s.save()
-        # clear the cache and make sure the file's deleted
-        self.pl.clear_cache()
-        self.failIf(os.path.isfile(self.pl.get_test_filename()))
-        
-    def test_accessor_methods(self):
-        self.assertEquals(self.pl.get_test_photosize(), self.s)
-        self.assertEquals(self.pl.get_test_size(),
-                          Image.open(self.pl.get_test_filename()).size)
-        self.assertEquals(self.pl.get_test_url(),
-                          self.pl.cache_url() + '/' + \
-                          self.pl._get_filename_for_size(self.s))
-        self.assertEquals(self.pl.get_test_filename(),
-                          os.path.join(self.pl.cache_path(),
-                          self.pl._get_filename_for_size(self.s)))
-        
-        
-class ImageResizeTest(PLTest):
-    def setUp(self):
-        super(ImageResizeTest, self).setUp()
-        self.pp = TestPhoto(name='portrait')
-        self.pp.image.save(os.path.basename(PORTRAIT_IMAGE_PATH),
-                           ContentFile(open(PORTRAIT_IMAGE_PATH, 'rb').read()))
-        self.pp.save()
-        self.ps = TestPhoto(name='square')
-        self.ps.image.save(os.path.basename(SQUARE_IMAGE_PATH),
-                           ContentFile(open(SQUARE_IMAGE_PATH, 'rb').read()))
-        self.ps.save()
-        
-    def tearDown(self):
-        super(ImageResizeTest, self).tearDown()
-        self.pp.delete()
-        self.ps.delete()
-             
-    def test_resize_to_fit(self):
-        self.assertEquals(self.pl.get_test_size(), (100, 75))
-        self.assertEquals(self.pp.get_test_size(), (75, 100))
-        self.assertEquals(self.ps.get_test_size(), (100, 100))
-        
-    def test_resize_to_fit_width(self):
-        self.s.size = (100, 0)
-        self.s.save()
-        self.assertEquals(self.pl.get_test_size(), (100, 75))
-        self.assertEquals(self.pp.get_test_size(), (100, 133))
-        self.assertEquals(self.ps.get_test_size(), (100, 100))
-        
-    def test_resize_to_fit_width_enlarge(self):
-        self.s.size = (2000, 0)
-        self.s.upscale = True
-        self.s.save()
-        self.assertEquals(self.pl.get_test_size(), (2000, 1500))
-        self.assertEquals(self.pp.get_test_size(), (2000, 2667))
-        self.assertEquals(self.ps.get_test_size(), (2000, 2000))
-
-    def test_resize_to_fit_height(self):
-        self.s.size = (0, 100)
-        self.s.save()
-        self.assertEquals(self.pl.get_test_size(), (133, 100))
-        self.assertEquals(self.pp.get_test_size(), (75, 100))
-        self.assertEquals(self.ps.get_test_size(), (100, 100))
-        
-    def test_resize_to_fit_height_enlarge(self):
-        self.s.size = (0, 2000)
-        self.s.upscale = True
-        self.s.save()
-        self.assertEquals(self.pl.get_test_size(), (2667, 2000))
-        self.assertEquals(self.pp.get_test_size(), (1500, 2000))
-        self.assertEquals(self.ps.get_test_size(), (2000, 2000))
-        
-    def test_resize_and_crop(self):
-        self.s.crop = True
-        self.s.save()
-        self.assertEquals(self.pl.get_test_size(), self.s.size)
-        self.assertEquals(self.pp.get_test_size(), self.s.size)
-        self.assertEquals(self.ps.get_test_size(), self.s.size)
-        
-    def test_resize_rounding_to_fit(self):
-        self.s.size = (113, 113)
-        self.s.save()
-        self.assertEquals(self.pl.get_test_size(), (113, 85))
-        self.assertEquals(self.pp.get_test_size(), (85, 113))
-        self.assertEquals(self.ps.get_test_size(), (113, 113))  
-        
-    def test_resize_rounding_cropped(self):
-        self.s.size = (113, 113)
-        self.s.crop = True
-        self.s.save()
-        self.assertEquals(self.pl.get_test_size(), self.s.size)
-        self.assertEquals(self.pp.get_test_size(), self.s.size)
-        self.assertEquals(self.ps.get_test_size(), self.s.size)
-        
-    def test_resize_one_dimension_width(self):
-        self.s.size = (1500, 1200)
-        self.s.save()
-        self.assertEquals(self.pl.get_test_size(), (1500, 1125))
-        
-    def test_resize_one_dimension_height(self):
-        self.s.size = (1600, 1100)
-        self.s.save()
-        self.assertEquals(self.pl.get_test_size(), (1467, 1100))
-        
-    def test_resize_no_upscale(self):
-        self.s.size = (2000, 2000)
-        self.s.save()
-        self.assertEquals(self.pl.get_test_size(), (1600, 1200))
-        
-    def test_resize_no_upscale_mixed_height(self):
-        self.s.size = (3200, 600)
-        self.s.save()
-        self.assertEquals(self.pl.get_test_size(), (800, 600))
-        
-    def test_resize_no_upscale_mixed_width(self):
-        self.s.size = (800, 2400)
-        self.s.save()
-        self.assertEquals(self.pl.get_test_size(), (800, 600))
-        
-    def test_resize_no_upscale_crop(self):
-        self.s.size = (2000, 2000)
-        self.s.crop = True
-        self.s.save()
-        self.assertEquals(self.pl.get_test_size(), (2000, 2000))
-        
-    def test_resize_upscale(self):
-        self.s.size = (2000, 2000)
-        self.s.upscale = True
-        self.s.save()
-        self.assertEquals(self.pl.get_test_size(), (2000, 1500))
-        self.assertEquals(self.pp.get_test_size(), (1500, 2000))
-        self.assertEquals(self.ps.get_test_size(), (2000, 2000))
-
-
-class PhotoEffectTest(PLTest):
-    def test(self):
-        effect = PhotoEffect(name='test')
-        im = Image.open(self.pl.image.path)
-        self.assert_(isinstance(effect.pre_process(im), Image.Image))
-        self.assert_(isinstance(effect.post_process(im), Image.Image))
-        self.assert_(isinstance(effect.process(im), Image.Image))
-
-
-class PhotoSizeCacheTest(PLTest):
-    def test(self):
-        cache = PhotoSizeCache()
-        self.assertEqual(cache.sizes['test'], self.s)    
-        
--- a/madeira/photologue/urls.py	Sat Apr 14 15:49:28 2012 -0500
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,36 +0,0 @@
-from django.conf import settings
-from django.conf.urls import *
-from models import *
-
-# Number of random images from the gallery to display.
-SAMPLE_SIZE = ":%s" % getattr(settings, 'GALLERY_SAMPLE_SIZE', 5)
-
-# galleries
-gallery_args = {'date_field': 'date_added', 'allow_empty': True, 'queryset': Gallery.objects.filter(is_public=True), 'extra_context':{'sample_size':SAMPLE_SIZE}}
-urlpatterns = patterns('django.views.generic.date_based',
-    url(r'^gallery/(?P<year>\d{4})/(?P<month>[a-z]{3})/(?P<day>\w{1,2})/(?P<slug>[\-\d\w]+)/$', 'object_detail', {'date_field': 'date_added', 'slug_field': 'title_slug', 'queryset': Gallery.objects.filter(is_public=True), 'extra_context':{'sample_size':SAMPLE_SIZE}}, name='pl-gallery-detail'),
-    url(r'^gallery/(?P<year>\d{4})/(?P<month>[a-z]{3})/(?P<day>\w{1,2})/$', 'archive_day', gallery_args, name='pl-gallery-archive-day'),
-    url(r'^gallery/(?P<year>\d{4})/(?P<month>[a-z]{3})/$', 'archive_month', gallery_args, name='pl-gallery-archive-month'),
-    url(r'^gallery/(?P<year>\d{4})/$', 'archive_year', gallery_args, name='pl-gallery-archive-year'),
-    url(r'^gallery/?$', 'archive_index', gallery_args, name='pl-gallery-archive'),
-)
-urlpatterns += patterns('django.views.generic.list_detail',
-    url(r'^gallery/(?P<slug>[\-\d\w]+)/$', 'object_detail', {'slug_field': 'title_slug', 'queryset': Gallery.objects.filter(is_public=True), 'extra_context':{'sample_size':SAMPLE_SIZE}}, name='pl-gallery'),
-    url(r'^gallery/page/(?P<page>[0-9]+)/$', 'object_list', {'queryset': Gallery.objects.filter(is_public=True), 'allow_empty': True, 'paginate_by': 5, 'extra_context':{'sample_size':SAMPLE_SIZE}}, name='pl-gallery-list'),
-)
-
-# photographs
-photo_args = {'date_field': 'date_added', 'allow_empty': True, 'queryset': Photo.objects.filter(is_public=True)}
-urlpatterns += patterns('django.views.generic.date_based',
-    url(r'^photo/(?P<year>\d{4})/(?P<month>[a-z]{3})/(?P<day>\w{1,2})/(?P<slug>[\-\d\w]+)/$', 'object_detail', {'date_field': 'date_added', 'slug_field': 'title_slug', 'queryset': Photo.objects.filter(is_public=True)}, name='pl-photo-detail'),
-    url(r'^photo/(?P<year>\d{4})/(?P<month>[a-z]{3})/(?P<day>\w{1,2})/$', 'archive_day', photo_args, name='pl-photo-archive-day'),
-    url(r'^photo/(?P<year>\d{4})/(?P<month>[a-z]{3})/$', 'archive_month', photo_args, name='pl-photo-archive-month'),
-    url(r'^photo/(?P<year>\d{4})/$', 'archive_year', photo_args, name='pl-photo-archive-year'),
-    url(r'^photo/$', 'archive_index', photo_args, name='pl-photo-archive'),
-)
-urlpatterns += patterns('django.views.generic.list_detail',
-    url(r'^photo/(?P<slug>[\-\d\w]+)/$', 'object_detail', {'slug_field': 'title_slug', 'queryset': Photo.objects.filter(is_public=True)}, name='pl-photo'),
-    url(r'^photo/page/(?P<page>[0-9]+)/$', 'object_list', {'queryset': Photo.objects.filter(is_public=True), 'allow_empty': True, 'paginate_by': 20}, name='pl-photo-list'),
-)
-
-
--- a/madeira/photologue/utils/EXIF.py	Sat Apr 14 15:49:28 2012 -0500
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,1568 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-#
-# Library to extract EXIF information from digital camera image files
-# http://sourceforge.net/projects/exif-py/
-#
-# VERSION 1.0.7
-#
-# To use this library call with:
-#    f = open(path_name, 'rb')
-#    tags = EXIF.process_file(f)
-#
-# To ignore makerNote tags, pass the -q or --quick
-# command line arguments, or as
-#    f = open(path_name, 'rb')
-#    tags = EXIF.process_file(f, details=False)
-#
-# To stop processing after a certain tag is retrieved,
-# pass the -t TAG or --stop-tag TAG argument, or as
-#    f = open(path_name, 'rb')
-#    tags = EXIF.process_file(f, stop_tag='TAG')
-#
-# where TAG is a valid tag name, ex 'DateTimeOriginal'
-#
-# These are useful when you are retrieving a large list of images
-#
-# Returned tags will be a dictionary mapping names of EXIF tags to their
-# values in the file named by path_name.  You can process the tags
-# as you wish.  In particular, you can iterate through all the tags with:
-#     for tag in tags.keys():
-#         if tag not in ('JPEGThumbnail', 'TIFFThumbnail', 'Filename',
-#                        'EXIF MakerNote'):
-#             print "Key: %s, value %s" % (tag, tags[tag])
-# (This code uses the if statement to avoid printing out a few of the
-# tags that tend to be long or boring.)
-#
-# The tags dictionary will include keys for all of the usual EXIF
-# tags, and will also include keys for Makernotes used by some
-# cameras, for which we have a good specification.
-#
-# Note that the dictionary keys are the IFD name followed by the
-# tag name. For example:
-# 'EXIF DateTimeOriginal', 'Image Orientation', 'MakerNote FocusMode'
-#
-# Copyright (c) 2002-2007 Gene Cash All rights reserved
-# Copyright (c) 2007 Ianaré Sévi All rights reserved
-#
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions
-# are met:
-#
-#  1. Redistributions of source code must retain the above copyright
-#     notice, this list of conditions and the following disclaimer.
-#
-#  2. Redistributions in binary form must reproduce the above
-#     copyright notice, this list of conditions and the following
-#     disclaimer in the documentation and/or other materials provided
-#     with the distribution.
-#
-#  3. Neither the name of the authors nor the names of its contributors
-#     may be used to endorse or promote products derived from this
-#     software without specific prior written permission.
-#
-# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
-# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
-# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
-# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
-# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
-# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
-# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
-# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
-# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
-# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-#
-#
-# ----- See 'changes.txt' file for all contributors and changes ----- #
-#
-
-
-# Don't throw an exception when given an out of range character.
-def make_string(seq):
-    str = ""
-    for c in seq:
-        # Screen out non-printing characters
-        if 32 <= c and c < 256:
-            str += chr(c)
-    # If no printing chars
-    if not str:
-        return seq
-    return str
-
-# Special version to deal with the code in the first 8 bytes of a user comment.
-def make_string_uc(seq):
-    code = seq[0:8]
-    seq = seq[8:]
-    # Of course, this is only correct if ASCII, and the standard explicitly
-    # allows JIS and Unicode.
-    return make_string(seq)
-
-# field type descriptions as (length, abbreviation, full name) tuples
-FIELD_TYPES = (
-    (0, 'X', 'Proprietary'), # no such type
-    (1, 'B', 'Byte'),
-    (1, 'A', 'ASCII'),
-    (2, 'S', 'Short'),
-    (4, 'L', 'Long'),
-    (8, 'R', 'Ratio'),
-    (1, 'SB', 'Signed Byte'),
-    (1, 'U', 'Undefined'),
-    (2, 'SS', 'Signed Short'),
-    (4, 'SL', 'Signed Long'),
-    (8, 'SR', 'Signed Ratio'),
-    )
-
-# dictionary of main EXIF tag names
-# first element of tuple is tag name, optional second element is
-# another dictionary giving names to values
-EXIF_TAGS = {
-    0x0100: ('ImageWidth', ),
-    0x0101: ('ImageLength', ),
-    0x0102: ('BitsPerSample', ),
-    0x0103: ('Compression',
-             {1: 'Uncompressed TIFF',
-              6: 'JPEG Compressed'}),
-    0x0106: ('PhotometricInterpretation', ),
-    0x010A: ('FillOrder', ),
-    0x010D: ('DocumentName', ),
-    0x010E: ('ImageDescription', ),
-    0x010F: ('Make', ),
-    0x0110: ('Model', ),
-    0x0111: ('StripOffsets', ),
-    0x0112: ('Orientation',
-             {1: 'Horizontal (normal)',
-              2: 'Mirrored horizontal',
-              3: 'Rotated 180',
-              4: 'Mirrored vertical',
-              5: 'Mirrored horizontal then rotated 90 CCW',
-              6: 'Rotated 90 CW',
-              7: 'Mirrored horizontal then rotated 90 CW',
-              8: 'Rotated 90 CCW'}),
-    0x0115: ('SamplesPerPixel', ),
-    0x0116: ('RowsPerStrip', ),
-    0x0117: ('StripByteCounts', ),
-    0x011A: ('XResolution', ),
-    0x011B: ('YResolution', ),
-    0x011C: ('PlanarConfiguration', ),
-    0x0128: ('ResolutionUnit',
-             {1: 'Not Absolute',
-              2: 'Pixels/Inch',
-              3: 'Pixels/Centimeter'}),
-    0x012D: ('TransferFunction', ),
-    0x0131: ('Software', ),
-    0x0132: ('DateTime', ),
-    0x013B: ('Artist', ),
-    0x013E: ('WhitePoint', ),
-    0x013F: ('PrimaryChromaticities', ),
-    0x0156: ('TransferRange', ),
-    0x0200: ('JPEGProc', ),
-    0x0201: ('JPEGInterchangeFormat', ),
-    0x0202: ('JPEGInterchangeFormatLength', ),
-    0x0211: ('YCbCrCoefficients', ),
-    0x0212: ('YCbCrSubSampling', ),
-    0x0213: ('YCbCrPositioning', ),
-    0x0214: ('ReferenceBlackWhite', ),
-    0x828D: ('CFARepeatPatternDim', ),
-    0x828E: ('CFAPattern', ),
-    0x828F: ('BatteryLevel', ),
-    0x8298: ('Copyright', ),
-    0x829A: ('ExposureTime', ),
-    0x829D: ('FNumber', ),
-    0x83BB: ('IPTC/NAA', ),
-    0x8769: ('ExifOffset', ),
-    0x8773: ('InterColorProfile', ),
-    0x8822: ('ExposureProgram',
-             {0: 'Unidentified',
-              1: 'Manual',
-              2: 'Program Normal',
-              3: 'Aperture Priority',
-              4: 'Shutter Priority',
-              5: 'Program Creative',
-              6: 'Program Action',
-              7: 'Portrait Mode',
-              8: 'Landscape Mode'}),
-    0x8824: ('SpectralSensitivity', ),
-    0x8825: ('GPSInfo', ),
-    0x8827: ('ISOSpeedRatings', ),
-    0x8828: ('OECF', ),
-    # print as string
-    0x9000: ('ExifVersion', make_string),
-    0x9003: ('DateTimeOriginal', ),
-    0x9004: ('DateTimeDigitized', ),
-    0x9101: ('ComponentsConfiguration',
-             {0: '',
-              1: 'Y',
-              2: 'Cb',
-              3: 'Cr',
-              4: 'Red',
-              5: 'Green',
-              6: 'Blue'}),
-    0x9102: ('CompressedBitsPerPixel', ),
-    0x9201: ('ShutterSpeedValue', ),
-    0x9202: ('ApertureValue', ),
-    0x9203: ('BrightnessValue', ),
-    0x9204: ('ExposureBiasValue', ),
-    0x9205: ('MaxApertureValue', ),
-    0x9206: ('SubjectDistance', ),
-    0x9207: ('MeteringMode',
-             {0: 'Unidentified',
-              1: 'Average',
-              2: 'CenterWeightedAverage',
-              3: 'Spot',
-              4: 'MultiSpot'}),
-    0x9208: ('LightSource',
-             {0: 'Unknown',
-              1: 'Daylight',
-              2: 'Fluorescent',
-              3: 'Tungsten',
-              10: 'Flash',
-              17: 'Standard Light A',
-              18: 'Standard Light B',
-              19: 'Standard Light C',
-              20: 'D55',
-              21: 'D65',
-              22: 'D75',
-              255: 'Other'}),
-    0x9209: ('Flash', {0: 'No',
-                       1: 'Fired',
-                       5: 'Fired (?)', # no return sensed
-                       7: 'Fired (!)', # return sensed
-                       9: 'Fill Fired',
-                       13: 'Fill Fired (?)',
-                       15: 'Fill Fired (!)',
-                       16: 'Off',
-                       24: 'Auto Off',
-                       25: 'Auto Fired',
-                       29: 'Auto Fired (?)',
-                       31: 'Auto Fired (!)',
-                       32: 'Not Available'}),
-    0x920A: ('FocalLength', ),
-    0x9214: ('SubjectArea', ),
-    0x927C: ('MakerNote', ),
-    # print as string
-    0x9286: ('UserComment', make_string_uc),  # First 8 bytes gives coding system e.g. ASCII vs. JIS vs Unicode
-    0x9290: ('SubSecTime', ),
-    0x9291: ('SubSecTimeOriginal', ),
-    0x9292: ('SubSecTimeDigitized', ),
-    # print as string
-    0xA000: ('FlashPixVersion', make_string),
-    0xA001: ('ColorSpace', ),
-    0xA002: ('ExifImageWidth', ),
-    0xA003: ('ExifImageLength', ),
-    0xA005: ('InteroperabilityOffset', ),
-    0xA20B: ('FlashEnergy', ),               # 0x920B in TIFF/EP
-    0xA20C: ('SpatialFrequencyResponse', ),  # 0x920C    -  -
-    0xA20E: ('FocalPlaneXResolution', ),     # 0x920E    -  -
-    0xA20F: ('FocalPlaneYResolution', ),     # 0x920F    -  -
-    0xA210: ('FocalPlaneResolutionUnit', ),  # 0x9210    -  -
-    0xA214: ('SubjectLocation', ),           # 0x9214    -  -
-    0xA215: ('ExposureIndex', ),             # 0x9215    -  -
-    0xA217: ('SensingMethod', ),             # 0x9217    -  -
-    0xA300: ('FileSource',
-             {3: 'Digital Camera'}),
-    0xA301: ('SceneType',
-             {1: 'Directly Photographed'}),
-    0xA302: ('CVAPattern', ),
-    0xA401: ('CustomRendered', ),
-    0xA402: ('ExposureMode',
-             {0: 'Auto Exposure',
-              1: 'Manual Exposure',
-              2: 'Auto Bracket'}),
-    0xA403: ('WhiteBalance',
-             {0: 'Auto',
-              1: 'Manual'}),
-    0xA404: ('DigitalZoomRatio', ),
-    0xA405: ('FocalLengthIn35mm', ),
-    0xA406: ('SceneCaptureType', ),
-    0xA407: ('GainControl', ),
-    0xA408: ('Contrast', ),
-    0xA409: ('Saturation', ),
-    0xA40A: ('Sharpness', ),
-    0xA40C: ('SubjectDistanceRange', ),
-    }
-
-# interoperability tags
-INTR_TAGS = {
-    0x0001: ('InteroperabilityIndex', ),
-    0x0002: ('InteroperabilityVersion', ),
-    0x1000: ('RelatedImageFileFormat', ),
-    0x1001: ('RelatedImageWidth', ),
-    0x1002: ('RelatedImageLength', ),
-    }
-
-# GPS tags (not used yet, haven't seen camera with GPS)
-GPS_TAGS = {
-    0x0000: ('GPSVersionID', ),
-    0x0001: ('GPSLatitudeRef', ),
-    0x0002: ('GPSLatitude', ),
-    0x0003: ('GPSLongitudeRef', ),
-    0x0004: ('GPSLongitude', ),
-    0x0005: ('GPSAltitudeRef', ),
-    0x0006: ('GPSAltitude', ),
-    0x0007: ('GPSTimeStamp', ),
-    0x0008: ('GPSSatellites', ),
-    0x0009: ('GPSStatus', ),
-    0x000A: ('GPSMeasureMode', ),
-    0x000B: ('GPSDOP', ),
-    0x000C: ('GPSSpeedRef', ),
-    0x000D: ('GPSSpeed', ),
-    0x000E: ('GPSTrackRef', ),
-    0x000F: ('GPSTrack', ),
-    0x0010: ('GPSImgDirectionRef', ),
-    0x0011: ('GPSImgDirection', ),
-    0x0012: ('GPSMapDatum', ),
-    0x0013: ('GPSDestLatitudeRef', ),
-    0x0014: ('GPSDestLatitude', ),
-    0x0015: ('GPSDestLongitudeRef', ),
-    0x0016: ('GPSDestLongitude', ),
-    0x0017: ('GPSDestBearingRef', ),
-    0x0018: ('GPSDestBearing', ),
-    0x0019: ('GPSDestDistanceRef', ),
-    0x001A: ('GPSDestDistance', ),
-    }
-
-# Ignore these tags when quick processing
-# 0x927C is MakerNote Tags
-# 0x9286 is user comment
-IGNORE_TAGS=(0x9286, 0x927C)
-
-# http://tomtia.plala.jp/DigitalCamera/MakerNote/index.asp
-def nikon_ev_bias(seq):
-    # First digit seems to be in steps of 1/6 EV.
-    # Does the third value mean the step size?  It is usually 6,
-    # but it is 12 for the ExposureDifference.
-    #
-    if seq == [252, 1, 6, 0]:
-        return "-2/3 EV"
-    if seq == [253, 1, 6, 0]:
-        return "-1/2 EV"
-    if seq == [254, 1, 6, 0]:
-        return "-1/3 EV"
-    if seq == [0, 1, 6, 0]:
-        return "0 EV"
-    if seq == [2, 1, 6, 0]:
-        return "+1/3 EV"
-    if seq == [3, 1, 6, 0]:
-        return "+1/2 EV"
-    if seq == [4, 1, 6, 0]:
-        return "+2/3 EV"
-    # Handle combinations not in the table.
-    a = seq[0]
-    # Causes headaches for the +/- logic, so special case it.
-    if a == 0:
-        return "0 EV"
-    if a > 127:
-        a = 256 - a
-        ret_str = "-"
-    else:
-        ret_str = "+"
-    b = seq[2]	# Assume third value means the step size
-    whole = a / b
-    a = a % b
-    if whole != 0:
-        ret_str = ret_str + str(whole) + " "
-    if a == 0:
-        ret_str = ret_str + "EV"
-    else:
-        r = Ratio(a, b)
-        ret_str = ret_str + r.__repr__() + " EV"
-    return ret_str
-
-# Nikon E99x MakerNote Tags
-MAKERNOTE_NIKON_NEWER_TAGS={
-    0x0001: ('MakernoteVersion', make_string),	# Sometimes binary
-    0x0002: ('ISOSetting', ),
-    0x0003: ('ColorMode', ),
-    0x0004: ('Quality', ),
-    0x0005: ('Whitebalance', ),
-    0x0006: ('ImageSharpening', ),
-    0x0007: ('FocusMode', ),
-    0x0008: ('FlashSetting', ),
-    0x0009: ('AutoFlashMode', ),
-    0x000B: ('WhiteBalanceBias', ),
-    0x000C: ('WhiteBalanceRBCoeff', ),
-    0x000D: ('ProgramShift', nikon_ev_bias),
-    # Nearly the same as the other EV vals, but step size is 1/12 EV (?)
-    0x000E: ('ExposureDifference', nikon_ev_bias),
-    0x000F: ('ISOSelection', ),
-    0x0011: ('NikonPreview', ),
-    0x0012: ('FlashCompensation', nikon_ev_bias),
-    0x0013: ('ISOSpeedRequested', ),
-    0x0016: ('PhotoCornerCoordinates', ),
-    # 0x0017: Unknown, but most likely an EV value
-    0x0018: ('FlashBracketCompensationApplied', nikon_ev_bias),
-    0x0019: ('AEBracketCompensationApplied', ),
-    0x001A: ('ImageProcessing', ),
-    0x0080: ('ImageAdjustment', ),
-    0x0081: ('ToneCompensation', ),
-    0x0082: ('AuxiliaryLens', ),
-    0x0083: ('LensType', ),
-    0x0084: ('LensMinMaxFocalMaxAperture', ),
-    0x0085: ('ManualFocusDistance', ),
-    0x0086: ('DigitalZoomFactor', ),
-    0x0087: ('FlashMode',
-             {0x00: 'Did Not Fire',
-              0x01: 'Fired, Manual',
-              0x07: 'Fired, External',
-              0x08: 'Fired, Commander Mode ',
-              0x09: 'Fired, TTL Mode'}),
-    0x0088: ('AFFocusPosition',
-             {0x0000: 'Center',
-              0x0100: 'Top',
-              0x0200: 'Bottom',
-              0x0300: 'Left',
-              0x0400: 'Right'}),
-    0x0089: ('BracketingMode',
-             {0x00: 'Single frame, no bracketing',
-              0x01: 'Continuous, no bracketing',
-              0x02: 'Timer, no bracketing',
-              0x10: 'Single frame, exposure bracketing',
-              0x11: 'Continuous, exposure bracketing',
-              0x12: 'Timer, exposure bracketing',
-              0x40: 'Single frame, white balance bracketing',
-              0x41: 'Continuous, white balance bracketing',
-              0x42: 'Timer, white balance bracketing'}),
-    0x008A: ('AutoBracketRelease', ),
-    0x008B: ('LensFStops', ),
-    0x008C: ('NEFCurve2', ),
-    0x008D: ('ColorMode', ),
-    0x008F: ('SceneMode', ),
-    0x0090: ('LightingType', ),
-    0x0091: ('ShotInfo', ),	# First 4 bytes are probably a version number in ASCII
-    0x0092: ('HueAdjustment', ),
-    # 0x0093: ('SaturationAdjustment', ),
-    0x0094: ('Saturation',	# Name conflict with 0x00AA !!
-             {-3: 'B&W',
-              -2: '-2',
-              -1: '-1',
-              0: '0',
-              1: '1',
-              2: '2'}),
-    0x0095: ('NoiseReduction', ),
-    0x0096: ('NEFCurve2', ),
-    0x0097: ('ColorBalance', ),
-    0x0098: ('LensData', ),	# First 4 bytes are a version number in ASCII
-    0x0099: ('RawImageCenter', ),
-    0x009A: ('SensorPixelSize', ),
-    0x009C: ('Scene Assist', ),
-    0x00A0: ('SerialNumber', ),
-    0x00A2: ('ImageDataSize', ),
-    # A4: In NEF, looks like a 4 byte ASCII version number
-    0x00A5: ('ImageCount', ),
-    0x00A6: ('DeletedImageCount', ),
-    0x00A7: ('TotalShutterReleases', ),
-    # A8: ExposureMode?  JPG: First 4 bytes are probably a version number in ASCII
-    # But in a sample NEF, its 8 zeros, then the string "NORMAL"
-    0x00A9: ('ImageOptimization', ),
-    0x00AA: ('Saturation', ),
-    0x00AB: ('DigitalVariProgram', ),
-    0x00AC: ('ImageStabilization', ),
-    0x00AD: ('Responsive AF', ),	# 'AFResponse'
-    0x0010: ('DataDump', ),
-    }
-
-MAKERNOTE_NIKON_OLDER_TAGS = {
-    0x0003: ('Quality',
-             {1: 'VGA Basic',
-              2: 'VGA Normal',
-              3: 'VGA Fine',
-              4: 'SXGA Basic',
-              5: 'SXGA Normal',
-              6: 'SXGA Fine'}),
-    0x0004: ('ColorMode',
-             {1: 'Color',
-              2: 'Monochrome'}),
-    0x0005: ('ImageAdjustment',
-             {0: 'Normal',
-              1: 'Bright+',
-              2: 'Bright-',
-              3: 'Contrast+',
-              4: 'Contrast-'}),
-    0x0006: ('CCDSpeed',
-             {0: 'ISO 80',
-              2: 'ISO 160',
-              4: 'ISO 320',
-              5: 'ISO 100'}),
-    0x0007: ('WhiteBalance',
-             {0: 'Auto',
-              1: 'Preset',
-              2: 'Daylight',
-              3: 'Incandescent',
-              4: 'Fluorescent',
-              5: 'Cloudy',
-              6: 'Speed Light'}),
-    }
-
-# decode Olympus SpecialMode tag in MakerNote
-def olympus_special_mode(v):
-    a={
-        0: 'Normal',
-        1: 'Unknown',
-        2: 'Fast',
-        3: 'Panorama'}
-    b={
-        0: 'Non-panoramic',
-        1: 'Left to right',
-        2: 'Right to left',
-        3: 'Bottom to top',
-        4: 'Top to bottom'}
-    if v[0] not in a or v[2] not in b:
-        return v
-    return '%s - sequence %d - %s' % (a[v[0]], v[1], b[v[2]])
-
-MAKERNOTE_OLYMPUS_TAGS={
-    # ah HAH! those sneeeeeaky bastids! this is how they get past the fact
-    # that a JPEG thumbnail is not allowed in an uncompressed TIFF file
-    0x0100: ('JPEGThumbnail', ),
-    0x0200: ('SpecialMode', olympus_special_mode),
-    0x0201: ('JPEGQual',
-             {1: 'SQ',
-              2: 'HQ',
-              3: 'SHQ'}),
-    0x0202: ('Macro',
-             {0: 'Normal',
-             1: 'Macro',
-             2: 'SuperMacro'}),
-    0x0203: ('BWMode',
-             {0: 'Off',
-             1: 'On'}),
-    0x0204: ('DigitalZoom', ),
-    0x0205: ('FocalPlaneDiagonal', ),
-    0x0206: ('LensDistortionParams', ),
-    0x0207: ('SoftwareRelease', ),
-    0x0208: ('PictureInfo', ),
-    0x0209: ('CameraID', make_string), # print as string
-    0x0F00: ('DataDump', ),
-    0x0300: ('PreCaptureFrames', ),
-    0x0404: ('SerialNumber', ),
-    0x1000: ('ShutterSpeedValue', ),
-    0x1001: ('ISOValue', ),
-    0x1002: ('ApertureValue', ),
-    0x1003: ('BrightnessValue', ),
-    0x1004: ('FlashMode', ),
-    0x1004: ('FlashMode',
-       {2: 'On',
-        3: 'Off'}),
-    0x1005: ('FlashDevice',
-       {0: 'None',
-        1: 'Internal',
-        4: 'External',
-        5: 'Internal + External'}),
-    0x1006: ('ExposureCompensation', ),
-    0x1007: ('SensorTemperature', ),
-    0x1008: ('LensTemperature', ),
-    0x100b: ('FocusMode',
-       {0: 'Auto',
-        1: 'Manual'}),
-    0x1017: ('RedBalance', ),
-    0x1018: ('BlueBalance', ),
-    0x101a: ('SerialNumber', ),
-    0x1023: ('FlashExposureComp', ),
-    0x1026: ('ExternalFlashBounce',
-       {0: 'No',
-        1: 'Yes'}),
-    0x1027: ('ExternalFlashZoom', ),
-    0x1028: ('ExternalFlashMode', ),
-    0x1029: ('Contrast 	int16u',
-       {0: 'High',
-        1: 'Normal',
-        2: 'Low'}),
-    0x102a: ('SharpnessFactor', ),
-    0x102b: ('ColorControl', ),
-    0x102c: ('ValidBits', ),
-    0x102d: ('CoringFilter', ),
-    0x102e: ('OlympusImageWidth', ),
-    0x102f: ('OlympusImageHeight', ),
-    0x1034: ('CompressionRatio', ),
-    0x1035: ('PreviewImageValid',
-       {0: 'No',
-        1: 'Yes'}),
-    0x1036: ('PreviewImageStart', ),
-    0x1037: ('PreviewImageLength', ),
-    0x1039: ('CCDScanMode',
-       {0: 'Interlaced',
-        1: 'Progressive'}),
-    0x103a: ('NoiseReduction',
-       {0: 'Off',
-        1: 'On'}),
-    0x103b: ('InfinityLensStep', ),
-    0x103c: ('NearLensStep', ),
-
-    # TODO - these need extra definitions
-    # http://search.cpan.org/src/EXIFTOOL/Image-ExifTool-6.90/html/TagNames/Olympus.html
-    0x2010: ('Equipment', ),
-    0x2020: ('CameraSettings', ),
-    0x2030: ('RawDevelopment', ),
-    0x2040: ('ImageProcessing', ),
-    0x2050: ('FocusInfo', ),
-    0x3000: ('RawInfo ', ),
-    }
-
-# 0x2020 CameraSettings
-MAKERNOTE_OLYMPUS_TAG_0x2020={
-    0x0100: ('PreviewImageValid',
-        {0: 'No',
-         1: 'Yes'}),
-    0x0101: ('PreviewImageStart', ),
-    0x0102: ('PreviewImageLength', ),
-    0x0200: ('ExposureMode', {
-        1: 'Manual',
-        2: 'Program',
-        3: 'Aperture-priority AE',
-        4: 'Shutter speed priority AE',
-        5: 'Program-shift'}),
-    0x0201: ('AELock',
-       {0: 'Off',
-        1: 'On'}),
-    0x0202: ('MeteringMode',
-       {2: 'Center Weighted',
-        3: 'Spot',
-        5: 'ESP',
-        261: 'Pattern+AF',
-        515: 'Spot+Highlight control',
-        1027: 'Spot+Shadow control'}),
-    0x0300: ('MacroMode',
-       {0: 'Off',
-        1: 'On'}),
-    0x0301: ('FocusMode',
-       {0: 'Single AF',
-        1: 'Sequential shooting AF',
-        2: 'Continuous AF',
-        3: 'Multi AF',
-        10: 'MF'}),
-    0x0302: ('FocusProcess',
-       {0: 'AF Not Used',
-        1: 'AF Used'}),
-    0x0303: ('AFSearch',
-       {0: 'Not Ready',
-        1: 'Ready'}),
-    0x0304: ('AFAreas', ),
-    0x0401: ('FlashExposureCompensation', ),
-    0x0500: ('WhiteBalance2',
-       {0: 'Auto',
-        16: '7500K (Fine Weather with Shade)',
-        17: '6000K (Cloudy)',
-        18: '5300K (Fine Weather)',
-        20: '3000K (Tungsten light)',
-        21: '3600K (Tungsten light-like)',
-        33: '6600K (Daylight fluorescent)',
-        34: '4500K (Neutral white fluorescent)',
-        35: '4000K (Cool white fluorescent)',
-        48: '3600K (Tungsten light-like)',
-        256: 'Custom WB 1',
-        257: 'Custom WB 2',
-        258: 'Custom WB 3',
-        259: 'Custom WB 4',
-        512: 'Custom WB 5400K',
-        513: 'Custom WB 2900K',
-        514: 'Custom WB 8000K', }),
-    0x0501: ('WhiteBalanceTemperature', ),
-    0x0502: ('WhiteBalanceBracket', ),
-    0x0503: ('CustomSaturation', ), # (3 numbers: 1. CS Value, 2. Min, 3. Max)
-    0x0504: ('ModifiedSaturation',
-       {0: 'Off',
-        1: 'CM1 (Red Enhance)',
-        2: 'CM2 (Green Enhance)',
-        3: 'CM3 (Blue Enhance)',
-        4: 'CM4 (Skin Tones)'}),
-    0x0505: ('ContrastSetting', ), # (3 numbers: 1. Contrast, 2. Min, 3. Max)
-    0x0506: ('SharpnessSetting', ), # (3 numbers: 1. Sharpness, 2. Min, 3. Max)
-    0x0507: ('ColorSpace',
-       {0: 'sRGB',
-        1: 'Adobe RGB',
-        2: 'Pro Photo RGB'}),
-    0x0509: ('SceneMode',
-       {0: 'Standard',
-        6: 'Auto',
-        7: 'Sport',
-        8: 'Portrait',
-        9: 'Landscape+Portrait',
-        10: 'Landscape',
-        11: 'Night scene',
-        13: 'Panorama',
-        16: 'Landscape+Portrait',
-        17: 'Night+Portrait',
-        19: 'Fireworks',
-        20: 'Sunset',
-        22: 'Macro',
-        25: 'Documents',
-        26: 'Museum',
-        28: 'Beach&Snow',
-        30: 'Candle',
-        35: 'Underwater Wide1',
-        36: 'Underwater Macro',
-        39: 'High Key',
-        40: 'Digital Image Stabilization',
-        44: 'Underwater Wide2',
-        45: 'Low Key',
-        46: 'Children',
-        48: 'Nature Macro'}),
-    0x050a: ('NoiseReduction',
-       {0: 'Off',
-        1: 'Noise Reduction',
-        2: 'Noise Filter',
-        3: 'Noise Reduction + Noise Filter',
-        4: 'Noise Filter (ISO Boost)',
-        5: 'Noise Reduction + Noise Filter (ISO Boost)'}),
-    0x050b: ('DistortionCorrection',
-       {0: 'Off',
-        1: 'On'}),
-    0x050c: ('ShadingCompensation',
-       {0: 'Off',
-        1: 'On'}),
-    0x050d: ('CompressionFactor', ),
-    0x050f: ('Gradation',
-       {'-1 -1 1': 'Low Key',
-        '0 -1 1': 'Normal',
-        '1 -1 1': 'High Key'}),
-    0x0520: ('PictureMode',
-       {1: 'Vivid',
-        2: 'Natural',
-        3: 'Muted',
-        256: 'Monotone',
-        512: 'Sepia'}),
-    0x0521: ('PictureModeSaturation', ),
-    0x0522: ('PictureModeHue?', ),
-    0x0523: ('PictureModeContrast', ),
-    0x0524: ('PictureModeSharpness', ),
-    0x0525: ('PictureModeBWFilter',
-       {0: 'n/a',
-        1: 'Neutral',
-        2: 'Yellow',
-        3: 'Orange',
-        4: 'Red',
-        5: 'Green'}),
-    0x0526: ('PictureModeTone',
-       {0: 'n/a',
-        1: 'Neutral',
-        2: 'Sepia',
-        3: 'Blue',
-        4: 'Purple',
-        5: 'Green'}),
-    0x0600: ('Sequence', ), # 2 or 3 numbers: 1. Mode, 2. Shot number, 3. Mode bits
-    0x0601: ('PanoramaMode', ), # (2 numbers: 1. Mode, 2. Shot number)
-    0x0603: ('ImageQuality2',
-       {1: 'SQ',
-        2: 'HQ',
-        3: 'SHQ',
-        4: 'RAW'}),
-    0x0901: ('ManometerReading', ),
-    }
-
-
-MAKERNOTE_CASIO_TAGS={
-    0x0001: ('RecordingMode',
-             {1: 'Single Shutter',
-              2: 'Panorama',
-              3: 'Night Scene',
-              4: 'Portrait',
-              5: 'Landscape'}),
-    0x0002: ('Quality',
-             {1: 'Economy',
-              2: 'Normal',
-              3: 'Fine'}),
-    0x0003: ('FocusingMode',
-             {2: 'Macro',
-              3: 'Auto Focus',
-              4: 'Manual Focus',
-              5: 'Infinity'}),
-    0x0004: ('FlashMode',
-             {1: 'Auto',
-              2: 'On',
-              3: 'Off',
-              4: 'Red Eye Reduction'}),
-    0x0005: ('FlashIntensity',
-             {11: 'Weak',
-              13: 'Normal',
-              15: 'Strong'}),
-    0x0006: ('Object Distance', ),
-    0x0007: ('WhiteBalance',
-             {1: 'Auto',
-              2: 'Tungsten',
-              3: 'Daylight',
-              4: 'Fluorescent',
-              5: 'Shade',
-              129: 'Manual'}),
-    0x000B: ('Sharpness',
-             {0: 'Normal',
-              1: 'Soft',
-              2: 'Hard'}),
-    0x000C: ('Contrast',
-             {0: 'Normal',
-              1: 'Low',
-              2: 'High'}),
-    0x000D: ('Saturation',
-             {0: 'Normal',
-              1: 'Low',
-              2: 'High'}),
-    0x0014: ('CCDSpeed',
-             {64: 'Normal',
-              80: 'Normal',
-              100: 'High',
-              125: '+1.0',
-              244: '+3.0',
-              250: '+2.0'}),
-    }
-
-MAKERNOTE_FUJIFILM_TAGS={
-    0x0000: ('NoteVersion', make_string),
-    0x1000: ('Quality', ),
-    0x1001: ('Sharpness',
-             {1: 'Soft',
-              2: 'Soft',
-              3: 'Normal',
-              4: 'Hard',
-              5: 'Hard'}),
-    0x1002: ('WhiteBalance',
-             {0: 'Auto',
-              256: 'Daylight',
-              512: 'Cloudy',
-              768: 'DaylightColor-Fluorescent',
-              769: 'DaywhiteColor-Fluorescent',
-              770: 'White-Fluorescent',
-              1024: 'Incandescent',
-              3840: 'Custom'}),
-    0x1003: ('Color',
-             {0: 'Normal',
-              256: 'High',
-              512: 'Low'}),
-    0x1004: ('Tone',
-             {0: 'Normal',
-              256: 'High',
-              512: 'Low'}),
-    0x1010: ('FlashMode',
-             {0: 'Auto',
-              1: 'On',
-              2: 'Off',
-              3: 'Red Eye Reduction'}),
-    0x1011: ('FlashStrength', ),
-    0x1020: ('Macro',
-             {0: 'Off',
-              1: 'On'}),
-    0x1021: ('FocusMode',
-             {0: 'Auto',
-              1: 'Manual'}),
-    0x1030: ('SlowSync',
-             {0: 'Off',
-              1: 'On'}),
-    0x1031: ('PictureMode',
-             {0: 'Auto',
-              1: 'Portrait',
-              2: 'Landscape',
-              4: 'Sports',
-              5: 'Night',
-              6: 'Program AE',
-              256: 'Aperture Priority AE',
-              512: 'Shutter Priority AE',
-              768: 'Manual Exposure'}),
-    0x1100: ('MotorOrBracket',
-             {0: 'Off',
-              1: 'On'}),
-    0x1300: ('BlurWarning',
-             {0: 'Off',
-              1: 'On'}),
-    0x1301: ('FocusWarning',
-             {0: 'Off',
-              1: 'On'}),
-    0x1302: ('AEWarning',
-             {0: 'Off',
-              1: 'On'}),
-    }
-
-MAKERNOTE_CANON_TAGS = {
-    0x0006: ('ImageType', ),
-    0x0007: ('FirmwareVersion', ),
-    0x0008: ('ImageNumber', ),
-    0x0009: ('OwnerName', ),
-    }
-
-# this is in element offset, name, optional value dictionary format
-MAKERNOTE_CANON_TAG_0x001 = {
-    1: ('Macromode',
-        {1: 'Macro',
-         2: 'Normal'}),
-    2: ('SelfTimer', ),
-    3: ('Quality',
-        {2: 'Normal',
-         3: 'Fine',
-         5: 'Superfine'}),
-    4: ('FlashMode',
-        {0: 'Flash Not Fired',
-         1: 'Auto',
-         2: 'On',
-         3: 'Red-Eye Reduction',
-         4: 'Slow Synchro',
-         5: 'Auto + Red-Eye Reduction',
-         6: 'On + Red-Eye Reduction',
-         16: 'external flash'}),
-    5: ('ContinuousDriveMode',
-        {0: 'Single Or Timer',
-         1: 'Continuous'}),
-    7: ('FocusMode',
-        {0: 'One-Shot',
-         1: 'AI Servo',
-         2: 'AI Focus',
-         3: 'MF',
-         4: 'Single',
-         5: 'Continuous',
-         6: 'MF'}),
-    10: ('ImageSize',
-         {0: 'Large',
-          1: 'Medium',
-          2: 'Small'}),
-    11: ('EasyShootingMode',
-         {0: 'Full Auto',
-          1: 'Manual',
-          2: 'Landscape',
-          3: 'Fast Shutter',
-          4: 'Slow Shutter',
-          5: 'Night',
-          6: 'B&W',
-          7: 'Sepia',
-          8: 'Portrait',
-          9: 'Sports',
-          10: 'Macro/Close-Up',
-          11: 'Pan Focus'}),
-    12: ('DigitalZoom',
-         {0: 'None',
-          1: '2x',
-          2: '4x'}),
-    13: ('Contrast',
-         {0xFFFF: 'Low',
-          0: 'Normal',
-          1: 'High'}),
-    14: ('Saturation',
-         {0xFFFF: 'Low',
-          0: 'Normal',
-          1: 'High'}),
-    15: ('Sharpness',
-         {0xFFFF: 'Low',
-          0: 'Normal',
-          1: 'High'}),
-    16: ('ISO',
-         {0: 'See ISOSpeedRatings Tag',
-          15: 'Auto',
-          16: '50',
-          17: '100',
-          18: '200',
-          19: '400'}),
-    17: ('MeteringMode',
-         {3: 'Evaluative',
-          4: 'Partial',
-          5: 'Center-weighted'}),
-    18: ('FocusType',
-         {0: 'Manual',
-          1: 'Auto',
-          3: 'Close-Up (Macro)',
-          8: 'Locked (Pan Mode)'}),
-    19: ('AFPointSelected',
-         {0x3000: 'None (MF)',
-          0x3001: 'Auto-Selected',
-          0x3002: 'Right',
-          0x3003: 'Center',
-          0x3004: 'Left'}),
-    20: ('ExposureMode',
-         {0: 'Easy Shooting',
-          1: 'Program',
-          2: 'Tv-priority',
-          3: 'Av-priority',
-          4: 'Manual',
-          5: 'A-DEP'}),
-    23: ('LongFocalLengthOfLensInFocalUnits', ),
-    24: ('ShortFocalLengthOfLensInFocalUnits', ),
-    25: ('FocalUnitsPerMM', ),
-    28: ('FlashActivity',
-         {0: 'Did Not Fire',
-          1: 'Fired'}),
-    29: ('FlashDetails',
-         {14: 'External E-TTL',
-          13: 'Internal Flash',
-          11: 'FP Sync Used',
-          7: '2nd("Rear")-Curtain Sync Used',
-          4: 'FP Sync Enabled'}),
-    32: ('FocusMode',
-         {0: 'Single',
-          1: 'Continuous'}),
-    }
-
-MAKERNOTE_CANON_TAG_0x004 = {
-    7: ('WhiteBalance',
-        {0: 'Auto',
-         1: 'Sunny',
-         2: 'Cloudy',
-         3: 'Tungsten',
-         4: 'Fluorescent',
-         5: 'Flash',
-         6: 'Custom'}),
-    9: ('SequenceNumber', ),
-    14: ('AFPointUsed', ),
-    15: ('FlashBias',
-        {0XFFC0: '-2 EV',
-         0XFFCC: '-1.67 EV',
-         0XFFD0: '-1.50 EV',
-         0XFFD4: '-1.33 EV',
-         0XFFE0: '-1 EV',
-         0XFFEC: '-0.67 EV',
-         0XFFF0: '-0.50 EV',
-         0XFFF4: '-0.33 EV',
-         0X0000: '0 EV',
-         0X000C: '0.33 EV',
-         0X0010: '0.50 EV',
-         0X0014: '0.67 EV',
-         0X0020: '1 EV',
-         0X002C: '1.33 EV',
-         0X0030: '1.50 EV',
-         0X0034: '1.67 EV',
-         0X0040: '2 EV'}),
-    19: ('SubjectDistance', ),
-    }
-
-# extract multibyte integer in Motorola format (little endian)
-def s2n_motorola(str):
-    x = 0
-    for c in str:
-        x = (x << 8) | ord(c)
-    return x
-
-# extract multibyte integer in Intel format (big endian)
-def s2n_intel(str):
-    x = 0
-    y = 0L
-    for c in str:
-        x = x | (ord(c) << y)
-        y = y + 8
-    return x
-
-# ratio object that eventually will be able to reduce itself to lowest
-# common denominator for printing
-def gcd(a, b):
-    if b == 0:
-        return a
-    else:
-        return gcd(b, a % b)
-
-class Ratio:
-    def __init__(self, num, den):
-        self.num = num
-        self.den = den
-
-    def __repr__(self):
-        self.reduce()
-        if self.den == 1:
-            return str(self.num)
-        return '%d/%d' % (self.num, self.den)
-
-    def reduce(self):
-        div = gcd(self.num, self.den)
-        if div > 1:
-            self.num = self.num / div
-            self.den = self.den / div
-
-# for ease of dealing with tags
-class IFD_Tag:
-    def __init__(self, printable, tag, field_type, values, field_offset,
-                 field_length):
-        # printable version of data
-        self.printable = printable
-        # tag ID number
-        self.tag = tag
-        # field type as index into FIELD_TYPES
-        self.field_type = field_type
-        # offset of start of field in bytes from beginning of IFD
-        self.field_offset = field_offset
-        # length of data field in bytes
-        self.field_length = field_length
-        # either a string or array of data items
-        self.values = values
-
-    def __str__(self):
-        return self.printable
-
-    def __repr__(self):
-        return '(0x%04X) %s=%s @ %d' % (self.tag,
-                                        FIELD_TYPES[self.field_type][2],
-                                        self.printable,
-                                        self.field_offset)
-
-# class that handles an EXIF header
-class EXIF_header:
-    def __init__(self, file, endian, offset, fake_exif, debug=0):
-        self.file = file
-        self.endian = endian
-        self.offset = offset
-        self.fake_exif = fake_exif
-        self.debug = debug
-        self.tags = {}
-
-    # convert slice to integer, based on sign and endian flags
-    # usually this offset is assumed to be relative to the beginning of the
-    # start of the EXIF information.  For some cameras that use relative tags,
-    # this offset may be relative to some other starting point.
-    def s2n(self, offset, length, signed=0):
-        self.file.seek(self.offset+offset)
-        slice=self.file.read(length)
-        if self.endian == 'I':
-            val=s2n_intel(slice)
-        else:
-            val=s2n_motorola(slice)
-        # Sign extension ?
-        if signed:
-            msb=1L << (8*length-1)
-            if val & msb:
-                val=val-(msb << 1)
-        return val
-
-    # convert offset to string
-    def n2s(self, offset, length):
-        s = ''
-        for dummy in range(length):
-            if self.endian == 'I':
-                s = s + chr(offset & 0xFF)
-            else:
-                s = chr(offset & 0xFF) + s
-            offset = offset >> 8
-        return s
-
-    # return first IFD
-    def first_IFD(self):
-        return self.s2n(4, 4)
-
-    # return pointer to next IFD
-    def next_IFD(self, ifd):
-        entries=self.s2n(ifd, 2)
-        return self.s2n(ifd+2+12*entries, 4)
-
-    # return list of IFDs in header
-    def list_IFDs(self):
-        i=self.first_IFD()
-        a=[]
-        while i:
-            a.append(i)
-            i=self.next_IFD(i)
-        return a
-
-    # return list of entries in this IFD
-    def dump_IFD(self, ifd, ifd_name, dict=EXIF_TAGS, relative=0, stop_tag='UNDEF'):
-        entries=self.s2n(ifd, 2)
-        for i in range(entries):
-            # entry is index of start of this IFD in the file
-            entry = ifd + 2 + 12 * i
-            tag = self.s2n(entry, 2)
-
-            # get tag name early to avoid errors, help debug
-            tag_entry = dict.get(tag)
-            if tag_entry:
-                tag_name = tag_entry[0]
-            else:
-                tag_name = 'Tag 0x%04X' % tag
-
-            # ignore certain tags for faster processing
-            if not (not detailed and tag in IGNORE_TAGS):
-                field_type = self.s2n(entry + 2, 2)
-                if not 0 < field_type < len(FIELD_TYPES):
-                    # unknown field type
-                    raise ValueError('unknown type %d in tag 0x%04X' % (field_type, tag))
-                typelen = FIELD_TYPES[field_type][0]
-                count = self.s2n(entry + 4, 4)
-                offset = entry + 8
-                if count * typelen > 4:
-                    # offset is not the value; it's a pointer to the value
-                    # if relative we set things up so s2n will seek to the right
-                    # place when it adds self.offset.  Note that this 'relative'
-                    # is for the Nikon type 3 makernote.  Other cameras may use
-                    # other relative offsets, which would have to be computed here
-                    # slightly differently.
-                    if relative:
-                        tmp_offset = self.s2n(offset, 4)
-                        offset = tmp_offset + ifd - self.offset + 4
-                        if self.fake_exif:
-                            offset = offset + 18
-                    else:
-                        offset = self.s2n(offset, 4)
-                field_offset = offset
-                if field_type == 2:
-                    # special case: null-terminated ASCII string
-                    if count != 0:
-                        self.file.seek(self.offset + offset)
-                        values = self.file.read(count)
-                        values = values.strip().replace('\x00', '')
-                    else:
-                        values = ''
-                else:
-                    values = []
-                    signed = (field_type in [6, 8, 9, 10])
-                    for dummy in range(count):
-                        if field_type in (5, 10):
-                            # a ratio
-                            value = Ratio(self.s2n(offset, 4, signed),
-                                          self.s2n(offset + 4, 4, signed))
-                        else:
-                            value = self.s2n(offset, typelen, signed)
-                        values.append(value)
-                        offset = offset + typelen
-                # now "values" is either a string or an array
-                if count == 1 and field_type != 2:
-                    printable=str(values[0])
-                else:
-                    printable=str(values)
-                # compute printable version of values
-                if tag_entry:
-                    if len(tag_entry) != 1:
-                        # optional 2nd tag element is present
-                        if callable(tag_entry[1]):
-                            # call mapping function
-                            printable = tag_entry[1](values)
-                        else:
-                            printable = ''
-                            for i in values:
-                                # use lookup table for this tag
-                                printable += tag_entry[1].get(i, repr(i))
-
-                self.tags[ifd_name + ' ' + tag_name] = IFD_Tag(printable, tag,
-                                                          field_type,
-                                                          values, field_offset,
-                                                          count * typelen)
-                if self.debug:
-                    print ' debug:   %s: %s' % (tag_name,
-                                                repr(self.tags[ifd_name + ' ' + tag_name]))
-
-            if tag_name == stop_tag:
-                break
-
-    # extract uncompressed TIFF thumbnail (like pulling teeth)
-    # we take advantage of the pre-existing layout in the thumbnail IFD as
-    # much as possible
-    def extract_TIFF_thumbnail(self, thumb_ifd):
-        entries = self.s2n(thumb_ifd, 2)
-        # this is header plus offset to IFD ...
-        if self.endian == 'M':
-            tiff = 'MM\x00*\x00\x00\x00\x08'
-        else:
-            tiff = 'II*\x00\x08\x00\x00\x00'
-        # ... plus thumbnail IFD data plus a null "next IFD" pointer
-        self.file.seek(self.offset+thumb_ifd)
-        tiff += self.file.read(entries*12+2)+'\x00\x00\x00\x00'
-
-        # fix up large value offset pointers into data area
-        for i in range(entries):
-            entry = thumb_ifd + 2 + 12 * i
-            tag = self.s2n(entry, 2)
-            field_type = self.s2n(entry+2, 2)
-            typelen = FIELD_TYPES[field_type][0]
-            count = self.s2n(entry+4, 4)
-            oldoff = self.s2n(entry+8, 4)
-            # start of the 4-byte pointer area in entry
-            ptr = i * 12 + 18
-            # remember strip offsets location
-            if tag == 0x0111:
-                strip_off = ptr
-                strip_len = count * typelen
-            # is it in the data area?
-            if count * typelen > 4:
-                # update offset pointer (nasty "strings are immutable" crap)
-                # should be able to say "tiff[ptr:ptr+4]=newoff"
-                newoff = len(tiff)
-                tiff = tiff[:ptr] + self.n2s(newoff, 4) + tiff[ptr+4:]
-                # remember strip offsets location
-                if tag == 0x0111:
-                    strip_off = newoff
-                    strip_len = 4
-                # get original data and store it
-                self.file.seek(self.offset + oldoff)
-                tiff += self.file.read(count * typelen)
-
-        # add pixel strips and update strip offset info
-        old_offsets = self.tags['Thumbnail StripOffsets'].values
-        old_counts = self.tags['Thumbnail StripByteCounts'].values
-        for i in range(len(old_offsets)):
-            # update offset pointer (more nasty "strings are immutable" crap)
-            offset = self.n2s(len(tiff), strip_len)
-            tiff = tiff[:strip_off] + offset + tiff[strip_off + strip_len:]
-            strip_off += strip_len
-            # add pixel strip to end
-            self.file.seek(self.offset + old_offsets[i])
-            tiff += self.file.read(old_counts[i])
-
-        self.tags['TIFFThumbnail'] = tiff
-
-    # decode all the camera-specific MakerNote formats
-
-    # Note is the data that comprises this MakerNote.  The MakerNote will
-    # likely have pointers in it that point to other parts of the file.  We'll
-    # use self.offset as the starting point for most of those pointers, since
-    # they are relative to the beginning of the file.
-    #
-    # If the MakerNote is in a newer format, it may use relative addressing
-    # within the MakerNote.  In that case we'll use relative addresses for the
-    # pointers.
-    #
-    # As an aside: it's not just to be annoying that the manufacturers use
-    # relative offsets.  It's so that if the makernote has to be moved by the
-    # picture software all of the offsets don't have to be adjusted.  Overall,
-    # this is probably the right strategy for makernotes, though the spec is
-    # ambiguous.  (The spec does not appear to imagine that makernotes would
-    # follow EXIF format internally.  Once they did, it's ambiguous whether
-    # the offsets should be from the header at the start of all the EXIF info,
-    # or from the header at the start of the makernote.)
-    def decode_maker_note(self):
-        note = self.tags['EXIF MakerNote']
-        make = self.tags['Image Make'].printable
-        # model = self.tags['Image Model'].printable # unused
-
-        # Nikon
-        # The maker note usually starts with the word Nikon, followed by the
-        # type of the makernote (1 or 2, as a short).  If the word Nikon is
-        # not at the start of the makernote, it's probably type 2, since some
-        # cameras work that way.
-        if make in ('NIKON', 'NIKON CORPORATION'):
-            if note.values[0:7] == [78, 105, 107, 111, 110, 0, 1]:
-                if self.debug:
-                    print "Looks like a type 1 Nikon MakerNote."
-                self.dump_IFD(note.field_offset+8, 'MakerNote',
-                              dict=MAKERNOTE_NIKON_OLDER_TAGS)
-            elif note.values[0:7] == [78, 105, 107, 111, 110, 0, 2]:
-                if self.debug:
-                    print "Looks like a labeled type 2 Nikon MakerNote"
-                if note.values[12:14] != [0, 42] and note.values[12:14] != [42L, 0L]:
-                    raise ValueError("Missing marker tag '42' in MakerNote.")
-                # skip the Makernote label and the TIFF header
-                self.dump_IFD(note.field_offset+10+8, 'MakerNote',
-                              dict=MAKERNOTE_NIKON_NEWER_TAGS, relative=1)
-            else:
-                # E99x or D1
-                if self.debug:
-                    print "Looks like an unlabeled type 2 Nikon MakerNote"
-                self.dump_IFD(note.field_offset, 'MakerNote',
-                              dict=MAKERNOTE_NIKON_NEWER_TAGS)
-            return
-
-        # Olympus
-        if make.startswith('OLYMPUS'):
-            self.dump_IFD(note.field_offset+8, 'MakerNote',
-                          dict=MAKERNOTE_OLYMPUS_TAGS)
-            # TODO
-            #for i in (('MakerNote Tag 0x2020', MAKERNOTE_OLYMPUS_TAG_0x2020),):
-            #    self.decode_olympus_tag(self.tags[i[0]].values, i[1])
-            #return
-
-        # Casio
-        if make == 'Casio':
-            self.dump_IFD(note.field_offset, 'MakerNote',
-                          dict=MAKERNOTE_CASIO_TAGS)
-            return
-
-        # Fujifilm
-        if make == 'FUJIFILM':
-            # bug: everything else is "Motorola" endian, but the MakerNote
-            # is "Intel" endian
-            endian = self.endian
-            self.endian = 'I'
-            # bug: IFD offsets are from beginning of MakerNote, not
-            # beginning of file header
-            offset = self.offset
-            self.offset += note.field_offset
-            # process note with bogus values (note is actually at offset 12)
-            self.dump_IFD(12, 'MakerNote', dict=MAKERNOTE_FUJIFILM_TAGS)
-            # reset to correct values
-            self.endian = endian
-            self.offset = offset
-            return
-
-        # Canon
-        if make == 'Canon':
-            self.dump_IFD(note.field_offset, 'MakerNote',
-                          dict=MAKERNOTE_CANON_TAGS)
-            for i in (('MakerNote Tag 0x0001', MAKERNOTE_CANON_TAG_0x001),
-                      ('MakerNote Tag 0x0004', MAKERNOTE_CANON_TAG_0x004)):
-                self.canon_decode_tag(self.tags[i[0]].values, i[1])
-            return
-
-    # decode Olympus MakerNote tag based on offset within tag
-    def olympus_decode_tag(self, value, dict):
-        pass
-
-    # decode Canon MakerNote tag based on offset within tag
-    # see http://www.burren.cx/david/canon.html by David Burren
-    def canon_decode_tag(self, value, dict):
-        for i in range(1, len(value)):
-            x=dict.get(i, ('Unknown', ))
-            if self.debug:
-                print i, x
-            name=x[0]
-            if len(x) > 1:
-                val=x[1].get(value[i], 'Unknown')
-            else:
-                val=value[i]
-            # it's not a real IFD Tag but we fake one to make everybody
-            # happy. this will have a "proprietary" type
-            self.tags['MakerNote '+name]=IFD_Tag(str(val), None, 0, None,
-                                                 None, None)
-
-# process an image file (expects an open file object)
-# this is the function that has to deal with all the arbitrary nasty bits
-# of the EXIF standard
-def process_file(f, stop_tag='UNDEF', details=True, debug=False):
-    # yah it's cheesy...
-    global detailed
-    detailed = details
-
-    # by default do not fake an EXIF beginning
-    fake_exif = 0
-
-    # determine whether it's a JPEG or TIFF
-    data = f.read(12)
-    if data[0:4] in ['II*\x00', 'MM\x00*']:
-        # it's a TIFF file
-        f.seek(0)
-        endian = f.read(1)
-        f.read(1)
-        offset = 0
-    elif data[0:2] == '\xFF\xD8':
-        # it's a JPEG file
-        while data[2] == '\xFF' and data[6:10] in ('JFIF', 'JFXX', 'OLYM', 'Phot'):
-            length = ord(data[4])*256+ord(data[5])
-            f.read(length-8)
-            # fake an EXIF beginning of file
-            data = '\xFF\x00'+f.read(10)
-            fake_exif = 1
-        if data[2] == '\xFF' and data[6:10] == 'Exif':
-            # detected EXIF header
-            offset = f.tell()
-            endian = f.read(1)
-        else:
-            # no EXIF information
-            return {}
-    else:
-        # file format not recognized
-        return {}
-
-    # deal with the EXIF info we found
-    if debug:
-        print {'I': 'Intel', 'M': 'Motorola'}[endian], 'format'
-    hdr = EXIF_header(f, endian, offset, fake_exif, debug)
-    ifd_list = hdr.list_IFDs()
-    ctr = 0
-    for i in ifd_list:
-        if ctr == 0:
-            IFD_name = 'Image'
-        elif ctr == 1:
-            IFD_name = 'Thumbnail'
-            thumb_ifd = i
-        else:
-            IFD_name = 'IFD %d' % ctr
-        if debug:
-            print ' IFD %d (%s) at offset %d:' % (ctr, IFD_name, i)
-        hdr.dump_IFD(i, IFD_name, stop_tag=stop_tag)
-        # EXIF IFD
-        exif_off = hdr.tags.get(IFD_name+' ExifOffset')
-        if exif_off:
-            if debug:
-                print ' EXIF SubIFD at offset %d:' % exif_off.values[0]
-            hdr.dump_IFD(exif_off.values[0], 'EXIF', stop_tag=stop_tag)
-            # Interoperability IFD contained in EXIF IFD
-            intr_off = hdr.tags.get('EXIF SubIFD InteroperabilityOffset')
-            if intr_off:
-                if debug:
-                    print ' EXIF Interoperability SubSubIFD at offset %d:' \
-                          % intr_off.values[0]
-                hdr.dump_IFD(intr_off.values[0], 'EXIF Interoperability',
-                             dict=INTR_TAGS, stop_tag=stop_tag)
-        # GPS IFD
-        gps_off = hdr.tags.get(IFD_name+' GPSInfo')
-        if gps_off:
-            if debug:
-                print ' GPS SubIFD at offset %d:' % gps_off.values[0]
-            hdr.dump_IFD(gps_off.values[0], 'GPS', dict=GPS_TAGS, stop_tag=stop_tag)
-        ctr += 1
-
-    # extract uncompressed TIFF thumbnail
-    thumb = hdr.tags.get('Thumbnail Compression')
-    if thumb and thumb.printable == 'Uncompressed TIFF':
-        hdr.extract_TIFF_thumbnail(thumb_ifd)
-
-    # JPEG thumbnail (thankfully the JPEG data is stored as a unit)
-    thumb_off = hdr.tags.get('Thumbnail JPEGInterchangeFormat')
-    if thumb_off:
-        f.seek(offset+thumb_off.values[0])
-        size = hdr.tags['Thumbnail JPEGInterchangeFormatLength'].values[0]
-        hdr.tags['JPEGThumbnail'] = f.read(size)
-
-    # deal with MakerNote contained in EXIF IFD
-    if 'EXIF MakerNote' in hdr.tags and detailed:
-        hdr.decode_maker_note()
-
-    # Sometimes in a TIFF file, a JPEG thumbnail is hidden in the MakerNote
-    # since it's not allowed in a uncompressed TIFF IFD
-    if 'JPEGThumbnail' not in hdr.tags:
-        thumb_off=hdr.tags.get('MakerNote JPEGThumbnail')
-        if thumb_off:
-            f.seek(offset+thumb_off.values[0])
-            hdr.tags['JPEGThumbnail']=file.read(thumb_off.field_length)
-
-    return hdr.tags
-
-
-# show command line usage
-def usage(exit_status):
-    msg = 'Usage: EXIF.py [OPTIONS] file1 [file2 ...]\n'
-    msg += 'Extract EXIF information from digital camera image files.\n\nOptions:\n'
-    msg += '-q --quick   Do not process MakerNotes.\n'
-    msg += '-t TAG --stop-tag TAG   Stop processing when this tag is retrieved.\n'
-    msg += '-d --debug   Run in debug mode.\n'
-    print msg
-    sys.exit(exit_status)
-
-# library test/debug function (dump given files)
-if __name__ == '__main__':
-    import sys
-    import getopt
-
-    # parse command line options/arguments
-    try:
-        opts, args = getopt.getopt(sys.argv[1:], "hqdt:v", ["help", "quick", "debug", "stop-tag="])
-    except getopt.GetoptError:
-        usage(2)
-    if args == []:
-        usage(2)
-    detailed = True
-    stop_tag = 'UNDEF'
-    debug = False
-    for o, a in opts:
-        if o in ("-h", "--help"):
-            usage(0)
-        if o in ("-q", "--quick"):
-            detailed = False
-        if o in ("-t", "--stop-tag"):
-            stop_tag = a
-        if o in ("-d", "--debug"):
-            debug = True
-
-    # output info for each file
-    for filename in args:
-        try:
-            file=open(filename, 'rb')
-        except:
-            print "'%s' is unreadable\n"%filename
-            continue
-        print filename + ':'
-        # get the tags
-        data = process_file(file, stop_tag=stop_tag, details=detailed, debug=debug)
-        if not data:
-            print 'No EXIF information found'
-            continue
-
-        x=data.keys()
-        x.sort()
-        for i in x:
-            if i in ('JPEGThumbnail', 'TIFFThumbnail'):
-                continue
-            try:
-                print '   %s (%s): %s' % \
-                      (i, FIELD_TYPES[data[i].field_type][2], data[i].printable)
-            except:
-                print 'error', i, '"', data[i], '"'
-        if 'JPEGThumbnail' in data:
-            print 'File has JPEG thumbnail'
-        print
-
--- a/madeira/photologue/utils/reflection.py	Sat Apr 14 15:49:28 2012 -0500
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,93 +0,0 @@
-""" Function for generating web 2.0 style image reflection effects.
-
-Copyright (c) 2007, Justin C. Driscoll
-All rights reserved.
-
-Redistribution and use in source and binary forms, with or without modification,
-are permitted provided that the following conditions are met:
-
-    1. Redistributions of source code must retain the above copyright notice,
-       this list of conditions and the following disclaimer.
-
-    2. Redistributions in binary form must reproduce the above copyright
-       notice, this list of conditions and the following disclaimer in the
-       documentation and/or other materials provided with the distribution.
-
-    3. Neither the name of reflection.py nor the names of its contributors may be used
-       to endorse or promote products derived from this software without
-       specific prior written permission.
-
-THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
-ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
-WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
-ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
-(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
-LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
-ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
-(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
-SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-"""
-
-try:
-    import Image
-    import ImageColor                    
-except ImportError:
-    try:
-        from PIL import Image
-        from PIL import ImageColor
-    except ImportError:
-        raise ImportError("The Python Imaging Library was not found.")
-    
-    
-def add_reflection(im, bgcolor="#00000", amount=0.4, opacity=0.6):
-    """ Returns the supplied PIL Image (im) with a reflection effect
-    
-    bgcolor  The background color of the reflection gradient
-    amount   The height of the reflection as a percentage of the orignal image
-    opacity  The initial opacity of the reflection gradient
-    
-    Originally written for the Photologue image management system for Django
-    and Based on the original concept by Bernd Schlapsi
-    
-    """
-    # convert bgcolor string to rgb value
-    background_color = ImageColor.getrgb(bgcolor)
-    
-    # copy orignial image and flip the orientation
-    reflection = im.copy().transpose(Image.FLIP_TOP_BOTTOM)
-    
-    # create a new image filled with the bgcolor the same size
-    background = Image.new("RGB", im.size, background_color)
-    
-    # calculate our alpha mask
-    start = int(255 - (255 * opacity)) # The start of our gradient
-    steps = int(255 * amount) # the number of intermedite values
-    increment = (255 - start) / float(steps)
-    mask = Image.new('L', (1, 255))
-    for y in range(255):
-        if y < steps:
-            val = int(y * increment + start)
-        else:
-            val = 255
-        mask.putpixel((0, y), val)
-    alpha_mask = mask.resize(im.size)    
-    
-    # merge the reflection onto our background color using the alpha mask
-    reflection = Image.composite(background, reflection, alpha_mask)
-    
-    # crop the reflection
-    reflection_height = int(im.size[1] * amount)
-    reflection = reflection.crop((0, 0, im.size[0], reflection_height))
-    
-    # create new image sized to hold both the original image and the reflection
-    composite = Image.new("RGB", (im.size[0], im.size[1]+reflection_height), background_color)
-    
-    # paste the orignal image and the reflection into the composite image
-    composite.paste(im, (0, 0))
-    composite.paste(reflection, (0, im.size[1]))
-    
-    # return the image complete with reflection effect
-    return composite
-            
\ No newline at end of file
--- a/madeira/photologue/utils/watermark.py	Sat Apr 14 15:49:28 2012 -0500
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,64 +0,0 @@
-""" Function for applying watermarks to images.
-
-Original found here:
-http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/362879
-
-"""
-
-try:
-    import Image
-    import ImageEnhance
-except ImportError:
-    try:
-        from PIL import Image
-        from PIL import ImageEnhance
-    except ImportError:
-        raise ImportError("The Python Imaging Library was not found.")
-
-def reduce_opacity(im, opacity):
-    """Returns an image with reduced opacity."""
-    assert opacity >= 0 and opacity <= 1
-    if im.mode != 'RGBA':
-        im = im.convert('RGBA')
-    else:
-        im = im.copy()
-    alpha = im.split()[3]
-    alpha = ImageEnhance.Brightness(alpha).enhance(opacity)
-    im.putalpha(alpha)
-    return im
-
-def apply_watermark(im, mark, position, opacity=1):
-    """Adds a watermark to an image."""
-    if opacity < 1:
-        mark = reduce_opacity(mark, opacity)
-    if im.mode != 'RGBA':
-        im = im.convert('RGBA')
-    # create a transparent layer the size of the image and draw the
-    # watermark in that layer.
-    layer = Image.new('RGBA', im.size, (0,0,0,0))
-    if position == 'tile':
-        for y in range(0, im.size[1], mark.size[1]):
-            for x in range(0, im.size[0], mark.size[0]):
-                layer.paste(mark, (x, y))
-    elif position == 'scale':
-        # scale, but preserve the aspect ratio
-        ratio = min(
-            float(im.size[0]) / mark.size[0], float(im.size[1]) / mark.size[1])
-        w = int(mark.size[0] * ratio)
-        h = int(mark.size[1] * ratio)
-        mark = mark.resize((w, h))
-        layer.paste(mark, ((im.size[0] - w) / 2, (im.size[1] - h) / 2))
-    else:
-        layer.paste(mark, position)
-    # composite the watermark with the layer
-    return Image.composite(layer, im, layer)
-
-def test():
-    im = Image.open('test.png')
-    mark = Image.open('overlay.png')
-    watermark(im, mark, 'tile', 0.5).show()
-    watermark(im, mark, 'scale', 1.0).show()
-    watermark(im, mark, (100, 100), 0.5).show()
-
-if __name__ == '__main__':
-    test()
--- a/madeira/pl-admin.py	Sat Apr 14 15:49:28 2012 -0500
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,116 +0,0 @@
-import getopt, sys
-
-try:
-    import settings # Assumed to be in the same directory.
-    from django.core.management import setup_environ
-except ImportError:
-    import sys
-    sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n(If the file settings.py does indeed exist, it's causing an ImportError somehow.)\n" % __file__)
-    sys.exit(1)
-
-
-def precache(sizes=[], reset=False):
-    # setup django environment
-    setup_environ(settings)
-
-    # import models
-    from photologue.models import Photo, PhotoSize, PhotoSizeCache
-
-    cache = PhotoSizeCache()
-
-    print 'Caching photos, this may take a while...'
-
-    for photo in Photo.objects.all():
-        if len(sizes):
-            for size in sizes:
-                photosize = cache.sizes.get(size, None)
-                if photosize is None:
-                    print '\nA photosize named "%s" was not found...' % size
-                else:
-                    if reset:
-                        photo.remove_size(photosize)
-                    photo.create_size(photosize)
-        else:
-            for size in caches.sizes.values():
-                if reset:
-                    Photo.remove_size(photosize)
-                photo.create_size(photosize)
-
-    print ' Complete.'
-    sys.exit(2)
-
-
-def reset():
-    # setup django environment
-    setup_environ(settings)
-
-    # import models
-    from photologue.models import Photo, PhotoSize
-
-    print 'Reseting photo cache, this may take a while...'
-
-    for photo in Photo.objects.all():
-        photo.clear_cache()
-
-    print ' Complete.'
-    sys.exit(2)
-
-
-def usage():
-    print """
-
-pl-admin.py - Photologue administration script.
-
-Available Commands:
- pl-admin.py create Resizes and caches all defined photo sizes for each image.
- pl-admin.py reset  Removes all cached images.
-
-Options:
- --reset (-r) If calling create the script will clear the existing photo cache
-              before regenerating the specified size (or sizes)
- --size (-s)  The name of a photosize generate
-
-Usage:
- pl-admin.py [options] command
-
-Examples:
- pl-admin.py -r -s=thumbnail create
- pl-admin.py -s=thumbnail -s=display create
- pl-admin.py reset
-
-"""
-
-def main():
-    try:
-        opts, args = getopt.getopt(sys.argv[1:], "hrs:",
-                                   ["help", "reset", "sizes="])
-    except getopt.GetoptError, err:
-        print str(err)
-        usage()
-        sys.exit(2)
-    r = False
-    s = []
-    for o, a in opts:
-        if o in ("-h", "--help"):
-            usage()
-            sys.exit(2)
-        if o in ("-r", "--reset"):
-            r = True
-        elif o in ("-s", "--sizes"):
-            s.append(a.strip('='))
-        else:
-            usage()
-            sys.exit(2)
-
-    if len(args) == 1:
-        command = args[0]
-        if command == 'create':
-            precache(s, r)
-        elif command == 'reset':
-            reset()
-
-    usage()
-
-
-if __name__ == '__main__':
-    main()
--- a/madeira/settings/base.py	Sat Apr 14 15:49:28 2012 -0500
+++ b/madeira/settings/base.py	Sat Apr 14 16:40:29 2012 -0500
@@ -77,7 +77,7 @@
 # Don't forget to use absolute paths, not relative paths.
 TEMPLATE_DIRS = [
     os.path.join(PROJECT_PATH, 'templates'),
-    os.path.join(PROJECT_PATH, 'photologue', 'templates'),
+    os.path.abspath(os.path.join(PROJECT_PATH, '..', 'photologue', 'templates')),
 ]
 
 TEMPLATE_CONTEXT_PROCESSORS = [
--- a/madeira/settings/local.py	Sat Apr 14 15:49:28 2012 -0500
+++ b/madeira/settings/local.py	Sat Apr 14 16:40:29 2012 -0500
@@ -3,7 +3,7 @@
 The contents of this file will vary depending on the local installation.
 
 """
-from settings.base import *
+from madeira.settings.base import *
 
 DATABASES = {
     'default': {
--- a/madeira/settings/production.py	Sat Apr 14 15:49:28 2012 -0500
+++ b/madeira/settings/production.py	Sat Apr 14 16:40:29 2012 -0500
@@ -1,6 +1,6 @@
 # Django production settings for the madeira project.
 
-from settings.base import *
+from madeira.settings.base import *
 
 DEBUG = False
 TEMPLATE_DEBUG = DEBUG
--- a/madeira/settings/test.py	Sat Apr 14 15:49:28 2012 -0500
+++ b/madeira/settings/test.py	Sat Apr 14 16:40:29 2012 -0500
@@ -4,8 +4,8 @@
 http://blog.davidziegler.net/post/370368042/test-database-settings-in-django
 
 """
-from settings.base import *
-from settings.local import *
+from madeira.settings.base import *
+from madeira.settings.local import *
 
 DATABASES = {
     'default': {
--- a/madeira/videos/admin.py	Sat Apr 14 15:49:28 2012 -0500
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,29 +0,0 @@
-"""
-Automatic admin definitions for the models in the videos application.
-
-"""
-from django.contrib import admin
-from django.conf import settings
-
-from videos.models import Collection, Video
-
-
-class VideoInline(admin.StackedInline):
-   model = Video
-
-
-class CollectionAdmin(admin.ModelAdmin):
-    list_filter = ['date_added']
-    list_display = ['title', 'date_added']
-    inlines = [VideoInline]
-
-    class Media:
-        js = ['js/videos/videos_admin.js'] + settings.THIRD_PARTY_JS['tiny_mce']
-
-
-class VideoAdmin(admin.ModelAdmin):
-    list_display = ['title', 'collection']
-
-
-admin.site.register(Collection, CollectionAdmin)
-admin.site.register(Video, VideoAdmin)
--- a/madeira/videos/management/commands/import_old_videos.py	Sat Apr 14 15:49:28 2012 -0500
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,69 +0,0 @@
-"""
-import_old_videos.py - For importing video data from the older version of this
-website.
-
-"""
-import datetime
-
-from django.core.management.base import LabelCommand
-from django.utils import simplejson as json
-from django.utils.html import linebreaks
-
-from videos.models import Collection, Video
-
-
-class Command(LabelCommand):
-    args = '<filename filename ...>'
-    help = 'Imports older video & video sets in JSON format'
-
-    collections = {}
-
-    def handle_label(self, filename, **options):
-        """
-        Process the file of older video & video sets in JSON. Convert to the new
-        model scheme.
-
-        """
-        with open(filename, 'rb') as f:
-            items = json.load(f)
-
-        for item in items:
-            if item['model'] == 'band.video_set':
-                self.process_set(item)
-
-        for item in items:
-            if item['model'] == 'band.video':
-                self.process_video(item)
-
-    def process_set(self, item):
-
-        fields = item['fields']
-
-        description = linebreaks(fields['text'].strip())
-
-        # there are several sets with the same date, so to get the ordering
-        # right, add the pk as seconds.
-
-        date_added = datetime.datetime.strptime(fields['date'], '%Y-%m-%d')
-        date_added += datetime.timedelta(seconds=int(item['pk']))
-
-        coll = Collection(
-                id=item['pk'],
-                title=fields['title'].strip(),
-                date_added=date_added,
-                description=description)
-
-        coll.save()
-        self.collections[coll.pk] = coll
-
-    def process_video(self, item):
-
-        fields = item['fields']
-
-        video = Video(
-                id=item['pk'],
-                title=fields['title'].strip(),
-                embed_code=fields['embed_code'],
-                collection=self.collections[fields['video_set']],
-                )
-        video.save()
--- a/madeira/videos/models.py	Sat Apr 14 15:49:28 2012 -0500
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,42 +0,0 @@
-"""
-Models for the videos application.
-
-"""
-from django.db import models
-
-
-class Collection(models.Model):
-    """
-    This model represents a collection of videos.
-
-    """
-    title = models.CharField(max_length=64)
-    description = models.TextField()
-    date_added = models.DateTimeField()
-
-    class Meta:
-        ordering = ['-date_added']
-
-    def __unicode__(self):
-        return self.title
-
-    @models.permalink
-    def get_absolute_url(self):
-        return ('videos-item', [], {'pk': str(self.id)})
-
-
-class Video(models.Model):
-    """
-    This model represents a video clip hosted on a remote video sharing site
-    (e.g. YouTube).
-
-    """
-    title = models.CharField(max_length=64)
-    embed_code = models.TextField()
-    collection = models.ForeignKey(Collection)
-
-    class Meta:
-        ordering = ['title']
-
-    def __unicode__(self):
-        return self.title
--- a/madeira/videos/static/js/videos/videos_admin.js	Sat Apr 14 15:49:28 2012 -0500
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,3 +0,0 @@
-django.jQuery(document).ready(function() {
-   django.jQuery('.embed_code textarea').addClass('mceNoEditor');      
-});
--- a/madeira/videos/urls.py	Sat Apr 14 15:49:28 2012 -0500
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,20 +0,0 @@
-"""
-Urls for the videos application.
-
-"""
-from django.conf.urls import patterns, url
-from django.views.generic import DetailView, ListView
-
-from videos.models import Collection
-
-
-urlpatterns = patterns('',
-   url(r'^$',
-       ListView.as_view(
-           model=Collection,
-           context_object_name='collection_list'),
-       name='videos-index'),
-   url(r'^(?P<pk>\d+)/$',
-       DetailView.as_view(model=Collection, context_object_name='collection'),
-       name='videos-item')
-)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/manage.py	Sat Apr 14 16:40:29 2012 -0500
@@ -0,0 +1,10 @@
+#!/usr/bin/env python
+import os
+import sys
+
+if __name__ == "__main__":
+    os.environ.setdefault("DJANGO_SETTINGS_MODULE", "madeira.settings")
+
+    from django.core.management import execute_from_command_line
+
+    execute_from_command_line(sys.argv)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mp3/admin.py	Sat Apr 14 16:40:29 2012 -0500
@@ -0,0 +1,29 @@
+"""
+Automatic admin definitions for the models in the mp3 application.
+
+"""
+from django.contrib import admin
+from django.conf import settings
+
+from mp3.models import Collection, Song
+
+
+class SongInline(admin.TabularInline):
+   model = Song
+
+
+class CollectionAdmin(admin.ModelAdmin):
+    list_filter = ['date_added']
+    list_display = ['title', 'date_added']
+    inlines = [SongInline]
+
+    class Media:
+        js = settings.THIRD_PARTY_JS['tiny_mce']
+
+
+class SongAdmin(admin.ModelAdmin):
+    list_display = ['title', 'collection']
+
+
+admin.site.register(Collection, CollectionAdmin)
+admin.site.register(Song, SongAdmin)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mp3/management/commands/import_old_mp3.py	Sat Apr 14 16:40:29 2012 -0500
@@ -0,0 +1,64 @@
+"""
+import_old_mp3.py - For importing mp3 data from the older version of this
+website.
+
+"""
+import datetime
+
+from django.core.management.base import LabelCommand
+from django.utils import simplejson as json
+from django.utils.html import linebreaks
+
+from mp3.models import Collection, Song
+
+
+class Command(LabelCommand):
+    args = '<filename filename ...>'
+    help = 'Imports older mp3 & mp3 sets in JSON format'
+
+    collections = {}
+
+    def handle_label(self, filename, **options):
+        """
+        Process the file of older mp3 & mp3 sets in JSON. Convert to the new
+        model scheme.
+
+        """
+        with open(filename, 'rb') as f:
+            items = json.load(f)
+
+        for item in items:
+            if item['model'] == 'band.mp3_set':
+                self.process_mp3_set(item)
+
+        for item in items:
+            if item['model'] == 'band.mp3':
+                self.process_mp3(item)
+
+    def process_mp3_set(self, item):
+
+        fields = item['fields']
+
+        description = linebreaks(fields['text'].strip())
+
+        coll = Collection(
+                id=item['pk'],
+                title=fields['title'].strip(),
+                date_added=datetime.datetime.strptime(fields['date'], '%Y-%m-%d'),
+                description=description)
+
+        coll.save()
+        self.collections[coll.pk] = coll
+
+    def process_mp3(self, item):
+
+        fields = item['fields']
+
+        song = Song(
+                id=item['pk'],
+                title=fields['title'].strip(),
+                description=fields['desc'].strip(),
+                file=fields['file'],
+                collection=self.collections[fields['mp3_set']],
+                )
+        song.save()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mp3/models.py	Sat Apr 14 16:40:29 2012 -0500
@@ -0,0 +1,39 @@
+"""
+Models for the mp3 application.
+
+"""
+from django.db import models
+
+
+class Collection(models.Model):
+    """
+    This model represents a collection of songs.
+
+    """
+    title = models.CharField(max_length=64)
+    description = models.TextField()
+    date_added = models.DateTimeField()
+
+    class Meta:
+        ordering = ['-date_added']
+
+    def __unicode__(self):
+        return self.title
+
+
+class Song(models.Model):
+    """
+    This model represents an uploaded song file.
+
+    """
+    title = models.CharField(max_length=64)
+    description = models.CharField(max_length=255, blank=True)
+    file = models.FileField(upload_to='mp3s/%Y/%m/%d/')
+    collection = models.ForeignKey(Collection)
+
+    class Meta:
+        ordering = ['title']
+
+    def __unicode__(self):
+        return self.title
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mp3/urls.py	Sat Apr 14 16:40:29 2012 -0500
@@ -0,0 +1,18 @@
+"""
+Urls for the mp3 application.
+
+"""
+from django.conf.urls import patterns, url
+from django.views.generic import ListView
+
+from mp3.models import Collection
+
+
+urlpatterns = patterns('',
+   url(r'^$',
+       ListView.as_view(
+           model=Collection,
+           paginate_by=10,
+           context_object_name='collection_list'),
+       name='mp3-index'),
+)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/news/admin.py	Sat Apr 14 16:40:29 2012 -0500
@@ -0,0 +1,19 @@
+"""
+Automatic admin definitions for the models in the news application.
+
+"""
+from django.contrib import admin
+from django.conf import settings
+
+from news.models import News
+
+
+class NewsAdmin(admin.ModelAdmin):
+    list_filter = ['date']
+    search_fields = ['title', 'content']
+    save_on_top = True
+
+    class Media:
+        js = settings.THIRD_PARTY_JS['tiny_mce']
+
+admin.site.register(News, NewsAdmin)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/news/management/commands/import_old_news.py	Sat Apr 14 16:40:29 2012 -0500
@@ -0,0 +1,65 @@
+"""
+import_old_news.py - For importing news stories from the older version of this
+website.
+
+"""
+import datetime
+
+from django.conf import settings
+from django.core.management.base import LabelCommand
+from django.utils import simplejson as json
+from django.utils.html import linebreaks
+import textile
+
+from news.models import News
+
+IMG_TAG = u"""\
+<img src="{src}" class="floatLeftBox" alt="{title}" title="{title}" border="0" />"""
+
+
+class Command(LabelCommand):
+    args = '<filename filename ...>'
+    help = 'Imports older news stories in JSON format'
+
+    def handle_label(self, filename, **options):
+        """
+        Process the file of older news stories in JSON. Convert to the new model
+        scheme.
+
+        """
+        with open(filename, 'rb') as f:
+            items = json.load(f)
+
+        for item in items:
+            if item['model'] == 'band.news':
+                self.process_item(item)
+
+    def process_item(self, item):
+
+        fields = item['fields']
+
+        content = fields['text'].strip()
+        if fields['markup_enabled']:
+            content = textile.textile(content, encoding='utf-8', output='utf-8')
+        else:
+            content = linebreaks(fields['text'])
+
+        author = fields['author'].strip()
+        if author:
+            content += u"<p>\u2013&nbsp;%s" % author
+
+        image = fields['photo'].strip()
+        if image:
+            caption = fields['photo_caption'].strip()
+            caption = caption if caption else 'Image'
+
+            src = u"%s%s" % (settings.MEDIA_URL, image)
+
+            content = IMG_TAG.format(src=src, title=caption) + content
+
+        news = News(id=item['pk'],
+                title=fields['title'].strip(),
+                date=datetime.datetime.strptime(fields['date'], '%Y-%m-%d'),
+                content=content)
+
+        news.save()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/news/models.py	Sat Apr 14 16:40:29 2012 -0500
@@ -0,0 +1,29 @@
+"""
+Models for the news application.
+
+"""
+from django.db import models
+
+
+class News(models.Model):
+    """
+    This model represents all the info we store about each news entry.
+
+    """
+    title = models.CharField(max_length=128, blank=True)
+    date = models.DateTimeField(db_index=True)
+    content = models.TextField()
+
+    def __unicode__(self):
+        date_str = self.date.strftime('%m/%d/%Y')
+        if self.title:
+            return u'%s %s' % (date_str, self.title)
+        return u'%s' % date_str
+
+    @models.permalink
+    def get_absolute_url(self):
+        return ('news-item', [], {'pk': str(self.id)})
+
+    class Meta:
+        verbose_name_plural = 'News'
+        ordering = ['-date']
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/news/urls.py	Sat Apr 14 16:40:29 2012 -0500
@@ -0,0 +1,21 @@
+"""
+Urls for the news application.
+
+"""
+from django.conf.urls import patterns, url
+from django.views.generic import DetailView, ListView
+
+from news.models import News
+
+
+urlpatterns = patterns('',
+   url(r'^$',
+       ListView.as_view(
+           model=News,
+           paginate_by=10,
+           context_object_name='news_list'),
+       name='news-index'),
+   url(r'^story/(?P<pk>\d+)/$',
+       DetailView.as_view(model=News, context_object_name='story'),
+       name='news-item')
+)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/photologue/LICENSE.txt	Sat Apr 14 16:40:29 2012 -0500
@@ -0,0 +1,27 @@
+Copyright (c) 2007-2008, Justin C. Driscoll
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+    1. Redistributions of source code must retain the above copyright notice,
+       this list of conditions and the following disclaimer.
+
+    2. Redistributions in binary form must reproduce the above copyright
+       notice, this list of conditions and the following disclaimer in the
+       documentation and/or other materials provided with the distribution.
+
+    3. Neither the name of django-photologue nor the names of its contributors may be used
+       to endorse or promote products derived from this software without
+       specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/photologue/README.txt	Sat Apr 14 16:40:29 2012 -0500
@@ -0,0 +1,38 @@
+Installation
+
+Step 1 - Download Photologue
+
+Photologue can be downloaded below or from the project page. Older versions are also available from the project page and users who like to live on the edge can checkout a copy of the latest trunk revision.
+
+Step 2 - Add Photologue To Your Project
+
+Copy the entire Photologue application folder (the folder named 'photologue' that contains 'models.py') to a location on your Python path such as your project root. Your project root is typically the directory where your 'settings.py' is found.
+
+Step 3 - Configure Your Settings
+
+Add 'photologue' to your INSTALLED_APPS setting:
+
+    INSTALLED_APPS = (
+         # ...other installed applications,
+         'photologue',
+    )
+
+Confirm that your MEDIA_ROOT and MEDIA_URL settings are correct.
+
+If you want to tweak things even more you can also over-ride a few default settings (optional, see documentation for more information on the available settings).
+
+Step 4 - Register Photologue with the Django Admin
+
+Add the following to your projects urls.py file:
+
+    from django.contrib import admin
+    
+    admin.autodiscover()
+
+Step 4 - Sync Your Database
+
+Run the 'manage.py syndb' command to create the appropriate tables. After the database in initialized, Photologue will walk you through creating some default models.
+
+Additional documentation available here:
+
+http://code.google.com/p/django-photologue/w/list
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/photologue/admin.py	Sat Apr 14 16:40:29 2012 -0500
@@ -0,0 +1,60 @@
+""" Newforms Admin configuration for Photologue
+
+"""
+from django.contrib import admin
+from models import *
+
+class GalleryAdmin(admin.ModelAdmin):
+    list_display = ('title', 'date_added', 'photo_count', 'is_public')
+    list_filter = ['date_added', 'is_public']
+    date_hierarchy = 'date_added'
+    prepopulated_fields = {'title_slug': ('title',)}
+    filter_horizontal = ('photos',)    
+    
+class PhotoAdmin(admin.ModelAdmin):
+    list_display = ('title', 'date_taken', 'date_added', 'is_public', 'tags', 'view_count', 'admin_thumbnail')
+    list_filter = ['date_added', 'is_public']
+    list_per_page = 10
+    prepopulated_fields = {'title_slug': ('title',)}
+    
+class PhotoEffectAdmin(admin.ModelAdmin):
+    list_display = ('name', 'description', 'admin_sample')
+    fieldsets = (
+        (None, {
+            'fields': ('name', 'description')
+        }),
+        ('Adjustments', {
+            'fields': ('color', 'brightness', 'contrast', 'sharpness')
+        }),
+        ('Filters', {
+            'fields': ('filters',)
+        }),
+        ('Reflection', {
+            'fields': ('reflection_size', 'reflection_strength', 'background_color')
+        }),
+    )
+
+class PhotoSizeAdmin(admin.ModelAdmin):
+    list_display = ('name', 'width', 'height', 'crop', 'pre_cache', 'effect', 'increment_count')
+    fieldsets = (
+        (None, {
+            'fields': ('name', 'width', 'height', 'quality')
+        }),
+        ('Options', {
+            'fields': ('upscale', 'crop', 'pre_cache', 'increment_count')
+        }),
+        ('Enhancements', {
+            'fields': ('effect', 'watermark',)
+        }),
+    )
+    
+class WatermarkAdmin(admin.ModelAdmin):
+    list_display = ('name', 'opacity', 'style')
+
+
+admin.site.register(Gallery, GalleryAdmin)
+admin.site.register(GalleryUpload)
+admin.site.register(Photo, PhotoAdmin)
+admin.site.register(PhotoEffect, PhotoEffectAdmin)
+admin.site.register(PhotoSize, PhotoSizeAdmin)
+admin.site.register(Watermark, WatermarkAdmin)
\ No newline at end of file
Binary file photologue/locale/pl/LC_MESSAGES/django.mo has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/photologue/locale/pl/LC_MESSAGES/django.po	Sat Apr 14 16:40:29 2012 -0500
@@ -0,0 +1,419 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Photologue Preview 2\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2008-07-22 23:05+0200\n"
+"PO-Revision-Date: 2008-07-22 23:08+0100\n"
+"Last-Translator: Jakub Wiśniowski <restless.being@gmail.com>\n"
+"Language-Team: Jakub Wiśniowski <restless.being@gmail.com>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=3; plural=n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\\n\n"
+"X-Poedit-Language: Polish\n"
+"X-Poedit-Country: POLAND\n"
+"X-Poedit-SourceCharset: utf-8\n"
+
+#: models.py:32
+msgid "Photologue was unable to import the Python Imaging Library. Please confirm it`s installed and available on your current Python path."
+msgstr "Photologue nie był w stanie zaimportować Python Imaging Library. Upewnij się, że pakiet ten jest zainstalowany i znajduje się w ścieżce dostępnej dla Pythona."
+
+#: models.py:38
+msgid "Separate tags with spaces, put quotes around multiple-word tags."
+msgstr "Rozdziel tagi spacjami, ujmij w cudzysłowy tagi złożone z wielu słów."
+
+#: models.py:47
+msgid "Django-tagging was not found, tags will be treated as plain text."
+msgstr "Django-tagging nie zostało znalezione. Tagi będą traktowane jako czysty tekst."
+
+#: models.py:64
+msgid "Very Low"
+msgstr "Bardzo niska"
+
+#: models.py:65
+msgid "Low"
+msgstr "Niska"
+
+#: models.py:66
+msgid "Medium-Low"
+msgstr "Niższa średnia"
+
+#: models.py:67
+msgid "Medium"
+msgstr "Åšrednia"
+
+#: models.py:68
+msgid "Medium-High"
+msgstr "Wyższa średnia"
+
+#: models.py:69
+msgid "High"
+msgstr "Wysoka"
+
+#: models.py:70
+msgid "Very High"
+msgstr "Bardzo wysoka"
+
+#: models.py:75
+msgid "Top"
+msgstr "Góra"
+
+#: models.py:76
+msgid "Right"
+msgstr "Prawo"
+
+#: models.py:77
+msgid "Bottom"
+msgstr "Dół"
+
+#: models.py:78
+msgid "Left"
+msgstr "Lewo"
+
+#: models.py:79
+msgid "Center (Default)"
+msgstr "Środek (Domyślnie)"
+
+#: models.py:83
+msgid "Flip left to right"
+msgstr "Odbij w poziomie"
+
+#: models.py:84
+msgid "Flip top to bottom"
+msgstr "Odbij w pionie"
+
+#: models.py:85
+msgid "Rotate 90 degrees counter-clockwise"
+msgstr "Odwróć 90 stopni w lewo"
+
+#: models.py:86
+msgid "Rotate 90 degrees clockwise"
+msgstr "Odwróć 90 stopni w prawo"
+
+#: models.py:87
+msgid "Rotate 180 degrees"
+msgstr "Obróć o 180 stopni"
+
+#: models.py:91
+msgid "Tile"
+msgstr "Kafelki"
+
+#: models.py:92
+msgid "Scale"
+msgstr "Skaluj"
+
+#: models.py:102
+#, python-format
+msgid "Chain multiple filters using the following pattern \"FILTER_ONE->FILTER_TWO->FILTER_THREE\". Image filters will be applied in order. The following filter are available: %s."
+msgstr "Połącz wiele filtrów używając następującego wzorca: \"FILTR_PIERWSZY->FILTR_DRUGI->FILTR_TRZECI\". Filtry obrazów będą zastosowane w kolejności. Dostępne są następujące filtry: %s."
+
+#: models.py:107
+msgid "date published"
+msgstr "data publikacji"
+
+#: models.py:108
+#: models.py:164
+#: models.py:448
+msgid "title"
+msgstr "tytuł"
+
+#: models.py:109
+msgid "title slug"
+msgstr "tytuł - slug "
+
+#: models.py:110
+msgid "A \"slug\" is a unique URL-friendly title for an object."
+msgstr "\"Slug\" jest unikalnym, zgodnym z formatem dla URL-i tytułem obiektu."
+
+#: models.py:111
+#: models.py:166
+#: models.py:483
+msgid "description"
+msgstr "opis"
+
+#: models.py:112
+#: models.py:167
+#: models.py:453
+msgid "is public"
+msgstr "jest publiczna"
+
+#: models.py:113
+msgid "Public galleries will be displayed in the default views."
+msgstr "Galerie publiczne będą wyświetlana w domyślnych widokach."
+
+#: models.py:114
+#: models.py:460
+msgid "photos"
+msgstr "zdjęcia"
+
+#: models.py:116
+#: models.py:168
+#: models.py:454
+msgid "tags"
+msgstr "tagi"
+
+#: models.py:121
+msgid "gallery"
+msgstr "galeria"
+
+#: models.py:122
+msgid "galleries"
+msgstr "galerie"
+
+#: models.py:155
+msgid "count"
+msgstr "ilość"
+
+#: models.py:162
+msgid "images file (.zip)"
+msgstr "plik z obrazami (.zip)"
+
+#: models.py:163
+msgid "Select a .zip file of images to upload into a new Gallery."
+msgstr "Wybierz plik .zip zawierający zdjęcia które chcesz załadować do nowej Galerii."
+
+#: models.py:164
+msgid "All photos in the gallery will be given a title made up of the gallery title + a sequential number."
+msgstr "Wszystkie "
+
+#: models.py:165
+#: models.py:451
+msgid "caption"
+msgstr "podpis"
+
+#: models.py:165
+msgid "Caption will be added to all photos."
+msgstr "Podpis będzie dodany do wszystkich zdjęć."
+
+#: models.py:166
+msgid "A description of this Gallery."
+msgstr "Opis tej Galerii."
+
+#: models.py:167
+msgid "Uncheck this to make the uploaded gallery and included photographs private."
+msgstr "Odznacz aby uczynić wrzucaną galerię oraz zawarte w niej zdjęcia prywatnymi."
+
+#: models.py:171
+msgid "gallery upload"
+msgstr "wrzucona galeria"
+
+#: models.py:172
+msgid "gallery uploads"
+msgstr "wrzucone galerie"
+
+#: models.py:228
+#: models.py:594
+msgid "image"
+msgstr "obraz"
+
+#: models.py:229
+msgid "date taken"
+msgstr "data wykonania"
+
+#: models.py:231
+msgid "crop from"
+msgstr "obetnij z"
+
+#: models.py:232
+msgid "effect"
+msgstr "efekt"
+
+#: models.py:250
+msgid "An \"admin_thumbnail\" photo size has not been defined."
+msgstr "Rozmiar zdjęcia \"admin_thumbnail\" nie został zdefiniowany."
+
+#: models.py:258
+msgid "Thumbnail"
+msgstr "Miniaturka"
+
+#: models.py:449
+msgid "slug"
+msgstr "slug"
+
+#: models.py:452
+msgid "date added"
+msgstr "data dodania"
+
+#: models.py:453
+msgid "Public photographs will be displayed in the default views."
+msgstr "Publiczne zdjęcia będą wyświetlane w domyślnych widokach."
+
+#: models.py:459
+msgid "photo"
+msgstr "zdjęcie"
+
+#: models.py:482
+#: models.py:608
+msgid "name"
+msgstr "nazwa"
+
+#: models.py:554
+msgid "rotate or flip"
+msgstr "obróć lub odbij"
+
+#: models.py:555
+#: models.py:562
+msgid "color"
+msgstr "kolor"
+
+#: models.py:555
+msgid "A factor of 0.0 gives a black and white image, a factor of 1.0 gives the original image."
+msgstr "Współczynnik 0.0 daje czarno-biały obraz, współczynnik 1.0 daje obraz oryginalny."
+
+#: models.py:556
+msgid "brightness"
+msgstr "jasność"
+
+#: models.py:556
+msgid "A factor of 0.0 gives a black image, a factor of 1.0 gives the original image."
+msgstr "Współczynnik 0.0 daje czarny obraz, współczynnik 1.0 daje obraz oryginalny."
+
+#: models.py:557
+msgid "contrast"
+msgstr "kontrast"
+
+#: models.py:557
+msgid "A factor of 0.0 gives a solid grey image, a factor of 1.0 gives the original image."
+msgstr "Współczynnik 0.0 daje jednolity szary obraz, współczynnik 1.0 daje obraz oryginalny."
+
+#: models.py:558
+msgid "sharpness"
+msgstr "ostrość"
+
+#: models.py:558
+msgid "A factor of 0.0 gives a blurred image, a factor of 1.0 gives the original image."
+msgstr "Współczynnik 0.0 daje rozmazany obraz, współczynnik 1.0 daje obraz oryginalny."
+
+#: models.py:559
+msgid "filters"
+msgstr "filtry"
+
+#: models.py:560
+msgid "size"
+msgstr "rozmiar"
+
+#: models.py:560
+msgid "The height of the reflection as a percentage of the orignal image. A factor of 0.0 adds no reflection, a factor of 1.0 adds a reflection equal to the height of the orignal image."
+msgstr "Wysokość odbicia jako procent oryginalnego obrazu. Współczynnik 0.0 nie dodaje odbicia, współczynnik 1.0 dodaje odbicie równe wysokości oryginalnego obrazu."
+
+#: models.py:561
+msgid "strength"
+msgstr "intensywność"
+
+#: models.py:565
+#: models.py:616
+msgid "photo effect"
+msgstr "efekt zdjęcia"
+
+#: models.py:566
+msgid "photo effects"
+msgstr "efekty zdjęć"
+
+#: models.py:595
+msgid "style"
+msgstr "styl"
+
+#: models.py:596
+msgid "opacity"
+msgstr "przeźroczystość"
+
+#: models.py:596
+msgid "The opacity of the overlay."
+msgstr "Poziom przezroczystości"
+
+#: models.py:599
+msgid "watermark"
+msgstr "znak wodny"
+
+#: models.py:600
+msgid "watermarks"
+msgstr "znaki wodne"
+
+#: models.py:608
+msgid "Photo size name should contain only letters, numbers and underscores. Examples: \"thumbnail\", \"display\", \"small\", \"main_page_widget\"."
+msgstr "Nazwa rozmiaru zdjęcia powinna zawierać tylko litery, cyfry i podkreślenia. Przykłady: \"miniatura\", \"wystawa\", \"male\", \"widget_strony_glownej\"."
+
+#: models.py:609
+msgid "width"
+msgstr "szerokość"
+
+#: models.py:609
+msgid "If width is set to \"0\" the image will be scaled to the supplied height."
+msgstr "Jeśli szerokość jest ustawiona na \"0\" to obraz będzie skalowany do podanej wysokości."
+
+#: models.py:610
+msgid "height"
+msgstr "wysokość"
+
+#: models.py:610
+msgid "If height is set to \"0\" the image will be scaled to the supplied width"
+msgstr "Jeśli wysokość jest ustawiona na \"0\" to obraz będzie skalowany do podanej szerokości."
+
+#: models.py:611
+msgid "quality"
+msgstr "jakość"
+
+#: models.py:611
+msgid "JPEG image quality."
+msgstr "Jakość obrazu JPEG"
+
+#: models.py:612
+msgid "upscale images?"
+msgstr "skalować obrazy w górę?"
+
+#: models.py:612
+msgid "If selected the image will be scaled up if necessary to fit the supplied dimensions. Cropped sizes will be upscaled regardless of this setting."
+msgstr "Jeśli zaznaczone to obraz będzie skalowany w górę tak aby pasował do podanych wymiarów. Obcinane rozmiary będą skalowane niezależnie od tego ustawienia."
+
+#: models.py:613
+msgid "crop to fit?"
+msgstr "przyciąć aby pasował?"
+
+#: models.py:613
+msgid "If selected the image will be scaled and cropped to fit the supplied dimensions."
+msgstr "Jeśli zaznaczone to obraz będzie skalowany i przycinany tak aby pasował do podanych wymiarów."
+
+#: models.py:614
+msgid "pre-cache?"
+msgstr "wstępnie cachować?"
+
+#: models.py:614
+msgid "If selected this photo size will be pre-cached as photos are added."
+msgstr "Jesli zaznaczone to ten rozmiar zdjęć będzie wstępnie cachowany przy dodawaniu zdjęć."
+
+#: models.py:615
+msgid "increment view count?"
+msgstr "zwiększyć licznik odsłon?"
+
+#: models.py:615
+msgid "If selected the image's \"view_count\" will be incremented when this photo size is displayed."
+msgstr "Jeśli zaznaczone to \"licznik_odslon\" będzie zwiększany gdy ten rozmiar zdjęcia będzie wyświetlany."
+
+#: models.py:617
+msgid "watermark image"
+msgstr "oznacz kluczem wodnym"
+
+#: models.py:621
+msgid "photo size"
+msgstr "rozmiar zdjęcia"
+
+#: models.py:622
+msgid "photo sizes"
+msgstr "rozmiary zdjęć"
+
+#: models.py:640
+msgid "A PhotoSize must have a positive height or width."
+msgstr "PhotoSize musi mieć dodatnią wysokość i szerokość."
+
+#~ msgid "Leave to size the image to the set height"
+#~ msgstr "Ustaw aby przeskalować obraz do wybranej wysokości"
+#~ msgid "Leave to size the image to the set width"
+#~ msgstr "Ustaw aby przeskalować obraz do wybranej szerokości"
+#~ msgid "original image"
+#~ msgstr "oryginalny obraz"
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/photologue/management/__init__.py	Sat Apr 14 16:40:29 2012 -0500
@@ -0,0 +1,1 @@
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/photologue/management/commands/__init__.py	Sat Apr 14 16:40:29 2012 -0500
@@ -0,0 +1,37 @@
+from photologue.models import PhotoSize
+
+def get_response(msg, func=int, default=None):
+    while True:
+        resp = raw_input(msg)
+        if not resp and default is not None:
+            return default            
+        try:
+            return func(resp)
+        except:
+            print 'Invalid input.'
+            
+def create_photosize(name, width=0, height=0, crop=False, pre_cache=False, increment_count=False):
+    try:
+        size = PhotoSize.objects.get(name=name)
+        exists = True
+    except PhotoSize.DoesNotExist:
+        size = PhotoSize(name=name)
+        exists = False
+    if exists:
+        msg = 'A "%s" photo size already exists. Do you want to replace it? (yes, no):' % name
+        if not get_response(msg, lambda inp: inp == 'yes', False):
+            return
+    print '\nWe will now define the "%s" photo size:\n' % size
+    w = get_response('Width (in pixels):', lambda inp: int(inp), width)
+    h = get_response('Height (in pixels):', lambda inp: int(inp), height)
+    c = get_response('Crop to fit? (yes, no):', lambda inp: inp == 'yes', crop)
+    p = get_response('Pre-cache? (yes, no):', lambda inp: inp == 'yes', pre_cache)
+    i = get_response('Increment count? (yes, no):', lambda inp: inp == 'yes', increment_count)
+    size.width = w
+    size.height = h
+    size.crop = c
+    size.pre_cache = p
+    size.increment_count = i
+    size.save()
+    print '\nA "%s" photo size has been created.\n' % name
+    return size
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/photologue/management/commands/plcache.py	Sat Apr 14 16:40:29 2012 -0500
@@ -0,0 +1,43 @@
+from django.core.management.base import BaseCommand, CommandError
+from optparse import make_option
+from photologue.models import PhotoSize, ImageModel
+
+class Command(BaseCommand):
+    option_list = BaseCommand.option_list + (
+        make_option('--reset', '-r', action='store_true', dest='reset', help='Reset photo cache before generating'),
+    )
+
+    help = ('Manages Photologue cache file for the given sizes.')
+    args = '[sizes]'
+
+    requires_model_validation = True
+    can_import_settings = True
+
+    def handle(self, *args, **options):
+        return create_cache(args, options)
+
+def create_cache(sizes, options):
+    """
+    Creates the cache for the given files
+    """
+    reset = options.get('reset', None)
+    
+    size_list = [size.strip(' ,') for size in sizes]
+    
+    if len(size_list) < 1:
+        sizes = PhotoSize.objects.filter(pre_cache=True)
+    else:
+        sizes = PhotoSize.objects.filter(name__in=size_list)
+        
+    if not len(sizes):
+        raise CommandError('No photo sizes were found.')
+        
+    print 'Caching photos, this may take a while...'
+    
+    for cls in ImageModel.__subclasses__():
+        for photosize in sizes:
+            print 'Cacheing %s size images' % photosize.name
+            for obj in cls.objects.all():
+                if reset:
+                    obj.remove_size(photosize)
+                obj.create_size(photosize)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/photologue/management/commands/plcreatesize.py	Sat Apr 14 16:40:29 2012 -0500
@@ -0,0 +1,13 @@
+from django.core.management.base import BaseCommand, CommandError
+from photologue.management.commands import create_photosize
+
+class Command(BaseCommand):  
+    help = ('Creates a new Photologue photo size interactively.')
+    requires_model_validation = True
+    can_import_settings = True
+
+    def handle(self, *args, **options):
+        create_size(args[0])
+
+def create_size(size):
+    create_photosize(size)
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/photologue/management/commands/plflush.py	Sat Apr 14 16:40:29 2012 -0500
@@ -0,0 +1,35 @@
+from django.core.management.base import BaseCommand, CommandError
+from optparse import make_option
+from photologue.models import PhotoSize, ImageModel
+
+class Command(BaseCommand):
+    help = ('Clears the Photologue cache for the given sizes.')
+    args = '[sizes]'
+
+    requires_model_validation = True
+    can_import_settings = True
+
+    def handle(self, *args, **options):
+        return create_cache(args, options)
+
+def create_cache(sizes, options):
+    """
+    Clears the cache for the given files
+    """    
+    size_list = [size.strip(' ,') for size in sizes]
+    
+    if len(size_list) < 1:
+        sizes = PhotoSize.objects.all()
+    else:
+        sizes = PhotoSize.objects.filter(name__in=size_list)
+        
+    if not len(sizes):
+        raise CommandError('No photo sizes were found.')
+        
+    print 'Flushing cache...'
+    
+    for cls in ImageModel.__subclasses__():
+        for photosize in sizes:
+            print 'Flushing %s size images' % photosize.name
+            for obj in cls.objects.all():
+                obj.remove_size(photosize)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/photologue/management/commands/plinit.py	Sat Apr 14 16:40:29 2012 -0500
@@ -0,0 +1,30 @@
+from django.core.management.base import BaseCommand, CommandError
+from photologue.management.commands import get_response, create_photosize
+from photologue.models import PhotoEffect
+
+class Command(BaseCommand):  
+    help = ('Prompts the user to set up the default photo sizes required by Photologue.')
+    requires_model_validation = True
+    can_import_settings = True
+
+    def handle(self, *args, **kwargs):
+        return init(*args, **kwargs)
+
+def init(*args, **kwargs):
+    msg = '\nPhotologue requires a specific photo size to display thumbnail previews in the Django admin application.\nWould you like to generate this size now? (yes, no):'
+    if get_response(msg, lambda inp: inp == 'yes', False):
+        admin_thumbnail = create_photosize('admin_thumbnail', width=100, height=75, crop=True, pre_cache=True)
+        msg = 'Would you like to apply a sample enhancement effect to your admin thumbnails? (yes, no):'
+        if get_response(msg, lambda inp: inp == 'yes', False):
+            effect, created = PhotoEffect.objects.get_or_create(name='Enhance Thumbnail', description="Increases sharpness and contrast. Works well for smaller image sizes such as thumbnails.", contrast=1.2, sharpness=1.3)
+            admin_thumbnail.effect = effect
+            admin_thumbnail.save()                
+    msg = '\nPhotologue comes with a set of templates for setting up a complete photo gallery. These templates require you to define both a "thumbnail" and "display" size.\nWould you like to define them now? (yes, no):'
+    if get_response(msg, lambda inp: inp == 'yes', False):
+        thumbnail = create_photosize('thumbnail', width=100, height=75)
+        display = create_photosize('display', width=400, increment_count=True)
+        msg = 'Would you like to apply a sample reflection effect to your display images? (yes, no):'
+        if get_response(msg, lambda inp: inp == 'yes', False):
+            effect, created = PhotoEffect.objects.get_or_create(name='Display Reflection', description="Generates a reflection with a white background", reflection_size=0.4)
+            display.effect = effect
+            display.save()
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/photologue/models.py	Sat Apr 14 16:40:29 2012 -0500
@@ -0,0 +1,700 @@
+import os
+import random
+import shutil
+import zipfile
+
+from datetime import datetime
+from inspect import isclass
+
+from django.db import models
+from django.db.models.signals import post_init
+from django.conf import settings
+from django.core.files.base import ContentFile
+from django.core.urlresolvers import reverse
+from django.template.defaultfilters import slugify
+from django.utils.functional import curry
+from django.utils.translation import ugettext_lazy as _
+
+# Required PIL classes may or may not be available from the root namespace
+# depending on the installation method used.
+try:
+    import Image
+    import ImageFile
+    import ImageFilter
+    import ImageEnhance
+except ImportError:
+    try:
+        from PIL import Image
+        from PIL import ImageFile
+        from PIL import ImageFilter
+        from PIL import ImageEnhance
+    except ImportError:
+        raise ImportError(_('Photologue was unable to import the Python Imaging Library. Please confirm it`s installed and available on your current Python path.'))
+
+# attempt to load the django-tagging TagField from default location,
+# otherwise we substitude a dummy TagField.
+try:
+    from tagging.fields import TagField
+    tagfield_help_text = _('Separate tags with spaces, put quotes around multiple-word tags.')
+except ImportError:
+    class TagField(models.CharField):
+        def __init__(self, **kwargs):
+            default_kwargs = {'max_length': 255, 'blank': True}
+            default_kwargs.update(kwargs)
+            super(TagField, self).__init__(**default_kwargs)
+        def get_internal_type(self):
+            return 'CharField'
+    tagfield_help_text = _('Django-tagging was not found, tags will be treated as plain text.')
+
+from utils import EXIF
+from utils.reflection import add_reflection
+from utils.watermark import apply_watermark
+
+# Path to sample image
+SAMPLE_IMAGE_PATH = getattr(settings, 'SAMPLE_IMAGE_PATH', os.path.join(os.path.dirname(__file__), 'res', 'sample.jpg')) # os.path.join(settings.PROJECT_PATH, 'photologue', 'res', 'sample.jpg'
+
+# Modify image file buffer size.
+ImageFile.MAXBLOCK = getattr(settings, 'PHOTOLOGUE_MAXBLOCK', 256 * 2 ** 10)
+
+# Photologue image path relative to media root
+PHOTOLOGUE_DIR = getattr(settings, 'PHOTOLOGUE_DIR', 'photologue')
+
+# Look for user function to define file paths
+PHOTOLOGUE_PATH = getattr(settings, 'PHOTOLOGUE_PATH', None)
+if PHOTOLOGUE_PATH is not None:
+    if callable(PHOTOLOGUE_PATH):
+        get_storage_path = PHOTOLOGUE_PATH
+    else:
+        parts = PHOTOLOGUE_PATH.split('.')
+        module_name = '.'.join(parts[:-1])
+        module = __import__(module_name)
+        get_storage_path = getattr(module, parts[-1])
+else:
+    def get_storage_path(instance, filename):
+        return os.path.join(PHOTOLOGUE_DIR, 'photos', filename)
+
+# Quality options for JPEG images
+JPEG_QUALITY_CHOICES = (
+    (30, _('Very Low')),
+    (40, _('Low')),
+    (50, _('Medium-Low')),
+    (60, _('Medium')),
+    (70, _('Medium-High')),
+    (80, _('High')),
+    (90, _('Very High')),
+)
+
+# choices for new crop_anchor field in Photo
+CROP_ANCHOR_CHOICES = (
+    ('top', _('Top')),
+    ('right', _('Right')),
+    ('bottom', _('Bottom')),
+    ('left', _('Left')),
+    ('center', _('Center (Default)')),
+)
+
+IMAGE_TRANSPOSE_CHOICES = (
+    ('FLIP_LEFT_RIGHT', _('Flip left to right')),
+    ('FLIP_TOP_BOTTOM', _('Flip top to bottom')),
+    ('ROTATE_90', _('Rotate 90 degrees counter-clockwise')),
+    ('ROTATE_270', _('Rotate 90 degrees clockwise')),
+    ('ROTATE_180', _('Rotate 180 degrees')),
+)
+
+WATERMARK_STYLE_CHOICES = (
+    ('tile', _('Tile')),
+    ('scale', _('Scale')),
+)
+
+# Prepare a list of image filters
+filter_names = []
+for n in dir(ImageFilter):
+    klass = getattr(ImageFilter, n)
+    if isclass(klass) and issubclass(klass, ImageFilter.BuiltinFilter) and \
+        hasattr(klass, 'name'):
+            filter_names.append(klass.__name__)
+IMAGE_FILTERS_HELP_TEXT = _('Chain multiple filters using the following pattern "FILTER_ONE->FILTER_TWO->FILTER_THREE". Image filters will be applied in order. The following filters are available: %s.' % (', '.join(filter_names)))
+
+
+class Gallery(models.Model):
+    date_added = models.DateTimeField(_('date published'), default=datetime.now)
+    title = models.CharField(_('title'), max_length=100, unique=True)
+    title_slug = models.SlugField(_('title slug'), unique=True,
+                                  help_text=_('A "slug" is a unique URL-friendly title for an object.'))
+    description = models.TextField(_('description'), blank=True)
+    is_public = models.BooleanField(_('is public'), default=True,
+                                    help_text=_('Public galleries will be displayed in the default views.'))
+    photos = models.ManyToManyField('Photo', related_name='galleries', verbose_name=_('photos'),
+                                    null=True, blank=True)
+    tags = TagField(help_text=tagfield_help_text, verbose_name=_('tags'))
+
+    class Meta:
+        ordering = ['-date_added']
+        get_latest_by = 'date_added'
+        verbose_name = _('gallery')
+        verbose_name_plural = _('galleries')
+
+    def __unicode__(self):
+        return self.title
+
+    def __str__(self):
+        return self.__unicode__()
+
+    def get_absolute_url(self):
+        return reverse('pl-gallery', args=[self.title_slug])
+
+    def latest(self, limit=0, public=True):
+        if limit == 0:
+            limit = self.photo_count()
+        if public:
+            return self.public()[:limit]
+        else:
+            return self.photos.all()[:limit]
+
+    def sample(self, count=0, public=True):
+        if count == 0 or count > self.photo_count():
+            count = self.photo_count()
+        if public:
+            photo_set = self.public()
+        else:
+            photo_set = self.photos.all()
+        return random.sample(photo_set, count)
+
+    def photo_count(self, public=True):
+        if public:
+            return self.public().count()
+        else:
+            return self.photos.all().count()
+    photo_count.short_description = _('count')
+
+    def public(self):
+        return self.photos.filter(is_public=True)
+
+
+class GalleryUpload(models.Model):
+    zip_file = models.FileField(_('images file (.zip)'), upload_to=PHOTOLOGUE_DIR+"/temp",
+                                help_text=_('Select a .zip file of images to upload into a new Gallery.'))
+    gallery = models.ForeignKey(Gallery, null=True, blank=True, help_text=_('Select a gallery to add these images to. leave this empty to create a new gallery from the supplied title.'))
+    title = models.CharField(_('title'), max_length=75, help_text=_('All photos in the gallery will be given a title made up of the gallery title + a sequential number.'))
+    caption = models.TextField(_('caption'), blank=True, help_text=_('Caption will be added to all photos.'))
+    description = models.TextField(_('description'), blank=True, help_text=_('A description of this Gallery.'))
+    is_public = models.BooleanField(_('is public'), default=True, help_text=_('Uncheck this to make the uploaded gallery and included photographs private.'))
+    tags = models.CharField(max_length=255, blank=True, help_text=tagfield_help_text, verbose_name=_('tags'))
+
+    class Meta:
+        verbose_name = _('gallery upload')
+        verbose_name_plural = _('gallery uploads')
+
+    def save(self):
+        super(GalleryUpload, self).save()
+        self.process_zipfile()
+        super(GalleryUpload, self).delete()
+
+    def process_zipfile(self):
+        if os.path.isfile(self.zip_file.path):
+            # TODO: implement try-except here
+            zip = zipfile.ZipFile(self.zip_file.path)
+            bad_file = zip.testzip()
+            if bad_file:
+                raise Exception('"%s" in the .zip archive is corrupt.' % bad_file)
+            count = 1
+            if self.gallery:
+                gallery = self.gallery
+            else:
+                gallery = Gallery.objects.create(title=self.title,
+                                                 title_slug=slugify(self.title),
+                                                 description=self.description,
+                                                 is_public=self.is_public,
+                                                 tags=self.tags)
+            from cStringIO import StringIO
+            for filename in zip.namelist():
+                if filename.startswith('__'): # do not process meta files
+                    continue
+                data = zip.read(filename)
+                if len(data):
+                    try:
+                        # the following is taken from django.newforms.fields.ImageField:
+                        #  load() is the only method that can spot a truncated JPEG,
+                        #  but it cannot be called sanely after verify()
+                        trial_image = Image.open(StringIO(data))
+                        trial_image.load()
+                        # verify() is the only method that can spot a corrupt PNG,
+                        #  but it must be called immediately after the constructor
+                        trial_image = Image.open(StringIO(data))
+                        trial_image.verify()
+                    except Exception:
+                        # if a "bad" file is found we just skip it.
+                        continue
+                    while 1:
+                        title = ' '.join([self.title, str(count)])
+                        slug = slugify(title)
+                        try:
+                            p = Photo.objects.get(title_slug=slug)
+                        except Photo.DoesNotExist:                            
+                            photo = Photo(title=title, title_slug=slug,
+                                          caption=self.caption,
+                                          is_public=self.is_public,
+                                          tags=self.tags)
+                            photo.image.save(filename, ContentFile(data))
+                            gallery.photos.add(photo)
+                            count = count + 1
+                            break
+                        count = count + 1
+            zip.close()
+
+
+class ImageModel(models.Model):
+    image = models.ImageField(_('image'), upload_to=get_storage_path)
+    date_taken = models.DateTimeField(_('date taken'), null=True, blank=True, editable=False)
+    view_count = models.PositiveIntegerField(default=0, editable=False)
+    crop_from = models.CharField(_('crop from'), blank=True, max_length=10, default='center', choices=CROP_ANCHOR_CHOICES)
+    effect = models.ForeignKey('PhotoEffect', null=True, blank=True, related_name="%(class)s_related", verbose_name=_('effect'))
+
+    class Meta:
+        abstract = True
+
+    @property
+    def EXIF(self):
+        try:
+            return EXIF.process_file(open(self.image.path, 'rb'))
+        except:
+            try:
+                return EXIF.process_file(open(self.image.path, 'rb'), details=False)
+            except:
+                return {}
+
+    def admin_thumbnail(self):
+        func = getattr(self, 'get_admin_thumbnail_url', None)
+        if func is None:
+            return _('An "admin_thumbnail" photo size has not been defined.')
+        else:
+            if hasattr(self, 'get_absolute_url'):
+                return u'<a href="%s"><img src="%s"></a>' % \
+                    (self.get_absolute_url(), func())
+            else:
+                return u'<a href="%s"><img src="%s"></a>' % \
+                    (self.image.url, func())
+    admin_thumbnail.short_description = _('Thumbnail')
+    admin_thumbnail.allow_tags = True
+
+    def cache_path(self):
+        return os.path.join(os.path.dirname(self.image.path), "cache")
+
+    def cache_url(self):
+        return '/'.join([os.path.dirname(self.image.url), "cache"])
+
+    def image_filename(self):
+        return os.path.basename(self.image.path)
+
+    def _get_filename_for_size(self, size):
+        size = getattr(size, 'name', size)
+        base, ext = os.path.splitext(self.image_filename())
+        return ''.join([base, '_', size, ext])
+
+    def _get_SIZE_photosize(self, size):
+        return PhotoSizeCache().sizes.get(size)
+
+    def _get_SIZE_size(self, size):
+        photosize = PhotoSizeCache().sizes.get(size)
+        if not self.size_exists(photosize):
+            self.create_size(photosize)
+        return Image.open(self._get_SIZE_filename(size)).size
+
+    def _get_SIZE_url(self, size):
+        photosize = PhotoSizeCache().sizes.get(size)
+        if not self.size_exists(photosize):
+            self.create_size(photosize)
+        if photosize.increment_count:
+            self.view_count += 1
+            self.save(update=True)
+        return '/'.join([self.cache_url(), self._get_filename_for_size(photosize.name)])
+
+    def _get_SIZE_filename(self, size):
+        photosize = PhotoSizeCache().sizes.get(size)
+        return os.path.join(self.cache_path(),
+                            self._get_filename_for_size(photosize.name))
+
+    def add_accessor_methods(self, *args, **kwargs):
+        for size in PhotoSizeCache().sizes.keys():
+            setattr(self, 'get_%s_size' % size,
+                    curry(self._get_SIZE_size, size=size))
+            setattr(self, 'get_%s_photosize' % size,
+                    curry(self._get_SIZE_photosize, size=size))
+            setattr(self, 'get_%s_url' % size,
+                    curry(self._get_SIZE_url, size=size))
+            setattr(self, 'get_%s_filename' % size,
+                    curry(self._get_SIZE_filename, size=size))
+
+    def size_exists(self, photosize):
+        func = getattr(self, "get_%s_filename" % photosize.name, None)
+        if func is not None:
+            if os.path.isfile(func()):
+                return True
+        return False
+
+    def resize_image(self, im, photosize):
+        cur_width, cur_height = im.size
+        new_width, new_height = photosize.size
+        if photosize.crop:
+            ratio = max(float(new_width)/cur_width,float(new_height)/cur_height)
+            x = (cur_width * ratio)
+            y = (cur_height * ratio)
+            xd = abs(new_width - x)
+            yd = abs(new_height - y)
+            x_diff = int(xd / 2)
+            y_diff = int(yd / 2)
+            if self.crop_from == 'top':
+                box = (int(x_diff), 0, int(x_diff+new_width), new_height)
+            elif self.crop_from == 'left':
+                box = (0, int(y_diff), new_width, int(y_diff+new_height))
+            elif self.crop_from == 'bottom':
+                box = (int(x_diff), int(yd), int(x_diff+new_width), int(y)) # y - yd = new_height
+            elif self.crop_from == 'right':
+                box = (int(xd), int(y_diff), int(x), int(y_diff+new_height)) # x - xd = new_width
+            else:
+                box = (int(x_diff), int(y_diff), int(x_diff+new_width), int(y_diff+new_height))
+            im = im.resize((int(x), int(y)), Image.ANTIALIAS).crop(box)
+        else:
+            if not new_width == 0 and not new_height == 0:
+                ratio = min(float(new_width)/cur_width,
+                            float(new_height)/cur_height)
+            else:
+                if new_width == 0:
+                    ratio = float(new_height)/cur_height
+                else:
+                    ratio = float(new_width)/cur_width
+            new_dimensions = (int(round(cur_width*ratio)),
+                              int(round(cur_height*ratio)))
+            if new_dimensions[0] > cur_width or \
+               new_dimensions[1] > cur_height:
+                if not photosize.upscale:
+                    return im
+            im = im.resize(new_dimensions, Image.ANTIALIAS)
+        return im
+
+    def create_size(self, photosize):
+        if self.size_exists(photosize):
+            return
+        if not os.path.isdir(self.cache_path()):
+            os.makedirs(self.cache_path())
+        try:
+            im = Image.open(self.image.path)
+        except IOError:
+            return
+        # Apply effect if found
+        if self.effect is not None:
+            im = self.effect.pre_process(im)
+        elif photosize.effect is not None:
+            im = photosize.effect.pre_process(im)
+        # Resize/crop image
+        if im.size != photosize.size:
+            im = self.resize_image(im, photosize)
+        # Apply watermark if found
+        if photosize.watermark is not None:
+            im = photosize.watermark.post_process(im)
+        # Apply effect if found
+        if self.effect is not None:
+            im = self.effect.post_process(im)
+        elif photosize.effect is not None:
+            im = photosize.effect.post_process(im)
+        # Save file
+        im_filename = getattr(self, "get_%s_filename" % photosize.name)()
+        try:
+            if im.format == 'JPEG':
+                im.save(im_filename, 'JPEG', quality=int(photosize.quality), optimize=True)
+            else:
+                im.save(im_filename)
+        except IOError, e:
+            if os.path.isfile(im_filename):
+                os.unlink(im_filename)
+            raise e
+
+    def remove_size(self, photosize, remove_dirs=True):
+        if not self.size_exists(photosize):
+            return
+        filename = getattr(self, "get_%s_filename" % photosize.name)()
+        if os.path.isfile(filename):
+            os.remove(filename)
+        if remove_dirs:
+            self.remove_cache_dirs()
+
+    def clear_cache(self):
+        cache = PhotoSizeCache()
+        for photosize in cache.sizes.values():
+            self.remove_size(photosize, False)
+        self.remove_cache_dirs()
+
+    def pre_cache(self):
+        cache = PhotoSizeCache()
+        for photosize in cache.sizes.values():
+            if photosize.pre_cache:
+                self.create_size(photosize)
+
+    def remove_cache_dirs(self):
+        try:
+            os.removedirs(self.cache_path())
+        except:
+            pass
+
+    def save(self, update=False):
+        if update:
+            models.Model.save(self)
+            return
+        if self.date_taken is None:
+            try:
+                exif_date = self.EXIF.get('EXIF DateTimeOriginal', None)
+                if exif_date is not None:
+                    d, t = str.split(exif_date.values)
+                    year, month, day = d.split(':')
+                    hour, minute, second = t.split(':')
+                    self.date_taken = datetime(int(year), int(month), int(day),
+                                               int(hour), int(minute), int(second))
+            except:
+                pass
+        if self.date_taken is None:
+            self.date_taken = datetime.now()
+        if self._get_pk_val():
+            self.clear_cache()
+        super(ImageModel, self).save()
+        self.pre_cache()
+
+    def delete(self):
+        self.clear_cache()
+        super(ImageModel, self).delete()
+
+
+class Photo(ImageModel):
+    title = models.CharField(_('title'), max_length=100, unique=True)
+    title_slug = models.SlugField(_('slug'), unique=True,
+                                  help_text=('A "slug" is a unique URL-friendly title for an object.'))
+    caption = models.TextField(_('caption'), blank=True)
+    date_added = models.DateTimeField(_('date added'), default=datetime.now, editable=False)
+    is_public = models.BooleanField(_('is public'), default=True, help_text=_('Public photographs will be displayed in the default views.'))
+    tags = TagField(help_text=tagfield_help_text, verbose_name=_('tags'))
+
+    class Meta:
+        ordering = ['-date_added']
+        get_latest_by = 'date_added'
+        verbose_name = _("photo")
+        verbose_name_plural = _("photos")
+
+    def __unicode__(self):
+        return self.title
+
+    def __str__(self):
+        return self.__unicode__()
+
+    def save(self, update=False):
+        if self.title_slug is None:
+            self.title_slug = slugify(self.title)
+        super(Photo, self).save(update)
+
+    def get_absolute_url(self):
+        return reverse('pl-photo', args=[self.title_slug])
+
+    def public_galleries(self):
+        """Return the public galleries to which this photo belongs."""
+        return self.galleries.filter(is_public=True)
+
+
+class BaseEffect(models.Model):
+    name = models.CharField(_('name'), max_length=30, unique=True)
+    description = models.TextField(_('description'), blank=True)
+
+    class Meta:
+        abstract = True
+
+    def sample_dir(self):
+        return os.path.join(settings.MEDIA_ROOT, PHOTOLOGUE_DIR, 'samples')
+
+    def sample_url(self):
+        return settings.MEDIA_URL + '/'.join([PHOTOLOGUE_DIR, 'samples', '%s %s.jpg' % (self.name.lower(), 'sample')])
+
+    def sample_filename(self):
+        return os.path.join(self.sample_dir(), '%s %s.jpg' % (self.name.lower(), 'sample'))
+
+    def create_sample(self):
+        if not os.path.isdir(self.sample_dir()):
+            os.makedirs(self.sample_dir())
+        try:
+            im = Image.open(SAMPLE_IMAGE_PATH)
+        except IOError:
+            raise IOError('Photologue was unable to open the sample image: %s.' % SAMPLE_IMAGE_PATH)
+        im = self.process(im)
+        im.save(self.sample_filename(), 'JPEG', quality=90, optimize=True)
+
+    def admin_sample(self):
+        return u'<img src="%s">' % self.sample_url()
+    admin_sample.short_description = 'Sample'
+    admin_sample.allow_tags = True
+
+    def pre_process(self, im):
+        return im
+
+    def post_process(self, im):
+        return im
+
+    def process(self, im):
+        im = self.pre_process(im)
+        im = self.post_process(im)
+        return im
+
+    def __unicode__(self):
+        return self.name
+
+    def __str__(self):
+        return self.__unicode__()
+
+    def save(self):
+        try:
+            os.remove(self.sample_filename())
+        except:
+            pass
+        models.Model.save(self)
+        self.create_sample()
+        for size in self.photo_sizes.all():
+            size.clear_cache()
+        # try to clear all related subclasses of ImageModel
+        for prop in [prop for prop in dir(self) if prop[-8:] == '_related']:
+            for obj in getattr(self, prop).all():
+                obj.clear_cache()
+                obj.pre_cache()
+
+    def delete(self):
+        try:
+            os.remove(self.sample_filename())
+        except:
+            pass
+        super(PhotoEffect, self).delete()
+
+
+class PhotoEffect(BaseEffect):
+    """ A pre-defined effect to apply to photos """
+    transpose_method = models.CharField(_('rotate or flip'), max_length=15, blank=True, choices=IMAGE_TRANSPOSE_CHOICES)
+    color = models.FloatField(_('color'), default=1.0, help_text=_("A factor of 0.0 gives a black and white image, a factor of 1.0 gives the original image."))
+    brightness = models.FloatField(_('brightness'), default=1.0, help_text=_("A factor of 0.0 gives a black image, a factor of 1.0 gives the original image."))
+    contrast = models.FloatField(_('contrast'), default=1.0, help_text=_("A factor of 0.0 gives a solid grey image, a factor of 1.0 gives the original image."))
+    sharpness = models.FloatField(_('sharpness'), default=1.0, help_text=_("A factor of 0.0 gives a blurred image, a factor of 1.0 gives the original image."))
+    filters = models.CharField(_('filters'), max_length=200, blank=True, help_text=_(IMAGE_FILTERS_HELP_TEXT))
+    reflection_size = models.FloatField(_('size'), default=0, help_text=_("The height of the reflection as a percentage of the orignal image. A factor of 0.0 adds no reflection, a factor of 1.0 adds a reflection equal to the height of the orignal image."))
+    reflection_strength = models.FloatField(_('strength'), default=0.6, help_text="The initial opacity of the reflection gradient.")
+    background_color = models.CharField(_('color'), max_length=7, default="#FFFFFF", help_text="The background color of the reflection gradient. Set this to match the background color of your page.")
+
+    class Meta:
+        verbose_name = _("photo effect")
+        verbose_name_plural = _("photo effects")
+
+    def pre_process(self, im):
+        if self.transpose_method != '':
+            method = getattr(Image, self.transpose_method)
+            im = im.transpose(method)
+        if im.mode != 'RGB' and im.mode != 'RGBA':
+            return im
+        for name in ['Color', 'Brightness', 'Contrast', 'Sharpness']:
+            factor = getattr(self, name.lower())
+            if factor != 1.0:
+                im = getattr(ImageEnhance, name)(im).enhance(factor)
+        for name in self.filters.split('->'):
+            image_filter = getattr(ImageFilter, name.upper(), None)
+            if image_filter is not None:
+                try:
+                    im = im.filter(image_filter)
+                except ValueError:
+                    pass
+        return im
+
+    def post_process(self, im):
+        if self.reflection_size != 0.0:
+            im = add_reflection(im, bgcolor=self.background_color, amount=self.reflection_size, opacity=self.reflection_strength)
+        return im
+
+
+class Watermark(BaseEffect):
+    image = models.ImageField(_('image'), upload_to=PHOTOLOGUE_DIR+"/watermarks")
+    style = models.CharField(_('style'), max_length=5, choices=WATERMARK_STYLE_CHOICES, default='scale')
+    opacity = models.FloatField(_('opacity'), default=1, help_text=_("The opacity of the overlay."))
+
+    class Meta:
+        verbose_name = _('watermark')
+        verbose_name_plural = _('watermarks')
+
+    def post_process(self, im):
+        mark = Image.open(self.image.path)
+        return apply_watermark(im, mark, self.style, self.opacity)
+
+
+class PhotoSize(models.Model):
+    name = models.CharField(_('name'), max_length=20, unique=True, help_text=_('Photo size name should contain only letters, numbers and underscores. Examples: "thumbnail", "display", "small", "main_page_widget".'))
+    width = models.PositiveIntegerField(_('width'), default=0, help_text=_('If width is set to "0" the image will be scaled to the supplied height.'))
+    height = models.PositiveIntegerField(_('height'), default=0, help_text=_('If height is set to "0" the image will be scaled to the supplied width'))
+    quality = models.PositiveIntegerField(_('quality'), choices=JPEG_QUALITY_CHOICES, default=70, help_text=_('JPEG image quality.'))
+    upscale = models.BooleanField(_('upscale images?'), default=False, help_text=_('If selected the image will be scaled up if necessary to fit the supplied dimensions. Cropped sizes will be upscaled regardless of this setting.'))
+    crop = models.BooleanField(_('crop to fit?'), default=False, help_text=_('If selected the image will be scaled and cropped to fit the supplied dimensions.'))
+    pre_cache = models.BooleanField(_('pre-cache?'), default=False, help_text=_('If selected this photo size will be pre-cached as photos are added.'))
+    increment_count = models.BooleanField(_('increment view count?'), default=False, help_text=_('If selected the image\'s "view_count" will be incremented when this photo size is displayed.'))
+    effect = models.ForeignKey('PhotoEffect', null=True, blank=True, related_name='photo_sizes', verbose_name=_('photo effect'))
+    watermark = models.ForeignKey('Watermark', null=True, blank=True, related_name='photo_sizes', verbose_name=_('watermark image'))
+
+    class Meta:
+        ordering = ['width', 'height']
+        verbose_name = _('photo size')
+        verbose_name_plural = _('photo sizes')
+
+    def __unicode__(self):
+        return self.name
+
+    def __str__(self):
+        return self.__unicode__()
+
+    def clear_cache(self):
+        for cls in ImageModel.__subclasses__():
+            for obj in cls.objects.all():
+                obj.remove_size(self)
+                if self.pre_cache:
+                    obj.create_size(self)
+        PhotoSizeCache().reset()
+
+    def save(self):
+        if self.width + self.height <= 0:
+            raise ValueError(_('A PhotoSize must have a positive height or width.'))
+        super(PhotoSize, self).save()
+        PhotoSizeCache().reset()
+        self.clear_cache()
+
+    def delete(self):
+        self.clear_cache()
+        super(PhotoSize, self).delete()
+
+    def _get_size(self):
+        return (self.width, self.height)
+    def _set_size(self, value):
+        self.width, self.height = value
+    size = property(_get_size, _set_size)
+
+
+class PhotoSizeCache(object):
+    __state = {"sizes": {}}
+
+    def __init__(self):
+        self.__dict__ = self.__state
+        if not len(self.sizes):
+            sizes = PhotoSize.objects.all()
+            for size in sizes:
+                self.sizes[size.name] = size
+
+    def reset(self):
+        self.sizes = {}
+
+
+# Set up the accessor methods
+def add_methods(sender, instance, signal, *args, **kwargs):
+    """ Adds methods to access sized images (urls, paths)
+
+    after the Photo model's __init__ function completes,
+    this method calls "add_accessor_methods" on each instance.
+    """
+    if hasattr(instance, 'add_accessor_methods'):
+        instance.add_accessor_methods()
+
+# connect the add_accessor_methods function to the post_init signal
+post_init.connect(add_methods)
Binary file photologue/res/sample.jpg has changed
Binary file photologue/res/test_landscape.jpg has changed
Binary file photologue/res/test_portrait.jpg has changed
Binary file photologue/res/test_square.jpg has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/photologue/templates/photologue/gallery_archive.html	Sat Apr 14 16:40:29 2012 -0500
@@ -0,0 +1,26 @@
+{% extends "photologue/root.html" %}
+
+{% block title %}Latest Photo Galleries{% endblock %}
+
+{% block content %}
+
+<h1>Latest Photo Galleries</h1>
+
+{% if latest %}
+    {% for gallery in latest %}
+    <div class="photo-gallery">
+        <h2><a href="{{ gallery.get_absolute_url }}">{{ gallery.title }}</a></h2>
+        {% for photo in gallery.sample|slice:sample_size %}
+        <div class="gallery-photo">
+            <a href="{{ photo.get_absolute_url }}"><img src="{{ photo.get_thumbnail_url }}" alt="{{ photo.title }}"/></a>
+        </div>
+        {% endfor %}
+    </div>
+    {% endfor %}
+{% else %}
+    <p>No galleries were found.</p>
+{% endif %}
+
+<p><a href="{% url pl-gallery-list 1 %}">View all galleries.</a></p>
+
+{% endblock %}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/photologue/templates/photologue/gallery_archive_day.html	Sat Apr 14 16:40:29 2012 -0500
@@ -0,0 +1,26 @@
+{% extends "photologue/root.html" %}
+
+{% block title %}Galleries for {{ day|date }}{% endblock %}
+
+{% block content %}
+
+<h1>Galleries for {{ day|date }}</h1>
+
+{% if object_list %}
+    {% for gallery in object_list %}
+    <div class="photo-gallery">
+        <h2>{{ gallery.title }}</h2>
+        {% for photo in gallery.sample|slice:sample_size %}
+        <div class="gallery-photo">
+            <a href="{{ photo.get_absolute_url }}"><img src="{{ photo.get_thumbnail_url }}" alt="{{ photo.title }}"/></a>
+        </div>
+        {% endfor %}
+    </div>
+    {% endfor %}
+{% else %}
+    <p>No galleries were found.</p>
+{% endif %}
+
+<p><a href="{% url pl-gallery-list 1 %}">View all galleries.</a></p>
+
+{% endblock %}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/photologue/templates/photologue/gallery_archive_month.html	Sat Apr 14 16:40:29 2012 -0500
@@ -0,0 +1,26 @@
+{% extends "photologue/root.html" %}
+
+{% block title %}Galleries for {{ month|date:"F Y" }}{% endblock %}
+
+{% block content %}
+
+<h1>Galleries for {{ month|date:"F Y" }}</h1>
+
+{% if object_list %}
+    {% for gallery in object_list %}
+    <div class="photo-gallery">
+        <h2>{{ gallery.title }}</h2>
+        {% for photo in gallery.sample|slice:sample_size %}
+        <div class="gallery-photo">
+            <a href="{{ photo.get_absolute_url }}"><img src="{{ photo.get_thumbnail_url }}" alt="{{ photo.title }}"/></a>
+        </div>
+        {% endfor %}
+    </div>
+    {% endfor %}
+{% else %}
+    <p>No galleries were found.</p>
+{% endif %}
+
+<p><a href="{% url pl-gallery-list 1 %}">View all galleries.</a></p>
+
+{% endblock %}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/photologue/templates/photologue/gallery_archive_year.html	Sat Apr 14 16:40:29 2012 -0500
@@ -0,0 +1,16 @@
+{% extends "photologue/root.html" %}
+
+{% block title %}Galleries for {{ year }}{% endblock %}
+
+{% block content %}
+
+<h1>Galleries for {{ year }}</h1>
+<ul>
+{% for date in date_list %}
+<li><a href="{{ date|date:"M"|lower }}/">{{ date|date:"F" }}</a></li>
+{% endfor %}
+</ul>
+
+<p><a href="{% url pl-gallery-list 1 %}">View all galleries.</a></p>
+
+{% endblock %}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/photologue/templates/photologue/gallery_detail.html	Sat Apr 14 16:40:29 2012 -0500
@@ -0,0 +1,19 @@
+{% extends "photologue/root.html" %}
+
+{% block title %}{{ object.title }}{% endblock %}
+
+{% block content %}
+
+<h1>{{ object.title }}</h1>
+<h2>Originally published {{ object.date_added|date:"l, F jS, Y" }}</h2>
+{% if object.description %}<p>{{ object.description }}</p>{% endif %}
+<div class="photo-gallery">
+    {% for photo in object.public %}
+    <div class="gallery-photo">
+        <a href="{{ photo.get_absolute_url }}"><img src="{{ photo.get_thumbnail_url }}" alt="{{ photo.title }}"/></a>
+    </div>
+    {% endfor %}
+</div>
+<p><a href="{% url pl-gallery-list 1 %}">View all galleries</a></p>
+
+{% endblock %}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/photologue/templates/photologue/gallery_list.html	Sat Apr 14 16:40:29 2012 -0500
@@ -0,0 +1,31 @@
+{% extends "photologue/root.html" %}
+
+{% block title %}All Galleries{% endblock %}
+
+{% block content %}
+
+<h1>All galleries</h1>
+
+{% if object_list %}
+    {% for gallery in object_list %}
+    <div class="photo-gallery">
+        <h2><a href="{{ gallery.get_absolute_url }}">{{ gallery.title }}</a></h2>
+        {% for photo in gallery.sample|slice:sample_size %}
+        <div class="gallery-photo">
+            <a href="{{ photo.get_absolute_url }}"><img src="{{ photo.get_thumbnail_url }}" alt="{{ photo.title }}"/></a>
+        </div>
+        {% endfor %}
+    </div>
+    {% endfor %}
+{% else %}
+    <p>No galleries were found.</p>
+{% endif %}
+
+{% if is_paginated %}
+<p>{{ hits }} galleries total.</p>
+<div id="page_controls">
+    <p>{% if has_previous %}<a href="{% url pl-gallery-list previous %}">Previous</a> | {% endif %} page {{ page }} of {{ pages }} {% if has_next %}| <a href="{% url pl-gallery-list next %}">Next</a>{% endif %}</p>
+</div>
+{% endif %}
+
+{% endblock %}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/photologue/templates/photologue/photo_archive.html	Sat Apr 14 16:40:29 2012 -0500
@@ -0,0 +1,20 @@
+{% extends "photologue/root.html" %}
+
+{% block title %}Latest Photos{% endblock %}
+
+{% block content %}
+
+<h1>Latest Photos</h1>
+
+{% if latest %}
+    {% for photo in latest %}
+    <div class="gallery-photo">
+        <a href="{{ photo.get_absolute_url }}"><img src="{{ photo.get_thumbnail_url }}" alt="{{ photo.title }}"/></a>
+    </div>
+    {% endfor %}
+{% else %}
+<p>No photos were found.</p>
+{% endif %}
+<p><a href="{% url pl-photo-list 1 %}">View all photographs</a></p>
+
+{% endblock %}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/photologue/templates/photologue/photo_archive_day.html	Sat Apr 14 16:40:29 2012 -0500
@@ -0,0 +1,20 @@
+{% extends "photologue/root.html" %}
+
+{% block title %}Photos for {{ day|date }}{% endblock %}
+
+{% block content %}
+
+<h1>Photos for {{ day|date }}</h1>
+
+{% if object_list %}
+    {% for photo in object_list %}
+    <div class="gallery-photo">
+        <a href="{{ photo.get_absolute_url }}"><img src="{{ photo.get_thumbnail_url }}" alt="{{ photo.title }}"/></a>
+    </div>
+    {% endfor %}
+{% else %}
+<p>No photos were found.</p>
+{% endif %}
+<p><a href="{% url pl-photo-list 1 %}">View all photographs</a></p>
+
+{% endblock %}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/photologue/templates/photologue/photo_archive_month.html	Sat Apr 14 16:40:29 2012 -0500
@@ -0,0 +1,20 @@
+{% extends "photologue/root.html" %}
+
+{% block title %}Photos for {{ month|date:"F Y" }}{% endblock %}
+
+{% block content %}
+
+<h1>Photos for {{ month|date:"F Y" }}</h1>
+
+{% if object_list %}
+    {% for photo in object_list %}
+    <div class="gallery-photo">
+        <a href="{{ photo.get_absolute_url }}"><img src="{{ photo.get_thumbnail_url }}" alt="{{ photo.title }}"/></a>
+    </div>
+    {% endfor %}
+{% else %}
+<p>No photos were found.</p>
+{% endif %}
+<p><a href="{% url pl-photo-list 1 %}">View all photographs</a></p>
+
+{% endblock %}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/photologue/templates/photologue/photo_archive_year.html	Sat Apr 14 16:40:29 2012 -0500
@@ -0,0 +1,14 @@
+{% extends "photologue/root.html" %}
+
+{% block title %}Galleries for {{ year }}{% endblock %}
+
+{% block content %}
+
+<h1>Photos for {{ year }}</h1>
+<ul>
+{% for date in date_list %}
+<li><a href="{{ date|date:"M"|lower }}/">{{ date|date:"F" }}</a></li>
+{% endfor %}
+</ul>
+
+{% endblock %}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/photologue/templates/photologue/photo_detail.html	Sat Apr 14 16:40:29 2012 -0500
@@ -0,0 +1,21 @@
+{% extends "photologue/root.html" %}
+
+{% block title %}{{ object.title }}{% endblock %}
+
+{% block content %}
+
+<h1>{{ object.title }}</h1>
+<div class="gallery-photo">
+    <a href="{{ object.image.url }}"><img src="{{ object.get_display_url }}" alt="{{ object.title }}"/></a>
+    {% if object.caption %}<p>{{ object.caption }}</p>{% endif %}
+</div>
+{% if object.public_galleries %}
+<h2>This photo is found in the following galleries:</h2>
+<ol>
+{% for gallery in object.public_galleries %}
+    <li><a href="{{ gallery.get_absolute_url }}">{{ gallery.title }}</a></li>
+{% endfor %}
+</ol>
+{% endif %}
+
+{% endblock %}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/photologue/templates/photologue/photo_list.html	Sat Apr 14 16:40:29 2012 -0500
@@ -0,0 +1,26 @@
+{% extends "photologue/root.html" %}
+
+{% block title %}All Photos{% endblock %}
+
+{% block content %}
+
+<h1>All Photos</h1>
+
+{% if object_list %}
+    {% for photo in object_list %}
+    <div class="gallery-photo">
+        <a href="{{ photo.get_absolute_url }}"><img src="{{ photo.get_thumbnail_url }}" alt="{{ photo.title }}"/></a>
+    </div>
+    {% endfor %}
+{% else %}
+<p>No photos were found.</p>
+{% endif %}
+
+{% if is_paginated %}
+<p>{{ hits }} photos total.</p>
+<div id="page_controls">
+    <p>{% if has_previous %}<a href="{% url pl-photo-list previous %}">Previous</a> | {% endif %} page {{ page }} of {{ pages }} {% if has_next %}| <a href="{% url pl-photo-list next %}">Next</a>{% endif %}</p>
+</div>
+{% endif %}
+
+{% endblock %}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/photologue/templates/photologue/root.html	Sat Apr 14 16:40:29 2012 -0500
@@ -0,0 +1,1 @@
+{% extends "base.html" %}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/photologue/tests.py	Sat Apr 14 16:40:29 2012 -0500
@@ -0,0 +1,218 @@
+import os
+import unittest
+from django.conf import settings
+from django.core.files.base import ContentFile
+from django.test import TestCase
+
+from models import *
+
+# Path to sample image
+RES_DIR = os.path.join(os.path.dirname(__file__), 'res')
+LANDSCAPE_IMAGE_PATH = os.path.join(RES_DIR, 'test_landscape.jpg')
+PORTRAIT_IMAGE_PATH = os.path.join(RES_DIR, 'test_portrait.jpg')
+SQUARE_IMAGE_PATH = os.path.join(RES_DIR, 'test_square.jpg')
+        
+        
+class TestPhoto(ImageModel):
+    """ Minimal ImageModel class for testing """
+    name = models.CharField(max_length=30)
+    
+    
+class PLTest(TestCase):
+    """ Base TestCase class """
+    def setUp(self):
+        self.s = PhotoSize(name='test', width=100, height=100)
+        self.s.save()
+        self.pl = TestPhoto(name='landscape')
+        self.pl.image.save(os.path.basename(LANDSCAPE_IMAGE_PATH),
+                           ContentFile(open(LANDSCAPE_IMAGE_PATH, 'rb').read()))
+        self.pl.save()
+
+    def tearDown(self):
+        path = self.pl.image.path
+        self.pl.delete()
+        self.failIf(os.path.isfile(path))
+        self.s.delete()
+
+
+class PhotoTest(PLTest):    
+    def test_new_photo(self):
+        self.assertEqual(TestPhoto.objects.count(), 1)
+        self.failUnless(os.path.isfile(self.pl.image.path))
+        self.assertEqual(os.path.getsize(self.pl.image.path),
+                         os.path.getsize(LANDSCAPE_IMAGE_PATH))
+                                         
+    def test_exif(self):
+        self.assert_(len(self.pl.EXIF.keys()) > 0)
+
+    def test_paths(self):
+        self.assertEqual(os.path.normpath(str(self.pl.cache_path())).lower(),
+                         os.path.normpath(os.path.join(settings.MEDIA_ROOT,
+                                      PHOTOLOGUE_DIR,
+                                      'photos',
+                                      'cache')).lower())
+        self.assertEqual(self.pl.cache_url(),
+                         settings.MEDIA_URL + PHOTOLOGUE_DIR + '/photos/cache')
+
+    def test_count(self):
+        for i in range(5):
+            self.pl.get_test_url()
+        self.assertEquals(self.pl.view_count, 0)
+        self.s.increment_count = True
+        self.s.save()
+        for i in range(5):
+            self.pl.get_test_url()
+        self.assertEquals(self.pl.view_count, 5)
+        
+    def test_precache(self):
+        # set the thumbnail photo size to pre-cache
+        self.s.pre_cache = True
+        self.s.save()
+        # make sure it created the file
+        self.failUnless(os.path.isfile(self.pl.get_test_filename()))
+        self.s.pre_cache = False
+        self.s.save()
+        # clear the cache and make sure the file's deleted
+        self.pl.clear_cache()
+        self.failIf(os.path.isfile(self.pl.get_test_filename()))
+        
+    def test_accessor_methods(self):
+        self.assertEquals(self.pl.get_test_photosize(), self.s)
+        self.assertEquals(self.pl.get_test_size(),
+                          Image.open(self.pl.get_test_filename()).size)
+        self.assertEquals(self.pl.get_test_url(),
+                          self.pl.cache_url() + '/' + \
+                          self.pl._get_filename_for_size(self.s))
+        self.assertEquals(self.pl.get_test_filename(),
+                          os.path.join(self.pl.cache_path(),
+                          self.pl._get_filename_for_size(self.s)))
+        
+        
+class ImageResizeTest(PLTest):
+    def setUp(self):
+        super(ImageResizeTest, self).setUp()
+        self.pp = TestPhoto(name='portrait')
+        self.pp.image.save(os.path.basename(PORTRAIT_IMAGE_PATH),
+                           ContentFile(open(PORTRAIT_IMAGE_PATH, 'rb').read()))
+        self.pp.save()
+        self.ps = TestPhoto(name='square')
+        self.ps.image.save(os.path.basename(SQUARE_IMAGE_PATH),
+                           ContentFile(open(SQUARE_IMAGE_PATH, 'rb').read()))
+        self.ps.save()
+        
+    def tearDown(self):
+        super(ImageResizeTest, self).tearDown()
+        self.pp.delete()
+        self.ps.delete()
+             
+    def test_resize_to_fit(self):
+        self.assertEquals(self.pl.get_test_size(), (100, 75))
+        self.assertEquals(self.pp.get_test_size(), (75, 100))
+        self.assertEquals(self.ps.get_test_size(), (100, 100))
+        
+    def test_resize_to_fit_width(self):
+        self.s.size = (100, 0)
+        self.s.save()
+        self.assertEquals(self.pl.get_test_size(), (100, 75))
+        self.assertEquals(self.pp.get_test_size(), (100, 133))
+        self.assertEquals(self.ps.get_test_size(), (100, 100))
+        
+    def test_resize_to_fit_width_enlarge(self):
+        self.s.size = (2000, 0)
+        self.s.upscale = True
+        self.s.save()
+        self.assertEquals(self.pl.get_test_size(), (2000, 1500))
+        self.assertEquals(self.pp.get_test_size(), (2000, 2667))
+        self.assertEquals(self.ps.get_test_size(), (2000, 2000))
+
+    def test_resize_to_fit_height(self):
+        self.s.size = (0, 100)
+        self.s.save()
+        self.assertEquals(self.pl.get_test_size(), (133, 100))
+        self.assertEquals(self.pp.get_test_size(), (75, 100))
+        self.assertEquals(self.ps.get_test_size(), (100, 100))
+        
+    def test_resize_to_fit_height_enlarge(self):
+        self.s.size = (0, 2000)
+        self.s.upscale = True
+        self.s.save()
+        self.assertEquals(self.pl.get_test_size(), (2667, 2000))
+        self.assertEquals(self.pp.get_test_size(), (1500, 2000))
+        self.assertEquals(self.ps.get_test_size(), (2000, 2000))
+        
+    def test_resize_and_crop(self):
+        self.s.crop = True
+        self.s.save()
+        self.assertEquals(self.pl.get_test_size(), self.s.size)
+        self.assertEquals(self.pp.get_test_size(), self.s.size)
+        self.assertEquals(self.ps.get_test_size(), self.s.size)
+        
+    def test_resize_rounding_to_fit(self):
+        self.s.size = (113, 113)
+        self.s.save()
+        self.assertEquals(self.pl.get_test_size(), (113, 85))
+        self.assertEquals(self.pp.get_test_size(), (85, 113))
+        self.assertEquals(self.ps.get_test_size(), (113, 113))  
+        
+    def test_resize_rounding_cropped(self):
+        self.s.size = (113, 113)
+        self.s.crop = True
+        self.s.save()
+        self.assertEquals(self.pl.get_test_size(), self.s.size)
+        self.assertEquals(self.pp.get_test_size(), self.s.size)
+        self.assertEquals(self.ps.get_test_size(), self.s.size)
+        
+    def test_resize_one_dimension_width(self):
+        self.s.size = (1500, 1200)
+        self.s.save()
+        self.assertEquals(self.pl.get_test_size(), (1500, 1125))
+        
+    def test_resize_one_dimension_height(self):
+        self.s.size = (1600, 1100)
+        self.s.save()
+        self.assertEquals(self.pl.get_test_size(), (1467, 1100))
+        
+    def test_resize_no_upscale(self):
+        self.s.size = (2000, 2000)
+        self.s.save()
+        self.assertEquals(self.pl.get_test_size(), (1600, 1200))
+        
+    def test_resize_no_upscale_mixed_height(self):
+        self.s.size = (3200, 600)
+        self.s.save()
+        self.assertEquals(self.pl.get_test_size(), (800, 600))
+        
+    def test_resize_no_upscale_mixed_width(self):
+        self.s.size = (800, 2400)
+        self.s.save()
+        self.assertEquals(self.pl.get_test_size(), (800, 600))
+        
+    def test_resize_no_upscale_crop(self):
+        self.s.size = (2000, 2000)
+        self.s.crop = True
+        self.s.save()
+        self.assertEquals(self.pl.get_test_size(), (2000, 2000))
+        
+    def test_resize_upscale(self):
+        self.s.size = (2000, 2000)
+        self.s.upscale = True
+        self.s.save()
+        self.assertEquals(self.pl.get_test_size(), (2000, 1500))
+        self.assertEquals(self.pp.get_test_size(), (1500, 2000))
+        self.assertEquals(self.ps.get_test_size(), (2000, 2000))
+
+
+class PhotoEffectTest(PLTest):
+    def test(self):
+        effect = PhotoEffect(name='test')
+        im = Image.open(self.pl.image.path)
+        self.assert_(isinstance(effect.pre_process(im), Image.Image))
+        self.assert_(isinstance(effect.post_process(im), Image.Image))
+        self.assert_(isinstance(effect.process(im), Image.Image))
+
+
+class PhotoSizeCacheTest(PLTest):
+    def test(self):
+        cache = PhotoSizeCache()
+        self.assertEqual(cache.sizes['test'], self.s)    
+        
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/photologue/urls.py	Sat Apr 14 16:40:29 2012 -0500
@@ -0,0 +1,36 @@
+from django.conf import settings
+from django.conf.urls import *
+from models import *
+
+# Number of random images from the gallery to display.
+SAMPLE_SIZE = ":%s" % getattr(settings, 'GALLERY_SAMPLE_SIZE', 5)
+
+# galleries
+gallery_args = {'date_field': 'date_added', 'allow_empty': True, 'queryset': Gallery.objects.filter(is_public=True), 'extra_context':{'sample_size':SAMPLE_SIZE}}
+urlpatterns = patterns('django.views.generic.date_based',
+    url(r'^gallery/(?P<year>\d{4})/(?P<month>[a-z]{3})/(?P<day>\w{1,2})/(?P<slug>[\-\d\w]+)/$', 'object_detail', {'date_field': 'date_added', 'slug_field': 'title_slug', 'queryset': Gallery.objects.filter(is_public=True), 'extra_context':{'sample_size':SAMPLE_SIZE}}, name='pl-gallery-detail'),
+    url(r'^gallery/(?P<year>\d{4})/(?P<month>[a-z]{3})/(?P<day>\w{1,2})/$', 'archive_day', gallery_args, name='pl-gallery-archive-day'),
+    url(r'^gallery/(?P<year>\d{4})/(?P<month>[a-z]{3})/$', 'archive_month', gallery_args, name='pl-gallery-archive-month'),
+    url(r'^gallery/(?P<year>\d{4})/$', 'archive_year', gallery_args, name='pl-gallery-archive-year'),
+    url(r'^gallery/?$', 'archive_index', gallery_args, name='pl-gallery-archive'),
+)
+urlpatterns += patterns('django.views.generic.list_detail',
+    url(r'^gallery/(?P<slug>[\-\d\w]+)/$', 'object_detail', {'slug_field': 'title_slug', 'queryset': Gallery.objects.filter(is_public=True), 'extra_context':{'sample_size':SAMPLE_SIZE}}, name='pl-gallery'),
+    url(r'^gallery/page/(?P<page>[0-9]+)/$', 'object_list', {'queryset': Gallery.objects.filter(is_public=True), 'allow_empty': True, 'paginate_by': 5, 'extra_context':{'sample_size':SAMPLE_SIZE}}, name='pl-gallery-list'),
+)
+
+# photographs
+photo_args = {'date_field': 'date_added', 'allow_empty': True, 'queryset': Photo.objects.filter(is_public=True)}
+urlpatterns += patterns('django.views.generic.date_based',
+    url(r'^photo/(?P<year>\d{4})/(?P<month>[a-z]{3})/(?P<day>\w{1,2})/(?P<slug>[\-\d\w]+)/$', 'object_detail', {'date_field': 'date_added', 'slug_field': 'title_slug', 'queryset': Photo.objects.filter(is_public=True)}, name='pl-photo-detail'),
+    url(r'^photo/(?P<year>\d{4})/(?P<month>[a-z]{3})/(?P<day>\w{1,2})/$', 'archive_day', photo_args, name='pl-photo-archive-day'),
+    url(r'^photo/(?P<year>\d{4})/(?P<month>[a-z]{3})/$', 'archive_month', photo_args, name='pl-photo-archive-month'),
+    url(r'^photo/(?P<year>\d{4})/$', 'archive_year', photo_args, name='pl-photo-archive-year'),
+    url(r'^photo/$', 'archive_index', photo_args, name='pl-photo-archive'),
+)
+urlpatterns += patterns('django.views.generic.list_detail',
+    url(r'^photo/(?P<slug>[\-\d\w]+)/$', 'object_detail', {'slug_field': 'title_slug', 'queryset': Photo.objects.filter(is_public=True)}, name='pl-photo'),
+    url(r'^photo/page/(?P<page>[0-9]+)/$', 'object_list', {'queryset': Photo.objects.filter(is_public=True), 'allow_empty': True, 'paginate_by': 20}, name='pl-photo-list'),
+)
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/photologue/utils/EXIF.py	Sat Apr 14 16:40:29 2012 -0500
@@ -0,0 +1,1568 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+#
+# Library to extract EXIF information from digital camera image files
+# http://sourceforge.net/projects/exif-py/
+#
+# VERSION 1.0.7
+#
+# To use this library call with:
+#    f = open(path_name, 'rb')
+#    tags = EXIF.process_file(f)
+#
+# To ignore makerNote tags, pass the -q or --quick
+# command line arguments, or as
+#    f = open(path_name, 'rb')
+#    tags = EXIF.process_file(f, details=False)
+#
+# To stop processing after a certain tag is retrieved,
+# pass the -t TAG or --stop-tag TAG argument, or as
+#    f = open(path_name, 'rb')
+#    tags = EXIF.process_file(f, stop_tag='TAG')
+#
+# where TAG is a valid tag name, ex 'DateTimeOriginal'
+#
+# These are useful when you are retrieving a large list of images
+#
+# Returned tags will be a dictionary mapping names of EXIF tags to their
+# values in the file named by path_name.  You can process the tags
+# as you wish.  In particular, you can iterate through all the tags with:
+#     for tag in tags.keys():
+#         if tag not in ('JPEGThumbnail', 'TIFFThumbnail', 'Filename',
+#                        'EXIF MakerNote'):
+#             print "Key: %s, value %s" % (tag, tags[tag])
+# (This code uses the if statement to avoid printing out a few of the
+# tags that tend to be long or boring.)
+#
+# The tags dictionary will include keys for all of the usual EXIF
+# tags, and will also include keys for Makernotes used by some
+# cameras, for which we have a good specification.
+#
+# Note that the dictionary keys are the IFD name followed by the
+# tag name. For example:
+# 'EXIF DateTimeOriginal', 'Image Orientation', 'MakerNote FocusMode'
+#
+# Copyright (c) 2002-2007 Gene Cash All rights reserved
+# Copyright (c) 2007 Ianaré Sévi All rights reserved
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+#
+#  1. Redistributions of source code must retain the above copyright
+#     notice, this list of conditions and the following disclaimer.
+#
+#  2. Redistributions in binary form must reproduce the above
+#     copyright notice, this list of conditions and the following
+#     disclaimer in the documentation and/or other materials provided
+#     with the distribution.
+#
+#  3. Neither the name of the authors nor the names of its contributors
+#     may be used to endorse or promote products derived from this
+#     software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+#
+#
+# ----- See 'changes.txt' file for all contributors and changes ----- #
+#
+
+
+# Don't throw an exception when given an out of range character.
+def make_string(seq):
+    str = ""
+    for c in seq:
+        # Screen out non-printing characters
+        if 32 <= c and c < 256:
+            str += chr(c)
+    # If no printing chars
+    if not str:
+        return seq
+    return str
+
+# Special version to deal with the code in the first 8 bytes of a user comment.
+def make_string_uc(seq):
+    code = seq[0:8]
+    seq = seq[8:]
+    # Of course, this is only correct if ASCII, and the standard explicitly
+    # allows JIS and Unicode.
+    return make_string(seq)
+
+# field type descriptions as (length, abbreviation, full name) tuples
+FIELD_TYPES = (
+    (0, 'X', 'Proprietary'), # no such type
+    (1, 'B', 'Byte'),
+    (1, 'A', 'ASCII'),
+    (2, 'S', 'Short'),
+    (4, 'L', 'Long'),
+    (8, 'R', 'Ratio'),
+    (1, 'SB', 'Signed Byte'),
+    (1, 'U', 'Undefined'),
+    (2, 'SS', 'Signed Short'),
+    (4, 'SL', 'Signed Long'),
+    (8, 'SR', 'Signed Ratio'),
+    )
+
+# dictionary of main EXIF tag names
+# first element of tuple is tag name, optional second element is
+# another dictionary giving names to values
+EXIF_TAGS = {
+    0x0100: ('ImageWidth', ),
+    0x0101: ('ImageLength', ),
+    0x0102: ('BitsPerSample', ),
+    0x0103: ('Compression',
+             {1: 'Uncompressed TIFF',
+              6: 'JPEG Compressed'}),
+    0x0106: ('PhotometricInterpretation', ),
+    0x010A: ('FillOrder', ),
+    0x010D: ('DocumentName', ),
+    0x010E: ('ImageDescription', ),
+    0x010F: ('Make', ),
+    0x0110: ('Model', ),
+    0x0111: ('StripOffsets', ),
+    0x0112: ('Orientation',
+             {1: 'Horizontal (normal)',
+              2: 'Mirrored horizontal',
+              3: 'Rotated 180',
+              4: 'Mirrored vertical',
+              5: 'Mirrored horizontal then rotated 90 CCW',
+              6: 'Rotated 90 CW',
+              7: 'Mirrored horizontal then rotated 90 CW',
+              8: 'Rotated 90 CCW'}),
+    0x0115: ('SamplesPerPixel', ),
+    0x0116: ('RowsPerStrip', ),
+    0x0117: ('StripByteCounts', ),
+    0x011A: ('XResolution', ),
+    0x011B: ('YResolution', ),
+    0x011C: ('PlanarConfiguration', ),
+    0x0128: ('ResolutionUnit',
+             {1: 'Not Absolute',
+              2: 'Pixels/Inch',
+              3: 'Pixels/Centimeter'}),
+    0x012D: ('TransferFunction', ),
+    0x0131: ('Software', ),
+    0x0132: ('DateTime', ),
+    0x013B: ('Artist', ),
+    0x013E: ('WhitePoint', ),
+    0x013F: ('PrimaryChromaticities', ),
+    0x0156: ('TransferRange', ),
+    0x0200: ('JPEGProc', ),
+    0x0201: ('JPEGInterchangeFormat', ),
+    0x0202: ('JPEGInterchangeFormatLength', ),
+    0x0211: ('YCbCrCoefficients', ),
+    0x0212: ('YCbCrSubSampling', ),
+    0x0213: ('YCbCrPositioning', ),
+    0x0214: ('ReferenceBlackWhite', ),
+    0x828D: ('CFARepeatPatternDim', ),
+    0x828E: ('CFAPattern', ),
+    0x828F: ('BatteryLevel', ),
+    0x8298: ('Copyright', ),
+    0x829A: ('ExposureTime', ),
+    0x829D: ('FNumber', ),
+    0x83BB: ('IPTC/NAA', ),
+    0x8769: ('ExifOffset', ),
+    0x8773: ('InterColorProfile', ),
+    0x8822: ('ExposureProgram',
+             {0: 'Unidentified',
+              1: 'Manual',
+              2: 'Program Normal',
+              3: 'Aperture Priority',
+              4: 'Shutter Priority',
+              5: 'Program Creative',
+              6: 'Program Action',
+              7: 'Portrait Mode',
+              8: 'Landscape Mode'}),
+    0x8824: ('SpectralSensitivity', ),
+    0x8825: ('GPSInfo', ),
+    0x8827: ('ISOSpeedRatings', ),
+    0x8828: ('OECF', ),
+    # print as string
+    0x9000: ('ExifVersion', make_string),
+    0x9003: ('DateTimeOriginal', ),
+    0x9004: ('DateTimeDigitized', ),
+    0x9101: ('ComponentsConfiguration',
+             {0: '',
+              1: 'Y',
+              2: 'Cb',
+              3: 'Cr',
+              4: 'Red',
+              5: 'Green',
+              6: 'Blue'}),
+    0x9102: ('CompressedBitsPerPixel', ),
+    0x9201: ('ShutterSpeedValue', ),
+    0x9202: ('ApertureValue', ),
+    0x9203: ('BrightnessValue', ),
+    0x9204: ('ExposureBiasValue', ),
+    0x9205: ('MaxApertureValue', ),
+    0x9206: ('SubjectDistance', ),
+    0x9207: ('MeteringMode',
+             {0: 'Unidentified',
+              1: 'Average',
+              2: 'CenterWeightedAverage',
+              3: 'Spot',
+              4: 'MultiSpot'}),
+    0x9208: ('LightSource',
+             {0: 'Unknown',
+              1: 'Daylight',
+              2: 'Fluorescent',
+              3: 'Tungsten',
+              10: 'Flash',
+              17: 'Standard Light A',
+              18: 'Standard Light B',
+              19: 'Standard Light C',
+              20: 'D55',
+              21: 'D65',
+              22: 'D75',
+              255: 'Other'}),
+    0x9209: ('Flash', {0: 'No',
+                       1: 'Fired',
+                       5: 'Fired (?)', # no return sensed
+                       7: 'Fired (!)', # return sensed
+                       9: 'Fill Fired',
+                       13: 'Fill Fired (?)',
+                       15: 'Fill Fired (!)',
+                       16: 'Off',
+                       24: 'Auto Off',
+                       25: 'Auto Fired',
+                       29: 'Auto Fired (?)',
+                       31: 'Auto Fired (!)',
+                       32: 'Not Available'}),
+    0x920A: ('FocalLength', ),
+    0x9214: ('SubjectArea', ),
+    0x927C: ('MakerNote', ),
+    # print as string
+    0x9286: ('UserComment', make_string_uc),  # First 8 bytes gives coding system e.g. ASCII vs. JIS vs Unicode
+    0x9290: ('SubSecTime', ),
+    0x9291: ('SubSecTimeOriginal', ),
+    0x9292: ('SubSecTimeDigitized', ),
+    # print as string
+    0xA000: ('FlashPixVersion', make_string),
+    0xA001: ('ColorSpace', ),
+    0xA002: ('ExifImageWidth', ),
+    0xA003: ('ExifImageLength', ),
+    0xA005: ('InteroperabilityOffset', ),
+    0xA20B: ('FlashEnergy', ),               # 0x920B in TIFF/EP
+    0xA20C: ('SpatialFrequencyResponse', ),  # 0x920C    -  -
+    0xA20E: ('FocalPlaneXResolution', ),     # 0x920E    -  -
+    0xA20F: ('FocalPlaneYResolution', ),     # 0x920F    -  -
+    0xA210: ('FocalPlaneResolutionUnit', ),  # 0x9210    -  -
+    0xA214: ('SubjectLocation', ),           # 0x9214    -  -
+    0xA215: ('ExposureIndex', ),             # 0x9215    -  -
+    0xA217: ('SensingMethod', ),             # 0x9217    -  -
+    0xA300: ('FileSource',
+             {3: 'Digital Camera'}),
+    0xA301: ('SceneType',
+             {1: 'Directly Photographed'}),
+    0xA302: ('CVAPattern', ),
+    0xA401: ('CustomRendered', ),
+    0xA402: ('ExposureMode',
+             {0: 'Auto Exposure',
+              1: 'Manual Exposure',
+              2: 'Auto Bracket'}),
+    0xA403: ('WhiteBalance',
+             {0: 'Auto',
+              1: 'Manual'}),
+    0xA404: ('DigitalZoomRatio', ),
+    0xA405: ('FocalLengthIn35mm', ),
+    0xA406: ('SceneCaptureType', ),
+    0xA407: ('GainControl', ),
+    0xA408: ('Contrast', ),
+    0xA409: ('Saturation', ),
+    0xA40A: ('Sharpness', ),
+    0xA40C: ('SubjectDistanceRange', ),
+    }
+
+# interoperability tags
+INTR_TAGS = {
+    0x0001: ('InteroperabilityIndex', ),
+    0x0002: ('InteroperabilityVersion', ),
+    0x1000: ('RelatedImageFileFormat', ),
+    0x1001: ('RelatedImageWidth', ),
+    0x1002: ('RelatedImageLength', ),
+    }
+
+# GPS tags (not used yet, haven't seen camera with GPS)
+GPS_TAGS = {
+    0x0000: ('GPSVersionID', ),
+    0x0001: ('GPSLatitudeRef', ),
+    0x0002: ('GPSLatitude', ),
+    0x0003: ('GPSLongitudeRef', ),
+    0x0004: ('GPSLongitude', ),
+    0x0005: ('GPSAltitudeRef', ),
+    0x0006: ('GPSAltitude', ),
+    0x0007: ('GPSTimeStamp', ),
+    0x0008: ('GPSSatellites', ),
+    0x0009: ('GPSStatus', ),
+    0x000A: ('GPSMeasureMode', ),
+    0x000B: ('GPSDOP', ),
+    0x000C: ('GPSSpeedRef', ),
+    0x000D: ('GPSSpeed', ),
+    0x000E: ('GPSTrackRef', ),
+    0x000F: ('GPSTrack', ),
+    0x0010: ('GPSImgDirectionRef', ),
+    0x0011: ('GPSImgDirection', ),
+    0x0012: ('GPSMapDatum', ),
+    0x0013: ('GPSDestLatitudeRef', ),
+    0x0014: ('GPSDestLatitude', ),
+    0x0015: ('GPSDestLongitudeRef', ),
+    0x0016: ('GPSDestLongitude', ),
+    0x0017: ('GPSDestBearingRef', ),
+    0x0018: ('GPSDestBearing', ),
+    0x0019: ('GPSDestDistanceRef', ),
+    0x001A: ('GPSDestDistance', ),
+    }
+
+# Ignore these tags when quick processing
+# 0x927C is MakerNote Tags
+# 0x9286 is user comment
+IGNORE_TAGS=(0x9286, 0x927C)
+
+# http://tomtia.plala.jp/DigitalCamera/MakerNote/index.asp
+def nikon_ev_bias(seq):
+    # First digit seems to be in steps of 1/6 EV.
+    # Does the third value mean the step size?  It is usually 6,
+    # but it is 12 for the ExposureDifference.
+    #
+    if seq == [252, 1, 6, 0]:
+        return "-2/3 EV"
+    if seq == [253, 1, 6, 0]:
+        return "-1/2 EV"
+    if seq == [254, 1, 6, 0]:
+        return "-1/3 EV"
+    if seq == [0, 1, 6, 0]:
+        return "0 EV"
+    if seq == [2, 1, 6, 0]:
+        return "+1/3 EV"
+    if seq == [3, 1, 6, 0]:
+        return "+1/2 EV"
+    if seq == [4, 1, 6, 0]:
+        return "+2/3 EV"
+    # Handle combinations not in the table.
+    a = seq[0]
+    # Causes headaches for the +/- logic, so special case it.
+    if a == 0:
+        return "0 EV"
+    if a > 127:
+        a = 256 - a
+        ret_str = "-"
+    else:
+        ret_str = "+"
+    b = seq[2]	# Assume third value means the step size
+    whole = a / b
+    a = a % b
+    if whole != 0:
+        ret_str = ret_str + str(whole) + " "
+    if a == 0:
+        ret_str = ret_str + "EV"
+    else:
+        r = Ratio(a, b)
+        ret_str = ret_str + r.__repr__() + " EV"
+    return ret_str
+
+# Nikon E99x MakerNote Tags
+MAKERNOTE_NIKON_NEWER_TAGS={
+    0x0001: ('MakernoteVersion', make_string),	# Sometimes binary
+    0x0002: ('ISOSetting', ),
+    0x0003: ('ColorMode', ),
+    0x0004: ('Quality', ),
+    0x0005: ('Whitebalance', ),
+    0x0006: ('ImageSharpening', ),
+    0x0007: ('FocusMode', ),
+    0x0008: ('FlashSetting', ),
+    0x0009: ('AutoFlashMode', ),
+    0x000B: ('WhiteBalanceBias', ),
+    0x000C: ('WhiteBalanceRBCoeff', ),
+    0x000D: ('ProgramShift', nikon_ev_bias),
+    # Nearly the same as the other EV vals, but step size is 1/12 EV (?)
+    0x000E: ('ExposureDifference', nikon_ev_bias),
+    0x000F: ('ISOSelection', ),
+    0x0011: ('NikonPreview', ),
+    0x0012: ('FlashCompensation', nikon_ev_bias),
+    0x0013: ('ISOSpeedRequested', ),
+    0x0016: ('PhotoCornerCoordinates', ),
+    # 0x0017: Unknown, but most likely an EV value
+    0x0018: ('FlashBracketCompensationApplied', nikon_ev_bias),
+    0x0019: ('AEBracketCompensationApplied', ),
+    0x001A: ('ImageProcessing', ),
+    0x0080: ('ImageAdjustment', ),
+    0x0081: ('ToneCompensation', ),
+    0x0082: ('AuxiliaryLens', ),
+    0x0083: ('LensType', ),
+    0x0084: ('LensMinMaxFocalMaxAperture', ),
+    0x0085: ('ManualFocusDistance', ),
+    0x0086: ('DigitalZoomFactor', ),
+    0x0087: ('FlashMode',
+             {0x00: 'Did Not Fire',
+              0x01: 'Fired, Manual',
+              0x07: 'Fired, External',
+              0x08: 'Fired, Commander Mode ',
+              0x09: 'Fired, TTL Mode'}),
+    0x0088: ('AFFocusPosition',
+             {0x0000: 'Center',
+              0x0100: 'Top',
+              0x0200: 'Bottom',
+              0x0300: 'Left',
+              0x0400: 'Right'}),
+    0x0089: ('BracketingMode',
+             {0x00: 'Single frame, no bracketing',
+              0x01: 'Continuous, no bracketing',
+              0x02: 'Timer, no bracketing',
+              0x10: 'Single frame, exposure bracketing',
+              0x11: 'Continuous, exposure bracketing',
+              0x12: 'Timer, exposure bracketing',
+              0x40: 'Single frame, white balance bracketing',
+              0x41: 'Continuous, white balance bracketing',
+              0x42: 'Timer, white balance bracketing'}),
+    0x008A: ('AutoBracketRelease', ),
+    0x008B: ('LensFStops', ),
+    0x008C: ('NEFCurve2', ),
+    0x008D: ('ColorMode', ),
+    0x008F: ('SceneMode', ),
+    0x0090: ('LightingType', ),
+    0x0091: ('ShotInfo', ),	# First 4 bytes are probably a version number in ASCII
+    0x0092: ('HueAdjustment', ),
+    # 0x0093: ('SaturationAdjustment', ),
+    0x0094: ('Saturation',	# Name conflict with 0x00AA !!
+             {-3: 'B&W',
+              -2: '-2',
+              -1: '-1',
+              0: '0',
+              1: '1',
+              2: '2'}),
+    0x0095: ('NoiseReduction', ),
+    0x0096: ('NEFCurve2', ),
+    0x0097: ('ColorBalance', ),
+    0x0098: ('LensData', ),	# First 4 bytes are a version number in ASCII
+    0x0099: ('RawImageCenter', ),
+    0x009A: ('SensorPixelSize', ),
+    0x009C: ('Scene Assist', ),
+    0x00A0: ('SerialNumber', ),
+    0x00A2: ('ImageDataSize', ),
+    # A4: In NEF, looks like a 4 byte ASCII version number
+    0x00A5: ('ImageCount', ),
+    0x00A6: ('DeletedImageCount', ),
+    0x00A7: ('TotalShutterReleases', ),
+    # A8: ExposureMode?  JPG: First 4 bytes are probably a version number in ASCII
+    # But in a sample NEF, its 8 zeros, then the string "NORMAL"
+    0x00A9: ('ImageOptimization', ),
+    0x00AA: ('Saturation', ),
+    0x00AB: ('DigitalVariProgram', ),
+    0x00AC: ('ImageStabilization', ),
+    0x00AD: ('Responsive AF', ),	# 'AFResponse'
+    0x0010: ('DataDump', ),
+    }
+
+MAKERNOTE_NIKON_OLDER_TAGS = {
+    0x0003: ('Quality',
+             {1: 'VGA Basic',
+              2: 'VGA Normal',
+              3: 'VGA Fine',
+              4: 'SXGA Basic',
+              5: 'SXGA Normal',
+              6: 'SXGA Fine'}),
+    0x0004: ('ColorMode',
+             {1: 'Color',
+              2: 'Monochrome'}),
+    0x0005: ('ImageAdjustment',
+             {0: 'Normal',
+              1: 'Bright+',
+              2: 'Bright-',
+              3: 'Contrast+',
+              4: 'Contrast-'}),
+    0x0006: ('CCDSpeed',
+             {0: 'ISO 80',
+              2: 'ISO 160',
+              4: 'ISO 320',
+              5: 'ISO 100'}),
+    0x0007: ('WhiteBalance',
+             {0: 'Auto',
+              1: 'Preset',
+              2: 'Daylight',
+              3: 'Incandescent',
+              4: 'Fluorescent',
+              5: 'Cloudy',
+              6: 'Speed Light'}),
+    }
+
+# decode Olympus SpecialMode tag in MakerNote
+def olympus_special_mode(v):
+    a={
+        0: 'Normal',
+        1: 'Unknown',
+        2: 'Fast',
+        3: 'Panorama'}
+    b={
+        0: 'Non-panoramic',
+        1: 'Left to right',
+        2: 'Right to left',
+        3: 'Bottom to top',
+        4: 'Top to bottom'}
+    if v[0] not in a or v[2] not in b:
+        return v
+    return '%s - sequence %d - %s' % (a[v[0]], v[1], b[v[2]])
+
+MAKERNOTE_OLYMPUS_TAGS={
+    # ah HAH! those sneeeeeaky bastids! this is how they get past the fact
+    # that a JPEG thumbnail is not allowed in an uncompressed TIFF file
+    0x0100: ('JPEGThumbnail', ),
+    0x0200: ('SpecialMode', olympus_special_mode),
+    0x0201: ('JPEGQual',
+             {1: 'SQ',
+              2: 'HQ',
+              3: 'SHQ'}),
+    0x0202: ('Macro',
+             {0: 'Normal',
+             1: 'Macro',
+             2: 'SuperMacro'}),
+    0x0203: ('BWMode',
+             {0: 'Off',
+             1: 'On'}),
+    0x0204: ('DigitalZoom', ),
+    0x0205: ('FocalPlaneDiagonal', ),
+    0x0206: ('LensDistortionParams', ),
+    0x0207: ('SoftwareRelease', ),
+    0x0208: ('PictureInfo', ),
+    0x0209: ('CameraID', make_string), # print as string
+    0x0F00: ('DataDump', ),
+    0x0300: ('PreCaptureFrames', ),
+    0x0404: ('SerialNumber', ),
+    0x1000: ('ShutterSpeedValue', ),
+    0x1001: ('ISOValue', ),
+    0x1002: ('ApertureValue', ),
+    0x1003: ('BrightnessValue', ),
+    0x1004: ('FlashMode', ),
+    0x1004: ('FlashMode',
+       {2: 'On',
+        3: 'Off'}),
+    0x1005: ('FlashDevice',
+       {0: 'None',
+        1: 'Internal',
+        4: 'External',
+        5: 'Internal + External'}),
+    0x1006: ('ExposureCompensation', ),
+    0x1007: ('SensorTemperature', ),
+    0x1008: ('LensTemperature', ),
+    0x100b: ('FocusMode',
+       {0: 'Auto',
+        1: 'Manual'}),
+    0x1017: ('RedBalance', ),
+    0x1018: ('BlueBalance', ),
+    0x101a: ('SerialNumber', ),
+    0x1023: ('FlashExposureComp', ),
+    0x1026: ('ExternalFlashBounce',
+       {0: 'No',
+        1: 'Yes'}),
+    0x1027: ('ExternalFlashZoom', ),
+    0x1028: ('ExternalFlashMode', ),
+    0x1029: ('Contrast 	int16u',
+       {0: 'High',
+        1: 'Normal',
+        2: 'Low'}),
+    0x102a: ('SharpnessFactor', ),
+    0x102b: ('ColorControl', ),
+    0x102c: ('ValidBits', ),
+    0x102d: ('CoringFilter', ),
+    0x102e: ('OlympusImageWidth', ),
+    0x102f: ('OlympusImageHeight', ),
+    0x1034: ('CompressionRatio', ),
+    0x1035: ('PreviewImageValid',
+       {0: 'No',
+        1: 'Yes'}),
+    0x1036: ('PreviewImageStart', ),
+    0x1037: ('PreviewImageLength', ),
+    0x1039: ('CCDScanMode',
+       {0: 'Interlaced',
+        1: 'Progressive'}),
+    0x103a: ('NoiseReduction',
+       {0: 'Off',
+        1: 'On'}),
+    0x103b: ('InfinityLensStep', ),
+    0x103c: ('NearLensStep', ),
+
+    # TODO - these need extra definitions
+    # http://search.cpan.org/src/EXIFTOOL/Image-ExifTool-6.90/html/TagNames/Olympus.html
+    0x2010: ('Equipment', ),
+    0x2020: ('CameraSettings', ),
+    0x2030: ('RawDevelopment', ),
+    0x2040: ('ImageProcessing', ),
+    0x2050: ('FocusInfo', ),
+    0x3000: ('RawInfo ', ),
+    }
+
+# 0x2020 CameraSettings
+MAKERNOTE_OLYMPUS_TAG_0x2020={
+    0x0100: ('PreviewImageValid',
+        {0: 'No',
+         1: 'Yes'}),
+    0x0101: ('PreviewImageStart', ),
+    0x0102: ('PreviewImageLength', ),
+    0x0200: ('ExposureMode', {
+        1: 'Manual',
+        2: 'Program',
+        3: 'Aperture-priority AE',
+        4: 'Shutter speed priority AE',
+        5: 'Program-shift'}),
+    0x0201: ('AELock',
+       {0: 'Off',
+        1: 'On'}),
+    0x0202: ('MeteringMode',
+       {2: 'Center Weighted',
+        3: 'Spot',
+        5: 'ESP',
+        261: 'Pattern+AF',
+        515: 'Spot+Highlight control',
+        1027: 'Spot+Shadow control'}),
+    0x0300: ('MacroMode',
+       {0: 'Off',
+        1: 'On'}),
+    0x0301: ('FocusMode',
+       {0: 'Single AF',
+        1: 'Sequential shooting AF',
+        2: 'Continuous AF',
+        3: 'Multi AF',
+        10: 'MF'}),
+    0x0302: ('FocusProcess',
+       {0: 'AF Not Used',
+        1: 'AF Used'}),
+    0x0303: ('AFSearch',
+       {0: 'Not Ready',
+        1: 'Ready'}),
+    0x0304: ('AFAreas', ),
+    0x0401: ('FlashExposureCompensation', ),
+    0x0500: ('WhiteBalance2',
+       {0: 'Auto',
+        16: '7500K (Fine Weather with Shade)',
+        17: '6000K (Cloudy)',
+        18: '5300K (Fine Weather)',
+        20: '3000K (Tungsten light)',
+        21: '3600K (Tungsten light-like)',
+        33: '6600K (Daylight fluorescent)',
+        34: '4500K (Neutral white fluorescent)',
+        35: '4000K (Cool white fluorescent)',
+        48: '3600K (Tungsten light-like)',
+        256: 'Custom WB 1',
+        257: 'Custom WB 2',
+        258: 'Custom WB 3',
+        259: 'Custom WB 4',
+        512: 'Custom WB 5400K',
+        513: 'Custom WB 2900K',
+        514: 'Custom WB 8000K', }),
+    0x0501: ('WhiteBalanceTemperature', ),
+    0x0502: ('WhiteBalanceBracket', ),
+    0x0503: ('CustomSaturation', ), # (3 numbers: 1. CS Value, 2. Min, 3. Max)
+    0x0504: ('ModifiedSaturation',
+       {0: 'Off',
+        1: 'CM1 (Red Enhance)',
+        2: 'CM2 (Green Enhance)',
+        3: 'CM3 (Blue Enhance)',
+        4: 'CM4 (Skin Tones)'}),
+    0x0505: ('ContrastSetting', ), # (3 numbers: 1. Contrast, 2. Min, 3. Max)
+    0x0506: ('SharpnessSetting', ), # (3 numbers: 1. Sharpness, 2. Min, 3. Max)
+    0x0507: ('ColorSpace',
+       {0: 'sRGB',
+        1: 'Adobe RGB',
+        2: 'Pro Photo RGB'}),
+    0x0509: ('SceneMode',
+       {0: 'Standard',
+        6: 'Auto',
+        7: 'Sport',
+        8: 'Portrait',
+        9: 'Landscape+Portrait',
+        10: 'Landscape',
+        11: 'Night scene',
+        13: 'Panorama',
+        16: 'Landscape+Portrait',
+        17: 'Night+Portrait',
+        19: 'Fireworks',
+        20: 'Sunset',
+        22: 'Macro',
+        25: 'Documents',
+        26: 'Museum',
+        28: 'Beach&Snow',
+        30: 'Candle',
+        35: 'Underwater Wide1',
+        36: 'Underwater Macro',
+        39: 'High Key',
+        40: 'Digital Image Stabilization',
+        44: 'Underwater Wide2',
+        45: 'Low Key',
+        46: 'Children',
+        48: 'Nature Macro'}),
+    0x050a: ('NoiseReduction',
+       {0: 'Off',
+        1: 'Noise Reduction',
+        2: 'Noise Filter',
+        3: 'Noise Reduction + Noise Filter',
+        4: 'Noise Filter (ISO Boost)',
+        5: 'Noise Reduction + Noise Filter (ISO Boost)'}),
+    0x050b: ('DistortionCorrection',
+       {0: 'Off',
+        1: 'On'}),
+    0x050c: ('ShadingCompensation',
+       {0: 'Off',
+        1: 'On'}),
+    0x050d: ('CompressionFactor', ),
+    0x050f: ('Gradation',
+       {'-1 -1 1': 'Low Key',
+        '0 -1 1': 'Normal',
+        '1 -1 1': 'High Key'}),
+    0x0520: ('PictureMode',
+       {1: 'Vivid',
+        2: 'Natural',
+        3: 'Muted',
+        256: 'Monotone',
+        512: 'Sepia'}),
+    0x0521: ('PictureModeSaturation', ),
+    0x0522: ('PictureModeHue?', ),
+    0x0523: ('PictureModeContrast', ),
+    0x0524: ('PictureModeSharpness', ),
+    0x0525: ('PictureModeBWFilter',
+       {0: 'n/a',
+        1: 'Neutral',
+        2: 'Yellow',
+        3: 'Orange',
+        4: 'Red',
+        5: 'Green'}),
+    0x0526: ('PictureModeTone',
+       {0: 'n/a',
+        1: 'Neutral',
+        2: 'Sepia',
+        3: 'Blue',
+        4: 'Purple',
+        5: 'Green'}),
+    0x0600: ('Sequence', ), # 2 or 3 numbers: 1. Mode, 2. Shot number, 3. Mode bits
+    0x0601: ('PanoramaMode', ), # (2 numbers: 1. Mode, 2. Shot number)
+    0x0603: ('ImageQuality2',
+       {1: 'SQ',
+        2: 'HQ',
+        3: 'SHQ',
+        4: 'RAW'}),
+    0x0901: ('ManometerReading', ),
+    }
+
+
+MAKERNOTE_CASIO_TAGS={
+    0x0001: ('RecordingMode',
+             {1: 'Single Shutter',
+              2: 'Panorama',
+              3: 'Night Scene',
+              4: 'Portrait',
+              5: 'Landscape'}),
+    0x0002: ('Quality',
+             {1: 'Economy',
+              2: 'Normal',
+              3: 'Fine'}),
+    0x0003: ('FocusingMode',
+             {2: 'Macro',
+              3: 'Auto Focus',
+              4: 'Manual Focus',
+              5: 'Infinity'}),
+    0x0004: ('FlashMode',
+             {1: 'Auto',
+              2: 'On',
+              3: 'Off',
+              4: 'Red Eye Reduction'}),
+    0x0005: ('FlashIntensity',
+             {11: 'Weak',
+              13: 'Normal',
+              15: 'Strong'}),
+    0x0006: ('Object Distance', ),
+    0x0007: ('WhiteBalance',
+             {1: 'Auto',
+              2: 'Tungsten',
+              3: 'Daylight',
+              4: 'Fluorescent',
+              5: 'Shade',
+              129: 'Manual'}),
+    0x000B: ('Sharpness',
+             {0: 'Normal',
+              1: 'Soft',
+              2: 'Hard'}),
+    0x000C: ('Contrast',
+             {0: 'Normal',
+              1: 'Low',
+              2: 'High'}),
+    0x000D: ('Saturation',
+             {0: 'Normal',
+              1: 'Low',
+              2: 'High'}),
+    0x0014: ('CCDSpeed',
+             {64: 'Normal',
+              80: 'Normal',
+              100: 'High',
+              125: '+1.0',
+              244: '+3.0',
+              250: '+2.0'}),
+    }
+
+MAKERNOTE_FUJIFILM_TAGS={
+    0x0000: ('NoteVersion', make_string),
+    0x1000: ('Quality', ),
+    0x1001: ('Sharpness',
+             {1: 'Soft',
+              2: 'Soft',
+              3: 'Normal',
+              4: 'Hard',
+              5: 'Hard'}),
+    0x1002: ('WhiteBalance',
+             {0: 'Auto',
+              256: 'Daylight',
+              512: 'Cloudy',
+              768: 'DaylightColor-Fluorescent',
+              769: 'DaywhiteColor-Fluorescent',
+              770: 'White-Fluorescent',
+              1024: 'Incandescent',
+              3840: 'Custom'}),
+    0x1003: ('Color',
+             {0: 'Normal',
+              256: 'High',
+              512: 'Low'}),
+    0x1004: ('Tone',
+             {0: 'Normal',
+              256: 'High',
+              512: 'Low'}),
+    0x1010: ('FlashMode',
+             {0: 'Auto',
+              1: 'On',
+              2: 'Off',
+              3: 'Red Eye Reduction'}),
+    0x1011: ('FlashStrength', ),
+    0x1020: ('Macro',
+             {0: 'Off',
+              1: 'On'}),
+    0x1021: ('FocusMode',
+             {0: 'Auto',
+              1: 'Manual'}),
+    0x1030: ('SlowSync',
+             {0: 'Off',
+              1: 'On'}),
+    0x1031: ('PictureMode',
+             {0: 'Auto',
+              1: 'Portrait',
+              2: 'Landscape',
+              4: 'Sports',
+              5: 'Night',
+              6: 'Program AE',
+              256: 'Aperture Priority AE',
+              512: 'Shutter Priority AE',
+              768: 'Manual Exposure'}),
+    0x1100: ('MotorOrBracket',
+             {0: 'Off',
+              1: 'On'}),
+    0x1300: ('BlurWarning',
+             {0: 'Off',
+              1: 'On'}),
+    0x1301: ('FocusWarning',
+             {0: 'Off',
+              1: 'On'}),
+    0x1302: ('AEWarning',
+             {0: 'Off',
+              1: 'On'}),
+    }
+
+MAKERNOTE_CANON_TAGS = {
+    0x0006: ('ImageType', ),
+    0x0007: ('FirmwareVersion', ),
+    0x0008: ('ImageNumber', ),
+    0x0009: ('OwnerName', ),
+    }
+
+# this is in element offset, name, optional value dictionary format
+MAKERNOTE_CANON_TAG_0x001 = {
+    1: ('Macromode',
+        {1: 'Macro',
+         2: 'Normal'}),
+    2: ('SelfTimer', ),
+    3: ('Quality',
+        {2: 'Normal',
+         3: 'Fine',
+         5: 'Superfine'}),
+    4: ('FlashMode',
+        {0: 'Flash Not Fired',
+         1: 'Auto',
+         2: 'On',
+         3: 'Red-Eye Reduction',
+         4: 'Slow Synchro',
+         5: 'Auto + Red-Eye Reduction',
+         6: 'On + Red-Eye Reduction',
+         16: 'external flash'}),
+    5: ('ContinuousDriveMode',
+        {0: 'Single Or Timer',
+         1: 'Continuous'}),
+    7: ('FocusMode',
+        {0: 'One-Shot',
+         1: 'AI Servo',
+         2: 'AI Focus',
+         3: 'MF',
+         4: 'Single',
+         5: 'Continuous',
+         6: 'MF'}),
+    10: ('ImageSize',
+         {0: 'Large',
+          1: 'Medium',
+          2: 'Small'}),
+    11: ('EasyShootingMode',
+         {0: 'Full Auto',
+          1: 'Manual',
+          2: 'Landscape',
+          3: 'Fast Shutter',
+          4: 'Slow Shutter',
+          5: 'Night',
+          6: 'B&W',
+          7: 'Sepia',
+          8: 'Portrait',
+          9: 'Sports',
+          10: 'Macro/Close-Up',
+          11: 'Pan Focus'}),
+    12: ('DigitalZoom',
+         {0: 'None',
+          1: '2x',
+          2: '4x'}),
+    13: ('Contrast',
+         {0xFFFF: 'Low',
+          0: 'Normal',
+          1: 'High'}),
+    14: ('Saturation',
+         {0xFFFF: 'Low',
+          0: 'Normal',
+          1: 'High'}),
+    15: ('Sharpness',
+         {0xFFFF: 'Low',
+          0: 'Normal',
+          1: 'High'}),
+    16: ('ISO',
+         {0: 'See ISOSpeedRatings Tag',
+          15: 'Auto',
+          16: '50',
+          17: '100',
+          18: '200',
+          19: '400'}),
+    17: ('MeteringMode',
+         {3: 'Evaluative',
+          4: 'Partial',
+          5: 'Center-weighted'}),
+    18: ('FocusType',
+         {0: 'Manual',
+          1: 'Auto',
+          3: 'Close-Up (Macro)',
+          8: 'Locked (Pan Mode)'}),
+    19: ('AFPointSelected',
+         {0x3000: 'None (MF)',
+          0x3001: 'Auto-Selected',
+          0x3002: 'Right',
+          0x3003: 'Center',
+          0x3004: 'Left'}),
+    20: ('ExposureMode',
+         {0: 'Easy Shooting',
+          1: 'Program',
+          2: 'Tv-priority',
+          3: 'Av-priority',
+          4: 'Manual',
+          5: 'A-DEP'}),
+    23: ('LongFocalLengthOfLensInFocalUnits', ),
+    24: ('ShortFocalLengthOfLensInFocalUnits', ),
+    25: ('FocalUnitsPerMM', ),
+    28: ('FlashActivity',
+         {0: 'Did Not Fire',
+          1: 'Fired'}),
+    29: ('FlashDetails',
+         {14: 'External E-TTL',
+          13: 'Internal Flash',
+          11: 'FP Sync Used',
+          7: '2nd("Rear")-Curtain Sync Used',
+          4: 'FP Sync Enabled'}),
+    32: ('FocusMode',
+         {0: 'Single',
+          1: 'Continuous'}),
+    }
+
+MAKERNOTE_CANON_TAG_0x004 = {
+    7: ('WhiteBalance',
+        {0: 'Auto',
+         1: 'Sunny',
+         2: 'Cloudy',
+         3: 'Tungsten',
+         4: 'Fluorescent',
+         5: 'Flash',
+         6: 'Custom'}),
+    9: ('SequenceNumber', ),
+    14: ('AFPointUsed', ),
+    15: ('FlashBias',
+        {0XFFC0: '-2 EV',
+         0XFFCC: '-1.67 EV',
+         0XFFD0: '-1.50 EV',
+         0XFFD4: '-1.33 EV',
+         0XFFE0: '-1 EV',
+         0XFFEC: '-0.67 EV',
+         0XFFF0: '-0.50 EV',
+         0XFFF4: '-0.33 EV',
+         0X0000: '0 EV',
+         0X000C: '0.33 EV',
+         0X0010: '0.50 EV',
+         0X0014: '0.67 EV',
+         0X0020: '1 EV',
+         0X002C: '1.33 EV',
+         0X0030: '1.50 EV',
+         0X0034: '1.67 EV',
+         0X0040: '2 EV'}),
+    19: ('SubjectDistance', ),
+    }
+
+# extract multibyte integer in Motorola format (little endian)
+def s2n_motorola(str):
+    x = 0
+    for c in str:
+        x = (x << 8) | ord(c)
+    return x
+
+# extract multibyte integer in Intel format (big endian)
+def s2n_intel(str):
+    x = 0
+    y = 0L
+    for c in str:
+        x = x | (ord(c) << y)
+        y = y + 8
+    return x
+
+# ratio object that eventually will be able to reduce itself to lowest
+# common denominator for printing
+def gcd(a, b):
+    if b == 0:
+        return a
+    else:
+        return gcd(b, a % b)
+
+class Ratio:
+    def __init__(self, num, den):
+        self.num = num
+        self.den = den
+
+    def __repr__(self):
+        self.reduce()
+        if self.den == 1:
+            return str(self.num)
+        return '%d/%d' % (self.num, self.den)
+
+    def reduce(self):
+        div = gcd(self.num, self.den)
+        if div > 1:
+            self.num = self.num / div
+            self.den = self.den / div
+
+# for ease of dealing with tags
+class IFD_Tag:
+    def __init__(self, printable, tag, field_type, values, field_offset,
+                 field_length):
+        # printable version of data
+        self.printable = printable
+        # tag ID number
+        self.tag = tag
+        # field type as index into FIELD_TYPES
+        self.field_type = field_type
+        # offset of start of field in bytes from beginning of IFD
+        self.field_offset = field_offset
+        # length of data field in bytes
+        self.field_length = field_length
+        # either a string or array of data items
+        self.values = values
+
+    def __str__(self):
+        return self.printable
+
+    def __repr__(self):
+        return '(0x%04X) %s=%s @ %d' % (self.tag,
+                                        FIELD_TYPES[self.field_type][2],
+                                        self.printable,
+                                        self.field_offset)
+
+# class that handles an EXIF header
+class EXIF_header:
+    def __init__(self, file, endian, offset, fake_exif, debug=0):
+        self.file = file
+        self.endian = endian
+        self.offset = offset
+        self.fake_exif = fake_exif
+        self.debug = debug
+        self.tags = {}
+
+    # convert slice to integer, based on sign and endian flags
+    # usually this offset is assumed to be relative to the beginning of the
+    # start of the EXIF information.  For some cameras that use relative tags,
+    # this offset may be relative to some other starting point.
+    def s2n(self, offset, length, signed=0):
+        self.file.seek(self.offset+offset)
+        slice=self.file.read(length)
+        if self.endian == 'I':
+            val=s2n_intel(slice)
+        else:
+            val=s2n_motorola(slice)
+        # Sign extension ?
+        if signed:
+            msb=1L << (8*length-1)
+            if val & msb:
+                val=val-(msb << 1)
+        return val
+
+    # convert offset to string
+    def n2s(self, offset, length):
+        s = ''
+        for dummy in range(length):
+            if self.endian == 'I':
+                s = s + chr(offset & 0xFF)
+            else:
+                s = chr(offset & 0xFF) + s
+            offset = offset >> 8
+        return s
+
+    # return first IFD
+    def first_IFD(self):
+        return self.s2n(4, 4)
+
+    # return pointer to next IFD
+    def next_IFD(self, ifd):
+        entries=self.s2n(ifd, 2)
+        return self.s2n(ifd+2+12*entries, 4)
+
+    # return list of IFDs in header
+    def list_IFDs(self):
+        i=self.first_IFD()
+        a=[]
+        while i:
+            a.append(i)
+            i=self.next_IFD(i)
+        return a
+
+    # return list of entries in this IFD
+    def dump_IFD(self, ifd, ifd_name, dict=EXIF_TAGS, relative=0, stop_tag='UNDEF'):
+        entries=self.s2n(ifd, 2)
+        for i in range(entries):
+            # entry is index of start of this IFD in the file
+            entry = ifd + 2 + 12 * i
+            tag = self.s2n(entry, 2)
+
+            # get tag name early to avoid errors, help debug
+            tag_entry = dict.get(tag)
+            if tag_entry:
+                tag_name = tag_entry[0]
+            else:
+                tag_name = 'Tag 0x%04X' % tag
+
+            # ignore certain tags for faster processing
+            if not (not detailed and tag in IGNORE_TAGS):
+                field_type = self.s2n(entry + 2, 2)
+                if not 0 < field_type < len(FIELD_TYPES):
+                    # unknown field type
+                    raise ValueError('unknown type %d in tag 0x%04X' % (field_type, tag))
+                typelen = FIELD_TYPES[field_type][0]
+                count = self.s2n(entry + 4, 4)
+                offset = entry + 8
+                if count * typelen > 4:
+                    # offset is not the value; it's a pointer to the value
+                    # if relative we set things up so s2n will seek to the right
+                    # place when it adds self.offset.  Note that this 'relative'
+                    # is for the Nikon type 3 makernote.  Other cameras may use
+                    # other relative offsets, which would have to be computed here
+                    # slightly differently.
+                    if relative:
+                        tmp_offset = self.s2n(offset, 4)
+                        offset = tmp_offset + ifd - self.offset + 4
+                        if self.fake_exif:
+                            offset = offset + 18
+                    else:
+                        offset = self.s2n(offset, 4)
+                field_offset = offset
+                if field_type == 2:
+                    # special case: null-terminated ASCII string
+                    if count != 0:
+                        self.file.seek(self.offset + offset)
+                        values = self.file.read(count)
+                        values = values.strip().replace('\x00', '')
+                    else:
+                        values = ''
+                else:
+                    values = []
+                    signed = (field_type in [6, 8, 9, 10])
+                    for dummy in range(count):
+                        if field_type in (5, 10):
+                            # a ratio
+                            value = Ratio(self.s2n(offset, 4, signed),
+                                          self.s2n(offset + 4, 4, signed))
+                        else:
+                            value = self.s2n(offset, typelen, signed)
+                        values.append(value)
+                        offset = offset + typelen
+                # now "values" is either a string or an array
+                if count == 1 and field_type != 2:
+                    printable=str(values[0])
+                else:
+                    printable=str(values)
+                # compute printable version of values
+                if tag_entry:
+                    if len(tag_entry) != 1:
+                        # optional 2nd tag element is present
+                        if callable(tag_entry[1]):
+                            # call mapping function
+                            printable = tag_entry[1](values)
+                        else:
+                            printable = ''
+                            for i in values:
+                                # use lookup table for this tag
+                                printable += tag_entry[1].get(i, repr(i))
+
+                self.tags[ifd_name + ' ' + tag_name] = IFD_Tag(printable, tag,
+                                                          field_type,
+                                                          values, field_offset,
+                                                          count * typelen)
+                if self.debug:
+                    print ' debug:   %s: %s' % (tag_name,
+                                                repr(self.tags[ifd_name + ' ' + tag_name]))
+
+            if tag_name == stop_tag:
+                break
+
+    # extract uncompressed TIFF thumbnail (like pulling teeth)
+    # we take advantage of the pre-existing layout in the thumbnail IFD as
+    # much as possible
+    def extract_TIFF_thumbnail(self, thumb_ifd):
+        entries = self.s2n(thumb_ifd, 2)
+        # this is header plus offset to IFD ...
+        if self.endian == 'M':
+            tiff = 'MM\x00*\x00\x00\x00\x08'
+        else:
+            tiff = 'II*\x00\x08\x00\x00\x00'
+        # ... plus thumbnail IFD data plus a null "next IFD" pointer
+        self.file.seek(self.offset+thumb_ifd)
+        tiff += self.file.read(entries*12+2)+'\x00\x00\x00\x00'
+
+        # fix up large value offset pointers into data area
+        for i in range(entries):
+            entry = thumb_ifd + 2 + 12 * i
+            tag = self.s2n(entry, 2)
+            field_type = self.s2n(entry+2, 2)
+            typelen = FIELD_TYPES[field_type][0]
+            count = self.s2n(entry+4, 4)
+            oldoff = self.s2n(entry+8, 4)
+            # start of the 4-byte pointer area in entry
+            ptr = i * 12 + 18
+            # remember strip offsets location
+            if tag == 0x0111:
+                strip_off = ptr
+                strip_len = count * typelen
+            # is it in the data area?
+            if count * typelen > 4:
+                # update offset pointer (nasty "strings are immutable" crap)
+                # should be able to say "tiff[ptr:ptr+4]=newoff"
+                newoff = len(tiff)
+                tiff = tiff[:ptr] + self.n2s(newoff, 4) + tiff[ptr+4:]
+                # remember strip offsets location
+                if tag == 0x0111:
+                    strip_off = newoff
+                    strip_len = 4
+                # get original data and store it
+                self.file.seek(self.offset + oldoff)
+                tiff += self.file.read(count * typelen)
+
+        # add pixel strips and update strip offset info
+        old_offsets = self.tags['Thumbnail StripOffsets'].values
+        old_counts = self.tags['Thumbnail StripByteCounts'].values
+        for i in range(len(old_offsets)):
+            # update offset pointer (more nasty "strings are immutable" crap)
+            offset = self.n2s(len(tiff), strip_len)
+            tiff = tiff[:strip_off] + offset + tiff[strip_off + strip_len:]
+            strip_off += strip_len
+            # add pixel strip to end
+            self.file.seek(self.offset + old_offsets[i])
+            tiff += self.file.read(old_counts[i])
+
+        self.tags['TIFFThumbnail'] = tiff
+
+    # decode all the camera-specific MakerNote formats
+
+    # Note is the data that comprises this MakerNote.  The MakerNote will
+    # likely have pointers in it that point to other parts of the file.  We'll
+    # use self.offset as the starting point for most of those pointers, since
+    # they are relative to the beginning of the file.
+    #
+    # If the MakerNote is in a newer format, it may use relative addressing
+    # within the MakerNote.  In that case we'll use relative addresses for the
+    # pointers.
+    #
+    # As an aside: it's not just to be annoying that the manufacturers use
+    # relative offsets.  It's so that if the makernote has to be moved by the
+    # picture software all of the offsets don't have to be adjusted.  Overall,
+    # this is probably the right strategy for makernotes, though the spec is
+    # ambiguous.  (The spec does not appear to imagine that makernotes would
+    # follow EXIF format internally.  Once they did, it's ambiguous whether
+    # the offsets should be from the header at the start of all the EXIF info,
+    # or from the header at the start of the makernote.)
+    def decode_maker_note(self):
+        note = self.tags['EXIF MakerNote']
+        make = self.tags['Image Make'].printable
+        # model = self.tags['Image Model'].printable # unused
+
+        # Nikon
+        # The maker note usually starts with the word Nikon, followed by the
+        # type of the makernote (1 or 2, as a short).  If the word Nikon is
+        # not at the start of the makernote, it's probably type 2, since some
+        # cameras work that way.
+        if make in ('NIKON', 'NIKON CORPORATION'):
+            if note.values[0:7] == [78, 105, 107, 111, 110, 0, 1]:
+                if self.debug:
+                    print "Looks like a type 1 Nikon MakerNote."
+                self.dump_IFD(note.field_offset+8, 'MakerNote',
+                              dict=MAKERNOTE_NIKON_OLDER_TAGS)
+            elif note.values[0:7] == [78, 105, 107, 111, 110, 0, 2]:
+                if self.debug:
+                    print "Looks like a labeled type 2 Nikon MakerNote"
+                if note.values[12:14] != [0, 42] and note.values[12:14] != [42L, 0L]:
+                    raise ValueError("Missing marker tag '42' in MakerNote.")
+                # skip the Makernote label and the TIFF header
+                self.dump_IFD(note.field_offset+10+8, 'MakerNote',
+                              dict=MAKERNOTE_NIKON_NEWER_TAGS, relative=1)
+            else:
+                # E99x or D1
+                if self.debug:
+                    print "Looks like an unlabeled type 2 Nikon MakerNote"
+                self.dump_IFD(note.field_offset, 'MakerNote',
+                              dict=MAKERNOTE_NIKON_NEWER_TAGS)
+            return
+
+        # Olympus
+        if make.startswith('OLYMPUS'):
+            self.dump_IFD(note.field_offset+8, 'MakerNote',
+                          dict=MAKERNOTE_OLYMPUS_TAGS)
+            # TODO
+            #for i in (('MakerNote Tag 0x2020', MAKERNOTE_OLYMPUS_TAG_0x2020),):
+            #    self.decode_olympus_tag(self.tags[i[0]].values, i[1])
+            #return
+
+        # Casio
+        if make == 'Casio':
+            self.dump_IFD(note.field_offset, 'MakerNote',
+                          dict=MAKERNOTE_CASIO_TAGS)
+            return
+
+        # Fujifilm
+        if make == 'FUJIFILM':
+            # bug: everything else is "Motorola" endian, but the MakerNote
+            # is "Intel" endian
+            endian = self.endian
+            self.endian = 'I'
+            # bug: IFD offsets are from beginning of MakerNote, not
+            # beginning of file header
+            offset = self.offset
+            self.offset += note.field_offset
+            # process note with bogus values (note is actually at offset 12)
+            self.dump_IFD(12, 'MakerNote', dict=MAKERNOTE_FUJIFILM_TAGS)
+            # reset to correct values
+            self.endian = endian
+            self.offset = offset
+            return
+
+        # Canon
+        if make == 'Canon':
+            self.dump_IFD(note.field_offset, 'MakerNote',
+                          dict=MAKERNOTE_CANON_TAGS)
+            for i in (('MakerNote Tag 0x0001', MAKERNOTE_CANON_TAG_0x001),
+                      ('MakerNote Tag 0x0004', MAKERNOTE_CANON_TAG_0x004)):
+                self.canon_decode_tag(self.tags[i[0]].values, i[1])
+            return
+
+    # decode Olympus MakerNote tag based on offset within tag
+    def olympus_decode_tag(self, value, dict):
+        pass
+
+    # decode Canon MakerNote tag based on offset within tag
+    # see http://www.burren.cx/david/canon.html by David Burren
+    def canon_decode_tag(self, value, dict):
+        for i in range(1, len(value)):
+            x=dict.get(i, ('Unknown', ))
+            if self.debug:
+                print i, x
+            name=x[0]
+            if len(x) > 1:
+                val=x[1].get(value[i], 'Unknown')
+            else:
+                val=value[i]
+            # it's not a real IFD Tag but we fake one to make everybody
+            # happy. this will have a "proprietary" type
+            self.tags['MakerNote '+name]=IFD_Tag(str(val), None, 0, None,
+                                                 None, None)
+
+# process an image file (expects an open file object)
+# this is the function that has to deal with all the arbitrary nasty bits
+# of the EXIF standard
+def process_file(f, stop_tag='UNDEF', details=True, debug=False):
+    # yah it's cheesy...
+    global detailed
+    detailed = details
+
+    # by default do not fake an EXIF beginning
+    fake_exif = 0
+
+    # determine whether it's a JPEG or TIFF
+    data = f.read(12)
+    if data[0:4] in ['II*\x00', 'MM\x00*']:
+        # it's a TIFF file
+        f.seek(0)
+        endian = f.read(1)
+        f.read(1)
+        offset = 0
+    elif data[0:2] == '\xFF\xD8':
+        # it's a JPEG file
+        while data[2] == '\xFF' and data[6:10] in ('JFIF', 'JFXX', 'OLYM', 'Phot'):
+            length = ord(data[4])*256+ord(data[5])
+            f.read(length-8)
+            # fake an EXIF beginning of file
+            data = '\xFF\x00'+f.read(10)
+            fake_exif = 1
+        if data[2] == '\xFF' and data[6:10] == 'Exif':
+            # detected EXIF header
+            offset = f.tell()
+            endian = f.read(1)
+        else:
+            # no EXIF information
+            return {}
+    else:
+        # file format not recognized
+        return {}
+
+    # deal with the EXIF info we found
+    if debug:
+        print {'I': 'Intel', 'M': 'Motorola'}[endian], 'format'
+    hdr = EXIF_header(f, endian, offset, fake_exif, debug)
+    ifd_list = hdr.list_IFDs()
+    ctr = 0
+    for i in ifd_list:
+        if ctr == 0:
+            IFD_name = 'Image'
+        elif ctr == 1:
+            IFD_name = 'Thumbnail'
+            thumb_ifd = i
+        else:
+            IFD_name = 'IFD %d' % ctr
+        if debug:
+            print ' IFD %d (%s) at offset %d:' % (ctr, IFD_name, i)
+        hdr.dump_IFD(i, IFD_name, stop_tag=stop_tag)
+        # EXIF IFD
+        exif_off = hdr.tags.get(IFD_name+' ExifOffset')
+        if exif_off:
+            if debug:
+                print ' EXIF SubIFD at offset %d:' % exif_off.values[0]
+            hdr.dump_IFD(exif_off.values[0], 'EXIF', stop_tag=stop_tag)
+            # Interoperability IFD contained in EXIF IFD
+            intr_off = hdr.tags.get('EXIF SubIFD InteroperabilityOffset')
+            if intr_off:
+                if debug:
+                    print ' EXIF Interoperability SubSubIFD at offset %d:' \
+                          % intr_off.values[0]
+                hdr.dump_IFD(intr_off.values[0], 'EXIF Interoperability',
+                             dict=INTR_TAGS, stop_tag=stop_tag)
+        # GPS IFD
+        gps_off = hdr.tags.get(IFD_name+' GPSInfo')
+        if gps_off:
+            if debug:
+                print ' GPS SubIFD at offset %d:' % gps_off.values[0]
+            hdr.dump_IFD(gps_off.values[0], 'GPS', dict=GPS_TAGS, stop_tag=stop_tag)
+        ctr += 1
+
+    # extract uncompressed TIFF thumbnail
+    thumb = hdr.tags.get('Thumbnail Compression')
+    if thumb and thumb.printable == 'Uncompressed TIFF':
+        hdr.extract_TIFF_thumbnail(thumb_ifd)
+
+    # JPEG thumbnail (thankfully the JPEG data is stored as a unit)
+    thumb_off = hdr.tags.get('Thumbnail JPEGInterchangeFormat')
+    if thumb_off:
+        f.seek(offset+thumb_off.values[0])
+        size = hdr.tags['Thumbnail JPEGInterchangeFormatLength'].values[0]
+        hdr.tags['JPEGThumbnail'] = f.read(size)
+
+    # deal with MakerNote contained in EXIF IFD
+    if 'EXIF MakerNote' in hdr.tags and detailed:
+        hdr.decode_maker_note()
+
+    # Sometimes in a TIFF file, a JPEG thumbnail is hidden in the MakerNote
+    # since it's not allowed in a uncompressed TIFF IFD
+    if 'JPEGThumbnail' not in hdr.tags:
+        thumb_off=hdr.tags.get('MakerNote JPEGThumbnail')
+        if thumb_off:
+            f.seek(offset+thumb_off.values[0])
+            hdr.tags['JPEGThumbnail']=file.read(thumb_off.field_length)
+
+    return hdr.tags
+
+
+# show command line usage
+def usage(exit_status):
+    msg = 'Usage: EXIF.py [OPTIONS] file1 [file2 ...]\n'
+    msg += 'Extract EXIF information from digital camera image files.\n\nOptions:\n'
+    msg += '-q --quick   Do not process MakerNotes.\n'
+    msg += '-t TAG --stop-tag TAG   Stop processing when this tag is retrieved.\n'
+    msg += '-d --debug   Run in debug mode.\n'
+    print msg
+    sys.exit(exit_status)
+
+# library test/debug function (dump given files)
+if __name__ == '__main__':
+    import sys
+    import getopt
+
+    # parse command line options/arguments
+    try:
+        opts, args = getopt.getopt(sys.argv[1:], "hqdt:v", ["help", "quick", "debug", "stop-tag="])
+    except getopt.GetoptError:
+        usage(2)
+    if args == []:
+        usage(2)
+    detailed = True
+    stop_tag = 'UNDEF'
+    debug = False
+    for o, a in opts:
+        if o in ("-h", "--help"):
+            usage(0)
+        if o in ("-q", "--quick"):
+            detailed = False
+        if o in ("-t", "--stop-tag"):
+            stop_tag = a
+        if o in ("-d", "--debug"):
+            debug = True
+
+    # output info for each file
+    for filename in args:
+        try:
+            file=open(filename, 'rb')
+        except:
+            print "'%s' is unreadable\n"%filename
+            continue
+        print filename + ':'
+        # get the tags
+        data = process_file(file, stop_tag=stop_tag, details=detailed, debug=debug)
+        if not data:
+            print 'No EXIF information found'
+            continue
+
+        x=data.keys()
+        x.sort()
+        for i in x:
+            if i in ('JPEGThumbnail', 'TIFFThumbnail'):
+                continue
+            try:
+                print '   %s (%s): %s' % \
+                      (i, FIELD_TYPES[data[i].field_type][2], data[i].printable)
+            except:
+                print 'error', i, '"', data[i], '"'
+        if 'JPEGThumbnail' in data:
+            print 'File has JPEG thumbnail'
+        print
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/photologue/utils/reflection.py	Sat Apr 14 16:40:29 2012 -0500
@@ -0,0 +1,93 @@
+""" Function for generating web 2.0 style image reflection effects.
+
+Copyright (c) 2007, Justin C. Driscoll
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+    1. Redistributions of source code must retain the above copyright notice,
+       this list of conditions and the following disclaimer.
+
+    2. Redistributions in binary form must reproduce the above copyright
+       notice, this list of conditions and the following disclaimer in the
+       documentation and/or other materials provided with the distribution.
+
+    3. Neither the name of reflection.py nor the names of its contributors may be used
+       to endorse or promote products derived from this software without
+       specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+"""
+
+try:
+    import Image
+    import ImageColor                    
+except ImportError:
+    try:
+        from PIL import Image
+        from PIL import ImageColor
+    except ImportError:
+        raise ImportError("The Python Imaging Library was not found.")
+    
+    
+def add_reflection(im, bgcolor="#00000", amount=0.4, opacity=0.6):
+    """ Returns the supplied PIL Image (im) with a reflection effect
+    
+    bgcolor  The background color of the reflection gradient
+    amount   The height of the reflection as a percentage of the orignal image
+    opacity  The initial opacity of the reflection gradient
+    
+    Originally written for the Photologue image management system for Django
+    and Based on the original concept by Bernd Schlapsi
+    
+    """
+    # convert bgcolor string to rgb value
+    background_color = ImageColor.getrgb(bgcolor)
+    
+    # copy orignial image and flip the orientation
+    reflection = im.copy().transpose(Image.FLIP_TOP_BOTTOM)
+    
+    # create a new image filled with the bgcolor the same size
+    background = Image.new("RGB", im.size, background_color)
+    
+    # calculate our alpha mask
+    start = int(255 - (255 * opacity)) # The start of our gradient
+    steps = int(255 * amount) # the number of intermedite values
+    increment = (255 - start) / float(steps)
+    mask = Image.new('L', (1, 255))
+    for y in range(255):
+        if y < steps:
+            val = int(y * increment + start)
+        else:
+            val = 255
+        mask.putpixel((0, y), val)
+    alpha_mask = mask.resize(im.size)    
+    
+    # merge the reflection onto our background color using the alpha mask
+    reflection = Image.composite(background, reflection, alpha_mask)
+    
+    # crop the reflection
+    reflection_height = int(im.size[1] * amount)
+    reflection = reflection.crop((0, 0, im.size[0], reflection_height))
+    
+    # create new image sized to hold both the original image and the reflection
+    composite = Image.new("RGB", (im.size[0], im.size[1]+reflection_height), background_color)
+    
+    # paste the orignal image and the reflection into the composite image
+    composite.paste(im, (0, 0))
+    composite.paste(reflection, (0, im.size[1]))
+    
+    # return the image complete with reflection effect
+    return composite
+            
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/photologue/utils/watermark.py	Sat Apr 14 16:40:29 2012 -0500
@@ -0,0 +1,64 @@
+""" Function for applying watermarks to images.
+
+Original found here:
+http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/362879
+
+"""
+
+try:
+    import Image
+    import ImageEnhance
+except ImportError:
+    try:
+        from PIL import Image
+        from PIL import ImageEnhance
+    except ImportError:
+        raise ImportError("The Python Imaging Library was not found.")
+
+def reduce_opacity(im, opacity):
+    """Returns an image with reduced opacity."""
+    assert opacity >= 0 and opacity <= 1
+    if im.mode != 'RGBA':
+        im = im.convert('RGBA')
+    else:
+        im = im.copy()
+    alpha = im.split()[3]
+    alpha = ImageEnhance.Brightness(alpha).enhance(opacity)
+    im.putalpha(alpha)
+    return im
+
+def apply_watermark(im, mark, position, opacity=1):
+    """Adds a watermark to an image."""
+    if opacity < 1:
+        mark = reduce_opacity(mark, opacity)
+    if im.mode != 'RGBA':
+        im = im.convert('RGBA')
+    # create a transparent layer the size of the image and draw the
+    # watermark in that layer.
+    layer = Image.new('RGBA', im.size, (0,0,0,0))
+    if position == 'tile':
+        for y in range(0, im.size[1], mark.size[1]):
+            for x in range(0, im.size[0], mark.size[0]):
+                layer.paste(mark, (x, y))
+    elif position == 'scale':
+        # scale, but preserve the aspect ratio
+        ratio = min(
+            float(im.size[0]) / mark.size[0], float(im.size[1]) / mark.size[1])
+        w = int(mark.size[0] * ratio)
+        h = int(mark.size[1] * ratio)
+        mark = mark.resize((w, h))
+        layer.paste(mark, ((im.size[0] - w) / 2, (im.size[1] - h) / 2))
+    else:
+        layer.paste(mark, position)
+    # composite the watermark with the layer
+    return Image.composite(layer, im, layer)
+
+def test():
+    im = Image.open('test.png')
+    mark = Image.open('overlay.png')
+    watermark(im, mark, 'tile', 0.5).show()
+    watermark(im, mark, 'scale', 1.0).show()
+    watermark(im, mark, (100, 100), 0.5).show()
+
+if __name__ == '__main__':
+    test()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pl-admin.py	Sat Apr 14 16:40:29 2012 -0500
@@ -0,0 +1,116 @@
+import getopt, sys
+
+try:
+    import settings # Assumed to be in the same directory.
+    from django.core.management import setup_environ
+except ImportError:
+    import sys
+    sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n(If the file settings.py does indeed exist, it's causing an ImportError somehow.)\n" % __file__)
+    sys.exit(1)
+
+
+def precache(sizes=[], reset=False):
+    # setup django environment
+    setup_environ(settings)
+
+    # import models
+    from photologue.models import Photo, PhotoSize, PhotoSizeCache
+
+    cache = PhotoSizeCache()
+
+    print 'Caching photos, this may take a while...'
+
+    for photo in Photo.objects.all():
+        if len(sizes):
+            for size in sizes:
+                photosize = cache.sizes.get(size, None)
+                if photosize is None:
+                    print '\nA photosize named "%s" was not found...' % size
+                else:
+                    if reset:
+                        photo.remove_size(photosize)
+                    photo.create_size(photosize)
+        else:
+            for size in caches.sizes.values():
+                if reset:
+                    Photo.remove_size(photosize)
+                photo.create_size(photosize)
+
+    print ' Complete.'
+    sys.exit(2)
+
+
+def reset():
+    # setup django environment
+    setup_environ(settings)
+
+    # import models
+    from photologue.models import Photo, PhotoSize
+
+    print 'Reseting photo cache, this may take a while...'
+
+    for photo in Photo.objects.all():
+        photo.clear_cache()
+
+    print ' Complete.'
+    sys.exit(2)
+
+
+def usage():
+    print """
+
+pl-admin.py - Photologue administration script.
+
+Available Commands:
+ pl-admin.py create Resizes and caches all defined photo sizes for each image.
+ pl-admin.py reset  Removes all cached images.
+
+Options:
+ --reset (-r) If calling create the script will clear the existing photo cache
+              before regenerating the specified size (or sizes)
+ --size (-s)  The name of a photosize generate
+
+Usage:
+ pl-admin.py [options] command
+
+Examples:
+ pl-admin.py -r -s=thumbnail create
+ pl-admin.py -s=thumbnail -s=display create
+ pl-admin.py reset
+
+"""
+
+def main():
+    try:
+        opts, args = getopt.getopt(sys.argv[1:], "hrs:",
+                                   ["help", "reset", "sizes="])
+    except getopt.GetoptError, err:
+        print str(err)
+        usage()
+        sys.exit(2)
+    r = False
+    s = []
+    for o, a in opts:
+        if o in ("-h", "--help"):
+            usage()
+            sys.exit(2)
+        if o in ("-r", "--reset"):
+            r = True
+        elif o in ("-s", "--sizes"):
+            s.append(a.strip('='))
+        else:
+            usage()
+            sys.exit(2)
+
+    if len(args) == 1:
+        command = args[0]
+        if command == 'create':
+            precache(s, r)
+        elif command == 'reset':
+            reset()
+
+    usage()
+
+
+if __name__ == '__main__':
+    main()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/videos/admin.py	Sat Apr 14 16:40:29 2012 -0500
@@ -0,0 +1,29 @@
+"""
+Automatic admin definitions for the models in the videos application.
+
+"""
+from django.contrib import admin
+from django.conf import settings
+
+from videos.models import Collection, Video
+
+
+class VideoInline(admin.StackedInline):
+   model = Video
+
+
+class CollectionAdmin(admin.ModelAdmin):
+    list_filter = ['date_added']
+    list_display = ['title', 'date_added']
+    inlines = [VideoInline]
+
+    class Media:
+        js = ['js/videos/videos_admin.js'] + settings.THIRD_PARTY_JS['tiny_mce']
+
+
+class VideoAdmin(admin.ModelAdmin):
+    list_display = ['title', 'collection']
+
+
+admin.site.register(Collection, CollectionAdmin)
+admin.site.register(Video, VideoAdmin)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/videos/management/commands/import_old_videos.py	Sat Apr 14 16:40:29 2012 -0500
@@ -0,0 +1,69 @@
+"""
+import_old_videos.py - For importing video data from the older version of this
+website.
+
+"""
+import datetime
+
+from django.core.management.base import LabelCommand
+from django.utils import simplejson as json
+from django.utils.html import linebreaks
+
+from videos.models import Collection, Video
+
+
+class Command(LabelCommand):
+    args = '<filename filename ...>'
+    help = 'Imports older video & video sets in JSON format'
+
+    collections = {}
+
+    def handle_label(self, filename, **options):
+        """
+        Process the file of older video & video sets in JSON. Convert to the new
+        model scheme.
+
+        """
+        with open(filename, 'rb') as f:
+            items = json.load(f)
+
+        for item in items:
+            if item['model'] == 'band.video_set':
+                self.process_set(item)
+
+        for item in items:
+            if item['model'] == 'band.video':
+                self.process_video(item)
+
+    def process_set(self, item):
+
+        fields = item['fields']
+
+        description = linebreaks(fields['text'].strip())
+
+        # there are several sets with the same date, so to get the ordering
+        # right, add the pk as seconds.
+
+        date_added = datetime.datetime.strptime(fields['date'], '%Y-%m-%d')
+        date_added += datetime.timedelta(seconds=int(item['pk']))
+
+        coll = Collection(
+                id=item['pk'],
+                title=fields['title'].strip(),
+                date_added=date_added,
+                description=description)
+
+        coll.save()
+        self.collections[coll.pk] = coll
+
+    def process_video(self, item):
+
+        fields = item['fields']
+
+        video = Video(
+                id=item['pk'],
+                title=fields['title'].strip(),
+                embed_code=fields['embed_code'],
+                collection=self.collections[fields['video_set']],
+                )
+        video.save()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/videos/models.py	Sat Apr 14 16:40:29 2012 -0500
@@ -0,0 +1,42 @@
+"""
+Models for the videos application.
+
+"""
+from django.db import models
+
+
+class Collection(models.Model):
+    """
+    This model represents a collection of videos.
+
+    """
+    title = models.CharField(max_length=64)
+    description = models.TextField()
+    date_added = models.DateTimeField()
+
+    class Meta:
+        ordering = ['-date_added']
+
+    def __unicode__(self):
+        return self.title
+
+    @models.permalink
+    def get_absolute_url(self):
+        return ('videos-item', [], {'pk': str(self.id)})
+
+
+class Video(models.Model):
+    """
+    This model represents a video clip hosted on a remote video sharing site
+    (e.g. YouTube).
+
+    """
+    title = models.CharField(max_length=64)
+    embed_code = models.TextField()
+    collection = models.ForeignKey(Collection)
+
+    class Meta:
+        ordering = ['title']
+
+    def __unicode__(self):
+        return self.title
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/videos/static/js/videos/videos_admin.js	Sat Apr 14 16:40:29 2012 -0500
@@ -0,0 +1,3 @@
+django.jQuery(document).ready(function() {
+   django.jQuery('.embed_code textarea').addClass('mceNoEditor');      
+});
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/videos/urls.py	Sat Apr 14 16:40:29 2012 -0500
@@ -0,0 +1,20 @@
+"""
+Urls for the videos application.
+
+"""
+from django.conf.urls import patterns, url
+from django.views.generic import DetailView, ListView
+
+from videos.models import Collection
+
+
+urlpatterns = patterns('',
+   url(r'^$',
+       ListView.as_view(
+           model=Collection,
+           context_object_name='collection_list'),
+       name='videos-index'),
+   url(r'^(?P<pk>\d+)/$',
+       DetailView.as_view(model=Collection, context_object_name='collection'),
+       name='videos-item')
+)