# HG changeset patch # User Brian Neal # Date 1329268516 21600 # Node ID 63e4211628e179d6ac2a892e44b785b592310a33 # Parent 25e00d1b99bf9e79700658e7b857883eb5482a54 Rename the mysite directory to madeira. diff -r 25e00d1b99bf -r 63e4211628e1 madeira/apache/madeira.wsgi --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/madeira/apache/madeira.wsgi Tue Feb 14 19:15:16 2012 -0600 @@ -0,0 +1,37 @@ +import os +import sys + +OFFLINE = False + +sys.path.append('/home/var/django-sites/madeira/django-trunk') +sys.path.append('/home/var/django-sites/madeira/madeira-trunk') +sys.path.append('/home/var/django-sites/madeira/madeira-trunk/mysite') +os.environ['PYTHON_EGG_CACHE'] = '/home/var/django-sites/madeira/eggs/' + + +def offline_handler(environ, start_response): + wsgi_dir = os.path.dirname(__file__) + sys.path.append(wsgi_dir) + + offline_file = os.path.join(wsgi_dir, '..', 'templates', 'offline.html') + if os.path.exists(offline_file): + response_headers = [('Content-type','text/html')] + response = open(offline_file).read() + else: + response_headers = [('Content-type','text/plain')] + response = 'themadeira.net website maintenance in progress; please check back soon.' + + if environ['REQUEST_METHOD'] == 'GET': + status = '503 Service Unavailable' + else: + status = '405 Method Not Allowed' + start_response(status, response_headers) + return [response] + + +if not OFFLINE: + os.environ['DJANGO_SETTINGS_MODULE'] = 'mysite.settings.production' + import django.core.handlers.wsgi + application = django.core.handlers.wsgi.WSGIHandler() +else: + application = offline_handler diff -r 25e00d1b99bf -r 63e4211628e1 madeira/band/admin.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/madeira/band/admin.py Tue Feb 14 19:15:16 2012 -0600 @@ -0,0 +1,244 @@ +####################################################################### +# +# PyBand Copyright (C) 2008 by Brian Neal +# +####################################################################### + +from django.contrib import admin + +from band.models import Article +from band.models import Album +from band.models import Album_Merchant +from band.models import Album_Track +from band.models import Band +from band.models import City +from band.models import Country +from band.models import Fan +from band.models import Gear +from band.models import Gig +from band.models import Label_Release +from band.models import Member +from band.models import Merchandise +from band.models import Mp3 +from band.models import Mp3_Set +from band.models import News +from band.models import Record_Label +from band.models import SiteConfig +from band.models import State +from band.models import Venue +from band.models import Video +from band.models import Video_Set + +####################################################################### + +admin.site.register(Video) + +####################################################################### + +class SiteConfigAdmin(admin.ModelAdmin): + list_display = ('band_name', 'url', 'contact_email') + fieldsets = ( + (None, { 'fields' : ('band_name', 'url', 'contact_email', 'intro_text', 'ordering_info', + 'intro_photo') }), + ) + +admin.site.register(SiteConfig, SiteConfigAdmin) + +####################################################################### + +class GearInline(admin.TabularInline): + model = Gear + +class GearAdmin(admin.ModelAdmin): + list_display = ('item', 'member') + list_filter = ('member', ) + +admin.site.register(Gear, GearAdmin) + +####################################################################### + +class MemberAdmin(admin.ModelAdmin): + list_display = ('name', 'instrument', 'is_active') + inlines = [ + GearInline, + ] + +admin.site.register(Member, MemberAdmin) + +####################################################################### + +class CityInline(admin.TabularInline): + model = City + +class CityAdmin(admin.ModelAdmin): + list_display = ('name', 'state', 'country') + list_filter = ('state', ) + search_fields = ('name', ) + +admin.site.register(City, CityAdmin) + +####################################################################### + +admin.site.register(Country) + +####################################################################### + +class StateAdmin(admin.ModelAdmin): + inlines = [ + CityInline, + ] + +admin.site.register(State, StateAdmin) + +####################################################################### + +class VenueAdmin(admin.ModelAdmin): + list_filter = ('city', ) + list_display = ('name', 'city', ) + search_fields = ('name', ) + +admin.site.register(Venue, VenueAdmin) + +####################################################################### + +class BandAdmin(admin.ModelAdmin): + search_fields = ('name', ) + +admin.site.register(Band, BandAdmin) + +####################################################################### + +class GigAdmin(admin.ModelAdmin): + list_filter = ('date', 'venue') + save_on_top = True + filter_horizontal = ('bands', ) + +admin.site.register(Gig, GigAdmin) + +####################################################################### + +class NewsAdmin(admin.ModelAdmin): + save_on_top = True + list_filter = ('date', ) + list_display = ('date', 'title') + search_fields = ('text', 'title') + +admin.site.register(News, NewsAdmin) + +####################################################################### + +class ArticleAdmin(admin.ModelAdmin): + save_on_top = True + list_filter = ('date', ) + list_display = ('title', 'date') + search_fields = ('text', 'title') + +admin.site.register(Article, ArticleAdmin) + +####################################################################### + +class Mp3Inline(admin.TabularInline): + model = Mp3 + +class Mp3Admin(admin.ModelAdmin): + prepopulated_fields = {'slug' : ('title', 'desc')} + +admin.site.register(Mp3, Mp3Admin) + +####################################################################### + +class Mp3_SetAdmin(admin.ModelAdmin): + list_filter = ('date', ) + list_display = ('title', 'date') + inlines = [ + Mp3Inline, + ] + +admin.site.register(Mp3_Set, Mp3_SetAdmin) + +####################################################################### + +class VideoInline(admin.TabularInline): + model = Video + +class Video_SetAdmin(admin.ModelAdmin): + list_filter = ('date', ) + list_display = ('title', 'date') + inlines = [ + VideoInline, + ] + +admin.site.register(Video_Set, Video_SetAdmin) + +####################################################################### + +class Album_TrackInline(admin.TabularInline): + model = Album_Track + +class Album_TrackAdmin(admin.ModelAdmin): + list_display = ('track_name', 'album') + list_filter = ('album', ) + +admin.site.register(Album_Track, Album_TrackAdmin) + +####################################################################### + +class Label_ReleaseInline(admin.TabularInline): + model = Label_Release + +class Label_ReleaseAdmin(admin.ModelAdmin): + list_display = ('catalog_number', 'album', 'record_label', 'release_date') + list_filter = ('record_label', 'album') + +admin.site.register(Label_Release, Label_ReleaseAdmin) + +####################################################################### + +class Record_LabelAdmin(admin.ModelAdmin): + inlines = [ + Label_ReleaseInline, + ] + +admin.site.register(Record_Label, Record_LabelAdmin) + +####################################################################### + +class Album_MerchantInline(admin.TabularInline): + model = Album_Merchant + +class Album_MerchantAdmin(admin.ModelAdmin): + list_display = ('name', 'album') + list_filter = ('album', ) + +admin.site.register(Album_Merchant, Album_MerchantAdmin) + +####################################################################### + +class AlbumAdmin(admin.ModelAdmin): + save_on_top = True + inlines = [ + Album_TrackInline, + Label_ReleaseInline, + Album_MerchantInline, + ] + +admin.site.register(Album, AlbumAdmin) + +####################################################################### + +class MerchandiseAdmin(admin.ModelAdmin): + list_display = ('name', 'price', 'in_stock') + list_filter = ('in_stock', ) + +admin.site.register(Merchandise, MerchandiseAdmin) + +####################################################################### + +class FanAdmin(admin.ModelAdmin): + list_display = ('name', 'email', 'current_status') + search_fields = ('name', 'email') + +admin.site.register(Fan, FanAdmin) + +####################################################################### + diff -r 25e00d1b99bf -r 63e4211628e1 madeira/band/admin_views.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/madeira/band/admin_views.py Tue Feb 14 19:15:16 2012 -0600 @@ -0,0 +1,77 @@ +####################################################################### +# +# PyBand Copyright (C) 2008 by Brian Neal +# +####################################################################### +from django import forms +from django.core.urlresolvers import reverse +from django.core.mail import EmailMessage +from django.template import RequestContext +from django.http import HttpResponseRedirect +from django.shortcuts import render_to_response +from django.contrib.admin.views.decorators import staff_member_required + +from band.models import SiteConfig +from band.models import Fan + +####################################################################### + +unsubscribeText = ''' + +---- +You are receiving this message because you are subscribed to our mailing list. +If you would like to unsubscribe please visit %s. +''' + +####################################################################### + +class EmailForm(forms.Form): + subject = forms.CharField(max_length = 255, required = True, label = 'Subject:', + widget = forms.TextInput(attrs = {'class' : 'vTextField required', 'size' : '60'})) + message = forms.CharField(label = 'Message:', + widget = forms.Textarea(attrs = {'class' : 'vLargeTextField required'})) + +####################################################################### + +def email_sent(request): + return render_to_response('admin/band/email_sent.html', + {}, + context_instance = RequestContext(request)) + +####################################################################### + +def email(request): + + config = SiteConfig.objects.get(pk = 1) + bandTag = '[%s] ' % (config.band_name, ) + + if request.method == 'POST': + form = EmailForm(request.POST) + if form.is_valid(): + subject = form.cleaned_data['subject'] + message = form.cleaned_data['message'] + + unsubscribeUrl = config.url + if unsubscribeUrl[-1] != '/': + unsubscribeUrl += '/' + unsubscribeUrl += 'mail' + + footer = unsubscribeText % (unsubscribeUrl, ) + message += footer + + fans = Fan.objects.all() + bcc = [fan.email for fan in fans] + + email = EmailMessage(subject, message, config.contact_email, + [config.contact_email], bcc) + email.send() + return HttpResponseRedirect(reverse(email_sent)) + + else: + form = EmailForm(initial = { 'subject' : bandTag }) + + return render_to_response('admin/band/email.html', + { 'form' : form }, + context_instance = RequestContext(request)) + +email = staff_member_required(email) diff -r 25e00d1b99bf -r 63e4211628e1 madeira/band/models.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/madeira/band/models.py Tue Feb 14 19:15:16 2012 -0600 @@ -0,0 +1,393 @@ +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 +import datetime +import random +import string + +####################################################################### + +class SiteConfig(models.Model): + band_name = models.CharField(max_length = 50) + url = models.URLField(verify_exists = False, max_length = 200) + contact_email = models.EmailField() + ordering_info = models.TextField(help_text = 'Enter instructions on how to order merchandise here') + intro_text = models.TextField(help_text = 'This text appears on the home page.') + intro_photo = models.ForeignKey(Photo) + + def __unicode__(self): + return self.band_name + + class Meta: + verbose_name = "Site Configuration" + verbose_name_plural = "Site Configuration" + +####################################################################### + +class Member(models.Model): + name = models.CharField(max_length = 50, db_index = True) + nickname = models.CharField(max_length = 50, blank = True) + instrument = models.CharField(max_length = 255) + bio = models.TextField(blank = True) + photo = models.FileField(upload_to = 'images/bio/', blank = True) + order = models.SmallIntegerField(help_text = '''Controls order of display on the bio page, lower numbers displayed + first''') + is_active = models.BooleanField(db_index = True) + start_date = models.DateField() + end_date = models.DateField(blank = True, help_text = 'Only used if the member is not active', + default = datetime.date(1985, 1, 1)) + 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 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 self.name + u', ' + self.state.abbrev + return self.name + +####################################################################### + +class Venue(models.Model): + name = models.CharField(max_length = 50, db_index = True) + url = models.URLField(verify_exists = False, 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(verify_exists = False, 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(verify_exists = False, 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) + + 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 u'' + self.date.strftime('%m/%d/%Y') + + class Meta: + ordering = ('-date', 'time') + +####################################################################### + +class News(models.Model): + title = models.CharField(max_length = 64, blank = True) + date = models.DateField(db_index = True) + author = models.CharField(max_length = 50, blank = True) + text = models.TextField() + markup_enabled = models.BooleanField(default = True, + help_text = 'Check this box to allow Textile style markup in the text field') + photo = models.FileField(upload_to = 'images/news/%Y/%m/%d/', blank = True) + photo_caption = models.CharField(max_length = 50, blank = True) + + def __unicode__(self): + return u'%s %s' % (self.date.strftime('%m/%d/%Y'), self.title) + + class Meta: + ordering = ('-date', ) + verbose_name_plural = "News" + +####################################################################### + +class Article(models.Model): + title = models.CharField(max_length = 64) + date = models.DateField(db_index = True) + text = models.TextField() + markup_enabled = models.BooleanField(default = True, + help_text = 'Check this box to allow Textile style markup in the text field') + 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', ) + +####################################################################### + +class Mp3_Set(models.Model): + date = models.DateField(auto_now_add = True, editable = False) + title = models.CharField(max_length = 64) + text = models.TextField() + + def __unicode__(self): + return self.title + + class Meta: + ordering = ('date', ) + verbose_name = "MP3 Set" + +####################################################################### + +class Mp3(models.Model): + mp3_set = models.ForeignKey(Mp3_Set) + title = models.CharField(max_length = 64) + desc = models.CharField(max_length = 128, blank = True) + file = models.FileField(upload_to = 'mp3s/%Y/%m/%d/') + slug = models.SlugField(unique = True) + + def __unicode__(self): + return self.title + + class Meta: + ordering = ('title', ) + verbose_name = "MP3" + +####################################################################### + +class Video_Set(models.Model): + date = models.DateField(blank=True) + title = models.CharField(max_length = 64) + text = models.TextField() + + def __unicode__(self): + return self.title + + class Meta: + ordering = ('date', ) + verbose_name = "Video Set" + + def save(self, *args, **kwargs): + if not self.id: + self.date = datetime.date.today() + + super(Video_Set, self).save(*args, **kwargs) + +####################################################################### + +class Video(models.Model): + video_set = models.ForeignKey(Video_Set) + title = models.CharField(max_length = 64) + embed_code = models.CharField(max_length = 1024) + + def __unicode__(self): + return self.title + + class Meta: + ordering = ('title', ) + +####################################################################### + +class Record_Label(models.Model): + name = models.CharField(max_length = 64) + url = models.URLField(verify_exists = False, max_length = 200) + + def __unicode__(self): + return self.name + + class Meta: + verbose_name = 'Record Label' + +####################################################################### + +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 Meta: + pass + +####################################################################### + +class Album_Track(models.Model): + album = models.ForeignKey(Album) + 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 Label_Release(models.Model): + record_label = models.ForeignKey(Record_Label) + album = models.ForeignKey(Album) + 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 Album_Merchant(models.Model): + album = models.ForeignKey(Album) + name = models.CharField(max_length = 64) + url = models.URLField(verify_exists = False, 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" + +####################################################################### + +class Fan(models.Model): + statusCodes = (('P', 'Pending'), ('A', 'Active'), ('L', 'Leaving')) + keyLength = 16 + + name = models.CharField(max_length = 32, blank = True) + email = models.EmailField(db_index = True) + location = models.CharField(max_length = 64, blank = True) + status = models.CharField(max_length = 1, choices = statusCodes, default = 'A', + editable = False, db_index = True) + key = models.CharField(max_length = keyLength, editable = False, blank = True, db_index = True) + status_date = models.DateField(default = datetime.date.today, editable = False, db_index = True) + + def __unicode__(self): + if self.name: + return u'%s <%s>' % (self.name, self.email) + return self.email + + class Meta: + ordering = ('name', 'email') + + def setPending(self): + self.status = 'P' + self.status_date = datetime.date.today() + self.genKey() + + def setActive(self): + self.status = 'A' + self.status_date = datetime.date.today() + + def setLeaving(self): + self.status = 'L' + self.status_date = datetime.date.today() + self.genKey() + + def isPending(self): + return self.status == 'P' + + def isLeaving(self): + return self.status == 'L' + + def isActive(self): + return self.status == 'A' + + def current_status(self): + if self.status == 'P': + return 'Pending' + elif self.status == 'L': + return 'Leaving' + elif self.status == 'A': + return 'Active' + else: + return 'Unknown' + + def genKey(self): + self.key = ''.join(random.sample(string.ascii_letters + string.digits, self.keyLength)) + diff -r 25e00d1b99bf -r 63e4211628e1 madeira/band/urls.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/madeira/band/urls.py Tue Feb 14 19:15:16 2012 -0600 @@ -0,0 +1,28 @@ +from django.conf.urls.defaults import * + +urlpatterns = patterns('band.views', + (r'^$', 'index'), + (r'^bio/$', 'bio'), + (r'^buy/$', 'buy'), + (r'^contact/$', 'contact'), + (r'^gigs/$', 'gigs'), + (r'^gigs/flyers$', 'flyers'), + (r'^mail/$', 'mail'), + (r'^mail/confirm/([a-zA-Z0-9]+)$', 'mail_confirm'), + (r'^mail/not_found$', 'mail_not_found'), + (r'^mail/thanks$', 'mail_thanks'), + (r'^mail/unsubscribe$', 'mail_unsubscribe'), + (r'^news/$', 'news'), + (r'^photos/$', 'photos_index'), + (r'^photos/(\d+)$', 'photo_detail'), + (r'^press/$', 'press_index'), + (r'^press/(\d+)$', 'press_detail'), + (r'^songs/$', 'songs'), + (r'^videos/$', 'videos_index'), + (r'^videos/(\d+)$', 'video_detail'), +) + +urlpatterns += patterns('band.admin_views', + (r'^admin/band/email/$', 'email'), + (r'^admin/band/email_sent/$', 'email_sent'), +) diff -r 25e00d1b99bf -r 63e4211628e1 madeira/band/views.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/madeira/band/views.py Tue Feb 14 19:15:16 2012 -0600 @@ -0,0 +1,340 @@ +####################################################################### +# +# PyBand Copyright (C) 2008 - 2011 by Brian Neal +# +####################################################################### +import collections +import datetime +import random + +from django import forms +from django.core.urlresolvers import reverse +from django.http import HttpResponse +from django.http import HttpResponseRedirect +from django.shortcuts import render_to_response +from django.shortcuts import get_object_or_404 +from django.template import RequestContext +from django.template.loader import render_to_string +from django.core.mail import send_mail +from django.db import connection + +from band.models import Article +from band.models import Album +from band.models import Band +from band.models import Fan +from band.models import Gear +from band.models import Gig +from band.models import Member +from band.models import Merchandise +from band.models import Mp3 +from band.models import Mp3_Set +from band.models import News +from band.models import SiteConfig +from band.models import Video_Set +from photologue.models import Gallery +from photologue.models import Photo + +####################################################################### + +def index(request): + config = SiteConfig.objects.get(pk = 1) + carpe = Photo.objects.get(title_slug = 'carpe-noctem') + sandstorm = Photo.objects.get(title_slug = 'sandstorm-cover') + ruins = Photo.objects.get(title_slug = 'ruins-cover') + + upcomingDates = Gig.objects.filter(date__gte = datetime.date.today).order_by('date')[:5] + + return render_to_response('band/index.html', + { + 'config' : config, + 'carpe' : carpe, + 'sandstorm' : sandstorm, + 'ruins' : ruins, + 'upcomingDates' : upcomingDates, + # 'tourPhotos' : tourPhotos, + }, + context_instance = RequestContext(request)) + +####################################################################### + +def bio(request): + members = Member.objects.exclude(is_active__exact = 0) + + return render_to_response('band/bio.html', + { 'members' : members, }, + context_instance = RequestContext(request)) + +####################################################################### + +def gigs(request): + 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 band_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) + + flyerGigs = Gig.objects.exclude(flyer__isnull = True).select_related( + 'venue', 'flyer').order_by('-date') + + return render_to_response('band/gigs.html', { + 'upcoming' : upcoming, + 'previous' : previous, + 'stats' : stats, + 'flyerGigs' : flyerGigs, + }, + context_instance = RequestContext(request)) + +####################################################################### + +def news(request): + news = News.objects.order_by('-date') + + return render_to_response('band/news.html', + { + 'news' : news + }, + context_instance = RequestContext(request)) + +####################################################################### + +def press_index(request): + articles = Article.objects.order_by('-date') + + return render_to_response('band/press.html', + { + 'articles' : articles + }, + context_instance = RequestContext(request)) + +####################################################################### + +def press_detail(request, id): + article = get_object_or_404(Article, pk = id) + + return render_to_response('band/press_detail.html', + { 'article' : article }, + context_instance = RequestContext(request)) + +####################################################################### + +def songs(request): + mp3Sets = Mp3_Set.objects.order_by('-date', '-id') + + return render_to_response('band/songs.html', + { 'mp3Sets' : mp3Sets }, + context_instance = RequestContext(request)) + +####################################################################### + +def photos_index(request): + galleries = Gallery.objects.values('title', 'id').order_by('-id') + + photos = Photo.objects.filter(is_public__exact = 1) + randomPhotos = random.sample(photos, 4) + + return render_to_response('band/photos.html', + { 'galleries' : galleries, 'randomPhotos' : randomPhotos }, + context_instance = RequestContext(request)) + +####################################################################### + +def photo_detail(request, id): + gallery = get_object_or_404(Gallery, pk = id) + photos = gallery.photos.order_by('id') + return render_to_response('band/photo_detail.html', + {'gallery' : gallery, 'photos': photos }, + context_instance = RequestContext(request)) + +####################################################################### + +def videos_index(request): + vidsets = Video_Set.objects.values('title', 'id').order_by('-date') + return render_to_response('band/videos.html', + { 'vidsets' : vidsets }, + context_instance = RequestContext(request)) + +####################################################################### + +def video_detail(request, id): + vidset = get_object_or_404(Video_Set, pk = id) + + return render_to_response('band/video_detail.html', + { 'vidset' : vidset }, + context_instance = RequestContext(request)) + +####################################################################### + +def buy(request): + albums = Album.objects.all().order_by('-id') + merchandise = Merchandise.objects.all().order_by('-id') + config = SiteConfig.objects.values('ordering_info').get(pk = 1) + return render_to_response('band/buy.html', + { 'albums' : albums, 'merchandise' : merchandise, 'config' : config }, + context_instance = RequestContext(request)) + +####################################################################### + +def confirmEmail(config, to, subscribe, key): + band = config.band_name + fromEmail = config.contact_email + url = config.url + if url[-1] != '/': + url += '/' + url += 'mail/confirm/' + key + + if subscribe: + emailTemplate = 'band/email_subscribe.txt' + else: + emailTemplate = 'band/email_unsubscribe.txt' + + msg = render_to_string(emailTemplate, { 'band' : band, 'url' : url, 'band_url' : config.url }) + + subject = '[' + band + '] Mailing List Confirmation' + + send_mail(subject, msg, fromEmail, [to]) + +####################################################################### + +def contact(request): + config = SiteConfig.objects.get(pk = 1) + band = Member.objects.exclude(is_active__exact = 0).order_by('order') + return render_to_response('band/contact.html', + { 'config' : config, 'band' : band }, + context_instance = RequestContext(request)) + +####################################################################### + +class ContactForm(forms.Form): + name = forms.CharField(max_length = 32, required = False, + widget = forms.TextInput(attrs = {'class' : 'form-box'})) + email = forms.EmailField(widget = forms.TextInput(attrs = {'class' : 'form-box'})) + location = forms.CharField(max_length = 32, required = False, + widget = forms.TextInput(attrs = {'class' : 'form-box'})) + option = forms.ChoiceField(choices = (('subscribe', 'Subscribe'), ('unsubscribe', 'Unsubscribe')), + widget = forms.Select(attrs = {'class' : 'form-box'})) + +def mail(request): + config = SiteConfig.objects.get(pk = 1) + form = ContactForm() + if request.method == 'POST': + form = ContactForm(request.POST) + if form.is_valid(): + if form.cleaned_data['option'] == 'unsubscribe': + try: + fan = Fan.objects.get(email = form.cleaned_data['email']) + except Fan.DoesNotExist: + return HttpResponseRedirect(reverse(mail_not_found)) + + fan.setLeaving() + fan.save() + confirmEmail(config, fan.email, False, fan.key) + return HttpResponseRedirect(reverse(mail_unsubscribe)) + + elif form.cleaned_data['option'] == 'subscribe': + try: + fan = Fan.objects.get(email = form.cleaned_data['email']) + except Fan.DoesNotExist: + fan = Fan(name = form.cleaned_data['name'], + email = form.cleaned_data['email'], + location = form.cleaned_data['location']) + + fan.setPending() + fan.save() + confirmEmail(config, fan.email, True, fan.key) + return HttpResponseRedirect(reverse(mail_thanks)) + + return render_to_response('band/mail.html', + { 'form' : form }, + context_instance = RequestContext(request)) + +####################################################################### + +def mail_not_found(request): + return render_to_response('band/mail_not_found.html', + {}, + context_instance = RequestContext(request)) + +####################################################################### + +def mail_thanks(request): + return render_to_response('band/mail_thanks.html', + {}, + context_instance = RequestContext(request)) + +####################################################################### + +def mail_unsubscribe(request): + return render_to_response('band/mail_unsubscribe.html', + {}, + context_instance = RequestContext(request)) + +####################################################################### + +def mail_confirm(request, key): + fan = get_object_or_404(Fan, key = key) + + email = fan.email + action = 'subscribed' + + if fan.isPending(): + fan.setActive() + fan.save() + elif fan.isLeaving(): + fan.delete() + action = 'unsubscribed' + + return render_to_response('band/mail_confirm.html', + { 'email' : email, 'action' : action }, + context_instance = RequestContext(request)) + +####################################################################### + +def flyers(request): + + gigs = Gig.objects.exclude(flyer__isnull = True).order_by('-date') + + return render_to_response('band/flyers.html', + { 'gigs' : gigs }, + context_instance = RequestContext(request)) diff -r 25e00d1b99bf -r 63e4211628e1 madeira/manage.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/madeira/manage.py Tue Feb 14 19:15:16 2012 -0600 @@ -0,0 +1,11 @@ +#!/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) diff -r 25e00d1b99bf -r 63e4211628e1 madeira/photologue/LICENSE.txt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/madeira/photologue/LICENSE.txt Tue Feb 14 19:15:16 2012 -0600 @@ -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. diff -r 25e00d1b99bf -r 63e4211628e1 madeira/photologue/README.txt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/madeira/photologue/README.txt Tue Feb 14 19:15:16 2012 -0600 @@ -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 diff -r 25e00d1b99bf -r 63e4211628e1 madeira/photologue/admin.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/madeira/photologue/admin.py Tue Feb 14 19:15:16 2012 -0600 @@ -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 diff -r 25e00d1b99bf -r 63e4211628e1 madeira/photologue/locale/pl/LC_MESSAGES/django.mo Binary file madeira/photologue/locale/pl/LC_MESSAGES/django.mo has changed diff -r 25e00d1b99bf -r 63e4211628e1 madeira/photologue/locale/pl/LC_MESSAGES/django.po --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/madeira/photologue/locale/pl/LC_MESSAGES/django.po Tue Feb 14 19:15:16 2012 -0600 @@ -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 , 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 \n" +"Language-Team: Jakub Wiśniowski \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" + diff -r 25e00d1b99bf -r 63e4211628e1 madeira/photologue/management/__init__.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/madeira/photologue/management/__init__.py Tue Feb 14 19:15:16 2012 -0600 @@ -0,0 +1,1 @@ + diff -r 25e00d1b99bf -r 63e4211628e1 madeira/photologue/management/commands/__init__.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/madeira/photologue/management/commands/__init__.py Tue Feb 14 19:15:16 2012 -0600 @@ -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 diff -r 25e00d1b99bf -r 63e4211628e1 madeira/photologue/management/commands/plcache.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/madeira/photologue/management/commands/plcache.py Tue Feb 14 19:15:16 2012 -0600 @@ -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) diff -r 25e00d1b99bf -r 63e4211628e1 madeira/photologue/management/commands/plcreatesize.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/madeira/photologue/management/commands/plcreatesize.py Tue Feb 14 19:15:16 2012 -0600 @@ -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 diff -r 25e00d1b99bf -r 63e4211628e1 madeira/photologue/management/commands/plflush.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/madeira/photologue/management/commands/plflush.py Tue Feb 14 19:15:16 2012 -0600 @@ -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) diff -r 25e00d1b99bf -r 63e4211628e1 madeira/photologue/management/commands/plinit.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/madeira/photologue/management/commands/plinit.py Tue Feb 14 19:15:16 2012 -0600 @@ -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 diff -r 25e00d1b99bf -r 63e4211628e1 madeira/photologue/models.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/madeira/photologue/models.py Tue Feb 14 19:15:16 2012 -0600 @@ -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'' % \ + (self.get_absolute_url(), func()) + else: + return u'' % \ + (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'' % 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) diff -r 25e00d1b99bf -r 63e4211628e1 madeira/photologue/res/sample.jpg Binary file madeira/photologue/res/sample.jpg has changed diff -r 25e00d1b99bf -r 63e4211628e1 madeira/photologue/res/test_landscape.jpg Binary file madeira/photologue/res/test_landscape.jpg has changed diff -r 25e00d1b99bf -r 63e4211628e1 madeira/photologue/res/test_portrait.jpg Binary file madeira/photologue/res/test_portrait.jpg has changed diff -r 25e00d1b99bf -r 63e4211628e1 madeira/photologue/res/test_square.jpg Binary file madeira/photologue/res/test_square.jpg has changed diff -r 25e00d1b99bf -r 63e4211628e1 madeira/photologue/templates/photologue/gallery_archive.html --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/madeira/photologue/templates/photologue/gallery_archive.html Tue Feb 14 19:15:16 2012 -0600 @@ -0,0 +1,26 @@ +{% extends "photologue/root.html" %} + +{% block title %}Latest Photo Galleries{% endblock %} + +{% block content %} + +

Latest Photo Galleries

+ +{% if latest %} + {% for gallery in latest %} + + {% endfor %} +{% else %} +

No galleries were found.

+{% endif %} + +

View all galleries.

+ +{% endblock %} diff -r 25e00d1b99bf -r 63e4211628e1 madeira/photologue/templates/photologue/gallery_archive_day.html --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/madeira/photologue/templates/photologue/gallery_archive_day.html Tue Feb 14 19:15:16 2012 -0600 @@ -0,0 +1,26 @@ +{% extends "photologue/root.html" %} + +{% block title %}Galleries for {{ day|date }}{% endblock %} + +{% block content %} + +

Galleries for {{ day|date }}

+ +{% if object_list %} + {% for gallery in object_list %} + + {% endfor %} +{% else %} +

No galleries were found.

+{% endif %} + +

View all galleries.

+ +{% endblock %} diff -r 25e00d1b99bf -r 63e4211628e1 madeira/photologue/templates/photologue/gallery_archive_month.html --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/madeira/photologue/templates/photologue/gallery_archive_month.html Tue Feb 14 19:15:16 2012 -0600 @@ -0,0 +1,26 @@ +{% extends "photologue/root.html" %} + +{% block title %}Galleries for {{ month|date:"F Y" }}{% endblock %} + +{% block content %} + +

Galleries for {{ month|date:"F Y" }}

+ +{% if object_list %} + {% for gallery in object_list %} + + {% endfor %} +{% else %} +

No galleries were found.

+{% endif %} + +

View all galleries.

+ +{% endblock %} diff -r 25e00d1b99bf -r 63e4211628e1 madeira/photologue/templates/photologue/gallery_archive_year.html --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/madeira/photologue/templates/photologue/gallery_archive_year.html Tue Feb 14 19:15:16 2012 -0600 @@ -0,0 +1,16 @@ +{% extends "photologue/root.html" %} + +{% block title %}Galleries for {{ year }}{% endblock %} + +{% block content %} + +

Galleries for {{ year }}

+ + +

View all galleries.

+ +{% endblock %} diff -r 25e00d1b99bf -r 63e4211628e1 madeira/photologue/templates/photologue/gallery_detail.html --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/madeira/photologue/templates/photologue/gallery_detail.html Tue Feb 14 19:15:16 2012 -0600 @@ -0,0 +1,19 @@ +{% extends "photologue/root.html" %} + +{% block title %}{{ object.title }}{% endblock %} + +{% block content %} + +

{{ object.title }}

+

Originally published {{ object.date_added|date:"l, F jS, Y" }}

+{% if object.description %}

{{ object.description }}

{% endif %} + +

View all galleries

+ +{% endblock %} diff -r 25e00d1b99bf -r 63e4211628e1 madeira/photologue/templates/photologue/gallery_list.html --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/madeira/photologue/templates/photologue/gallery_list.html Tue Feb 14 19:15:16 2012 -0600 @@ -0,0 +1,31 @@ +{% extends "photologue/root.html" %} + +{% block title %}All Galleries{% endblock %} + +{% block content %} + +

All galleries

+ +{% if object_list %} + {% for gallery in object_list %} + + {% endfor %} +{% else %} +

No galleries were found.

+{% endif %} + +{% if is_paginated %} +

{{ hits }} galleries total.

+
+

{% if has_previous %}Previous | {% endif %} page {{ page }} of {{ pages }} {% if has_next %}| Next{% endif %}

+
+{% endif %} + +{% endblock %} diff -r 25e00d1b99bf -r 63e4211628e1 madeira/photologue/templates/photologue/photo_archive.html --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/madeira/photologue/templates/photologue/photo_archive.html Tue Feb 14 19:15:16 2012 -0600 @@ -0,0 +1,20 @@ +{% extends "photologue/root.html" %} + +{% block title %}Latest Photos{% endblock %} + +{% block content %} + +

Latest Photos

+ +{% if latest %} + {% for photo in latest %} + + {% endfor %} +{% else %} +

No photos were found.

+{% endif %} +

View all photographs

+ +{% endblock %} diff -r 25e00d1b99bf -r 63e4211628e1 madeira/photologue/templates/photologue/photo_archive_day.html --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/madeira/photologue/templates/photologue/photo_archive_day.html Tue Feb 14 19:15:16 2012 -0600 @@ -0,0 +1,20 @@ +{% extends "photologue/root.html" %} + +{% block title %}Photos for {{ day|date }}{% endblock %} + +{% block content %} + +

Photos for {{ day|date }}

+ +{% if object_list %} + {% for photo in object_list %} + + {% endfor %} +{% else %} +

No photos were found.

+{% endif %} +

View all photographs

+ +{% endblock %} diff -r 25e00d1b99bf -r 63e4211628e1 madeira/photologue/templates/photologue/photo_archive_month.html --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/madeira/photologue/templates/photologue/photo_archive_month.html Tue Feb 14 19:15:16 2012 -0600 @@ -0,0 +1,20 @@ +{% extends "photologue/root.html" %} + +{% block title %}Photos for {{ month|date:"F Y" }}{% endblock %} + +{% block content %} + +

Photos for {{ month|date:"F Y" }}

+ +{% if object_list %} + {% for photo in object_list %} + + {% endfor %} +{% else %} +

No photos were found.

+{% endif %} +

View all photographs

+ +{% endblock %} diff -r 25e00d1b99bf -r 63e4211628e1 madeira/photologue/templates/photologue/photo_archive_year.html --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/madeira/photologue/templates/photologue/photo_archive_year.html Tue Feb 14 19:15:16 2012 -0600 @@ -0,0 +1,14 @@ +{% extends "photologue/root.html" %} + +{% block title %}Galleries for {{ year }}{% endblock %} + +{% block content %} + +

Photos for {{ year }}

+ + +{% endblock %} diff -r 25e00d1b99bf -r 63e4211628e1 madeira/photologue/templates/photologue/photo_detail.html --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/madeira/photologue/templates/photologue/photo_detail.html Tue Feb 14 19:15:16 2012 -0600 @@ -0,0 +1,21 @@ +{% extends "photologue/root.html" %} + +{% block title %}{{ object.title }}{% endblock %} + +{% block content %} + +

{{ object.title }}

+ +{% if object.public_galleries %} +

This photo is found in the following galleries:

+
    +{% for gallery in object.public_galleries %} +
  1. {{ gallery.title }}
  2. +{% endfor %} +
+{% endif %} + +{% endblock %} diff -r 25e00d1b99bf -r 63e4211628e1 madeira/photologue/templates/photologue/photo_list.html --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/madeira/photologue/templates/photologue/photo_list.html Tue Feb 14 19:15:16 2012 -0600 @@ -0,0 +1,26 @@ +{% extends "photologue/root.html" %} + +{% block title %}All Photos{% endblock %} + +{% block content %} + +

All Photos

+ +{% if object_list %} + {% for photo in object_list %} + + {% endfor %} +{% else %} +

No photos were found.

+{% endif %} + +{% if is_paginated %} +

{{ hits }} photos total.

+
+

{% if has_previous %}Previous | {% endif %} page {{ page }} of {{ pages }} {% if has_next %}| Next{% endif %}

+
+{% endif %} + +{% endblock %} diff -r 25e00d1b99bf -r 63e4211628e1 madeira/photologue/templates/photologue/root.html --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/madeira/photologue/templates/photologue/root.html Tue Feb 14 19:15:16 2012 -0600 @@ -0,0 +1,1 @@ +{% extends "base.html" %} \ No newline at end of file diff -r 25e00d1b99bf -r 63e4211628e1 madeira/photologue/tests.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/madeira/photologue/tests.py Tue Feb 14 19:15:16 2012 -0600 @@ -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) + diff -r 25e00d1b99bf -r 63e4211628e1 madeira/photologue/urls.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/madeira/photologue/urls.py Tue Feb 14 19:15:16 2012 -0600 @@ -0,0 +1,36 @@ +from django.conf import settings +from django.conf.urls.defaults 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\d{4})/(?P[a-z]{3})/(?P\w{1,2})/(?P[\-\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\d{4})/(?P[a-z]{3})/(?P\w{1,2})/$', 'archive_day', gallery_args, name='pl-gallery-archive-day'), + url(r'^gallery/(?P\d{4})/(?P[a-z]{3})/$', 'archive_month', gallery_args, name='pl-gallery-archive-month'), + url(r'^gallery/(?P\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[\-\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[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\d{4})/(?P[a-z]{3})/(?P\w{1,2})/(?P[\-\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\d{4})/(?P[a-z]{3})/(?P\w{1,2})/$', 'archive_day', photo_args, name='pl-photo-archive-day'), + url(r'^photo/(?P\d{4})/(?P[a-z]{3})/$', 'archive_month', photo_args, name='pl-photo-archive-month'), + url(r'^photo/(?P\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[\-\d\w]+)/$', 'object_detail', {'slug_field': 'title_slug', 'queryset': Photo.objects.filter(is_public=True)}, name='pl-photo'), + url(r'^photo/page/(?P[0-9]+)/$', 'object_list', {'queryset': Photo.objects.filter(is_public=True), 'allow_empty': True, 'paginate_by': 20}, name='pl-photo-list'), +) + + diff -r 25e00d1b99bf -r 63e4211628e1 madeira/photologue/utils/EXIF.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/madeira/photologue/utils/EXIF.py Tue Feb 14 19:15:16 2012 -0600 @@ -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 + diff -r 25e00d1b99bf -r 63e4211628e1 madeira/photologue/utils/reflection.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/madeira/photologue/utils/reflection.py Tue Feb 14 19:15:16 2012 -0600 @@ -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 diff -r 25e00d1b99bf -r 63e4211628e1 madeira/photologue/utils/watermark.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/madeira/photologue/utils/watermark.py Tue Feb 14 19:15:16 2012 -0600 @@ -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() diff -r 25e00d1b99bf -r 63e4211628e1 madeira/pl-admin.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/madeira/pl-admin.py Tue Feb 14 19:15:16 2012 -0600 @@ -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() diff -r 25e00d1b99bf -r 63e4211628e1 madeira/settings/base.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/madeira/settings/base.py Tue Feb 14 19:15:16 2012 -0600 @@ -0,0 +1,133 @@ +# Base Django settings for madeira project. + +import os +import django.utils.simplejson as json + +PROJECT_PATH = os.path.abspath(os.path.join(os.path.split(__file__)[0], '..')) + +DEBUG = True +TEMPLATE_DEBUG = DEBUG + +ADMINS = [ + ('Brian Neal', 'admin@surfguitar101.com'), +] + +MANAGERS = ADMINS + +INTERNAL_IPS = ['127.0.0.1'] + +# Local time zone for this installation. Choices can be found here: +# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name +# although not all choices may be available on all operating systems. +# If running in a Windows environment this must be set to the same as your +# system time zone. +TIME_ZONE = 'America/Chicago' + +# Language code for this installation. All choices can be found here: +# http://www.i18nguy.com/unicode/language-identifiers.html +LANGUAGE_CODE = 'en-us' + +SITE_ID = 1 + +# If you set this to False, Django will make some optimizations so as not +# to load the internationalization machinery. +USE_I18N = False + +# Absolute path to the directory that holds media. +# Example: "/home/media/media.lawrence.com/" +MEDIA_ROOT = os.path.abspath(os.path.join(PROJECT_PATH, '..', 'media')) + +# URL that handles the media served from MEDIA_ROOT. Make sure to use a +# trailing slash if there is a path component (optional in other cases). +# Examples: "http://media.lawrence.com", "http://example.com/media/" +MEDIA_URL = '/media/' + +# URL prefix for admin media -- CSS, JavaScript and images. Make sure to use a +# trailing slash. +# Examples: "http://foo.com/media/", "/media/". +ADMIN_MEDIA_PREFIX = '/static/admin/' + +# Staticfiles settings: +STATICFILES_DIRS = [ + os.path.abspath(os.path.join(PROJECT_PATH, '..', 'static')), +] +STATIC_ROOT = '/tmp/test_madeira_static_root' +STATIC_URL = '/static/' + +# Make this unique, and don't share it with anybody. +SECRETS = json.load(open(os.path.join(PROJECT_PATH, 'settings', 'secrets.json'))) +SECRET_KEY = SECRETS['SECRET_KEY'] + +TEMPLATE_LOADERS = [ + 'django.template.loaders.filesystem.Loader', + 'django.template.loaders.app_directories.Loader', +# 'django.template.loaders.eggs.Loader', +] + +MIDDLEWARE_CLASSES = [ + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.middleware.doc.XViewMiddleware', + 'django.contrib.flatpages.middleware.FlatpageFallbackMiddleware', +] + +ROOT_URLCONF = 'madeira.urls' + +# Put strings here, like "/home/html/django_templates" or "C:/www/django/templates". +# Always use forward slashes, even on Windows. +# Don't forget to use absolute paths, not relative paths. +TEMPLATE_DIRS = [ + os.path.join(PROJECT_PATH, 'templates'), + os.path.join(PROJECT_PATH, 'templates', 'band'), + os.path.join(PROJECT_PATH, 'photologue', 'templates'), +] + +TEMPLATE_CONTEXT_PROCESSORS = [ + "django.contrib.auth.context_processors.auth", + "django.core.context_processors.debug", + "django.core.context_processors.request", + "django.core.context_processors.media", + "django.core.context_processors.static", + "django.contrib.messages.context_processors.messages", +] + +INSTALLED_APPS = [ + 'django.contrib.admin', + 'django.contrib.admindocs', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.flatpages', + 'django.contrib.markup', + 'django.contrib.messages', + 'django.contrib.sessions', + 'django.contrib.sites', + 'django.contrib.staticfiles', + 'band', + 'photologue', +] + +####################################################################### +# Messages +####################################################################### +MESSAGE_STORAGE = 'django.contrib.messages.storage.session.SessionStorage' + +####################################################################### +# Email +####################################################################### +EMAIL_HOST = 'localhost' +EMAIL_PORT = 1025 + +####################################################################### +# Sessions +####################################################################### +SESSION_ENGINE = "django.contrib.sessions.backends.cached_db" +SESSION_COOKIE_AGE = 2 * 7 * 24 * 60 * 60 # 2 weeks in seconds +SESSION_COOKIE_DOMAIN = None +SESSION_COOKIE_NAME = 'madeira_sessionid' +SESSION_COOKIE_PATH = '/' +SESSION_COOKIE_SECURE = False +SESSION_EXPIRE_AT_BROWSER_CLOSE = False +SESSION_SAVE_EVERY_REQUEST = False diff -r 25e00d1b99bf -r 63e4211628e1 madeira/settings/local.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/madeira/settings/local.py Tue Feb 14 19:15:16 2012 -0600 @@ -0,0 +1,77 @@ +""" +Local Django settings for The Madeira site. +The contents of this file will vary depending on the local installation. + +""" +from settings.base import * + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.mysql', + 'NAME': SECRETS['DB_NAME'], + 'USER': SECRETS['DB_USER'], + 'PASSWORD': SECRETS['DB_PASSWORD'], + }, +} + +# Django Debug Toolbar support +if DEBUG: + try: + import debug_toolbar + except ImportError: + pass + else: + i = MIDDLEWARE_CLASSES.index('django.middleware.common.CommonMiddleware') + MIDDLEWARE_CLASSES.insert(i + 1, + 'debug_toolbar.middleware.DebugToolbarMiddleware') + INSTALLED_APPS.append('debug_toolbar') + DEBUG_TOOLBAR_CONFIG = { + 'INTERCEPT_REDIRECTS': True, + } + +# Logging configuration + +LOGGING = { + 'version': 1, + 'disable_existing_loggers': True, + 'formatters': { + 'verbose': { + 'format': '%(asctime)s %(levelname)s %(module)s %(process)d %(thread)d %(message)s' + }, + 'simple': { + 'format': '%(asctime)s %(levelname)s %(message)s' + }, + }, + 'handlers': { + 'console': { + 'class': 'logging.StreamHandler', + 'level': 'DEBUG', + 'formatter': 'simple', + }, + 'file': { + 'class': 'logging.handlers.RotatingFileHandler', + 'level': 'DEBUG', + 'formatter': 'simple', + 'filename': os.path.join(PROJECT_PATH, 'logs', 'madeira.log'), + 'mode': 'a', + 'maxBytes': 100 * 1024, + 'backupCount': 10, + }, + 'mail_admins': { + 'class': 'django.utils.log.AdminEmailHandler', + 'level': 'ERROR', + 'formatter': 'simple', + }, + }, + 'loggers': { + 'django':{ + 'level': 'WARNING', + 'propagate': False, + 'handlers': ['file'], + }, + }, + 'root': { + 'level': 'DEBUG', + 'handlers': ['file'], + }, +} diff -r 25e00d1b99bf -r 63e4211628e1 madeira/settings/production.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/madeira/settings/production.py Tue Feb 14 19:15:16 2012 -0600 @@ -0,0 +1,91 @@ +# Django production settings for the madeira project. + +from settings.base import * + +DEBUG = False +TEMPLATE_DEBUG = DEBUG + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.mysql', + 'NAME': 'madeira_django', + 'USER': SECRETS['DB_USER'], + 'PASSWORD': SECRETS['DB_PASSWORD'], + }, +} + +STATIC_ROOT = os.path.abspath(os.path.join(PROJECT_PATH, '..', 'static_serve')) + +TEMPLATE_LOADERS = [ + ('django.template.loaders.cached.Loader', ( + 'django.template.loaders.filesystem.Loader', + 'django.template.loaders.app_directories.Loader', + )), +] + +CACHES = { + 'default': { + 'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache', + 'LOCATION': '127.0.0.1:11211', + 'TIMEOUT': 600, + }, +} +CACHE_MIDDLEWARE_ANONYMOUS_ONLY = True +CACHE_MIDDLEWARE_SECONDS = 600 +CACHE_MIDDLEWARE_KEY_PREFIX = '' + +MIDDLEWARE_CLASSES.insert(0, 'django.middleware.cache.UpdateCacheMiddleware') +MIDDLEWARE_CLASSES.append('django.middleware.cache.FetchFromCacheMiddleware') + +EMAIL_HOST = 'localhost' +EMAIL_PORT = 25 + +LOGGING = { + 'version': 1, + 'disable_existing_loggers': True, + 'formatters': { + 'verbose': { + 'format': '%(asctime)s %(levelname)s %(module)s %(process)d %(thread)d %(message)s' + }, + 'simple': { + 'format': '%(asctime)s %(levelname)s %(message)s' + }, + }, + 'handlers': { + 'console': { + 'class': 'logging.StreamHandler', + 'level': 'DEBUG', + 'formatter': 'simple', + }, + 'file': { + 'class': 'logging.handlers.RotatingFileHandler', + 'level': 'DEBUG', + 'formatter': 'simple', + 'filename': os.path.join(PROJECT_PATH, 'logs', 'madeira.log'), + 'mode': 'a', + 'maxBytes': 100 * 1024, + 'backupCount': 10, + }, + 'mail_admins': { + 'class': 'django.utils.log.AdminEmailHandler', + 'level': 'ERROR', + 'formatter': 'simple', + }, + }, + 'loggers': { + 'django':{ + 'level': 'WARNING', + 'propagate': False, + 'handlers': ['file'], + }, + 'django.request':{ + 'level': 'ERROR', + 'propagate': True, + 'handlers': ['mail_admins'], + }, + }, + 'root': { + 'level': 'INFO', + 'handlers': ['file'], + }, +} diff -r 25e00d1b99bf -r 63e4211628e1 madeira/templates/404.html --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/madeira/templates/404.html Tue Feb 14 19:15:16 2012 -0600 @@ -0,0 +1,16 @@ + + + +Page Not Found + + + + + +

Not Found

+ +

The requested URL {{ request.path|escape }} was not found on this server.

+ + + diff -r 25e00d1b99bf -r 63e4211628e1 madeira/templates/500.html --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/madeira/templates/500.html Tue Feb 14 19:15:16 2012 -0600 @@ -0,0 +1,15 @@ + + + +Internal Server Error + + + +

Internal Server Error

+ +

We're sorry, that page is currently unavailable due to a server misconfiguration.

+

The server administrator has been notified, and we apologise for any inconvenience.

+ + + diff -r 25e00d1b99bf -r 63e4211628e1 madeira/templates/admin/band/email.html --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/madeira/templates/admin/band/email.html Tue Feb 14 19:15:16 2012 -0600 @@ -0,0 +1,34 @@ +{% extends 'admin/base_site.html' %} +{% load adminmedia %} +{% block title %}The Madeira | Mailing List Email Form{% endblock %} +{% block extrastyle %} + +{% endblock %} +{% block bodyclass %}change-form{% endblock %} +{% block breadcrumbs %} + +{% endblock %} +{% block content %} +

Madeira Mailing List Email Form

+
+

Use this form to send an email to all subscribers of the The Madeira mailing list.

+
{% csrf_token %} +
+
+ {% for field in form %} +
+ {% if field.errors %}{{ field.errors }}{% endif %} + {{ field.label_tag }} + {{ field }} +
+ {% endfor %} +
+
+ +
+
+
+
+{% endblock %} diff -r 25e00d1b99bf -r 63e4211628e1 madeira/templates/admin/band/email_sent.html --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/madeira/templates/admin/band/email_sent.html Tue Feb 14 19:15:16 2012 -0600 @@ -0,0 +1,17 @@ +{% extends 'admin/base_site.html' %} +{% load adminmedia %} +{% block title %}The Madeira | Mailing List Email Sent{% endblock %} +{% block extrastyle %} + +{% endblock %} +{% block breadcrumbs %} + +{% endblock %} +{% block content %} +

Madeira Mailing List Email Sent

+
+

Your email has been sent.

+
+{% endblock %} diff -r 25e00d1b99bf -r 63e4211628e1 madeira/templates/admin/base_site.html --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/madeira/templates/admin/base_site.html Tue Feb 14 19:15:16 2012 -0600 @@ -0,0 +1,13 @@ +{% extends "admin/base.html" %} +{% load i18n %} + +{% block title %}{{ title }} | {% trans 'Madeira site admin' %}{% endblock %} +{% block extrahead %} + +{% endblock %} + +{% block branding %} +

{% trans 'Madeira site administration' %}

+{% endblock %} + +{% block nav-global %}{% endblock %} diff -r 25e00d1b99bf -r 63e4211628e1 madeira/templates/admin/index.html --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/madeira/templates/admin/index.html Tue Feb 14 19:15:16 2012 -0600 @@ -0,0 +1,129 @@ +{% extends "admin/base_site.html" %} +{% load i18n %} + +{% block extrastyle %}{{ block.super }}{% endblock %} + +{% block coltype %}colMS{% endblock %} +{% block bodyclass %}dashboard{% endblock %} +{% block breadcrumbs %}{% endblock %} +{% block content %} +
+ +{% if app_list %} +
+ + + + {% if perms.band.add_news or perms.band.change_news %} + + + + + + {% endif %} + {% if perms.band.add_member or perms.band.change_member %} + + + + + + {% endif %} + {% if perms.band.add_gig or perms.band.change_gig %} + + + + + + {% endif %} + {% if perms.band.add_article or perms.band.change_article %} + + + + + + {% endif %} + {% if perms.band.add_mp3_set or perms.band.change_mp3_set %} + + + + + + {% endif %} + {% if perms.band.add_video_set or perms.band.change_video_set %} + + + + + + {% endif %} + {% if perms.band.add_merchandise or perms.band.change_merchandise %} + + + + + + {% endif %} + + + + + + + +
Madeira Quick Links
{% if perms.band.change_news %}{% endif %}News Items{% if perms.band.change_news %}{% endif %}{% if perms.band.add_news %}{% endif %}Add{% if perms.band.add_news %}{% endif %}{% if perms.band.change_news %}{% endif %}Change{% if perms.band.change_news %}{% endif %}
{% if perms.band.change_member %}{% endif %}Biography & Gear{% if perms.band.change_member %}{% endif %}{% if perms.band.add_member %}{% endif %}Add{% if perms.band.add_member %}{% endif %}{% if perms.band.change_member %}{% endif %}Change{% if perms.band.change_member %}{% endif %}
{% if perms.band.change_gig %}{% endif %}Gigs{% if perms.band.change_gig %}{% endif %}{% if perms.band.add_gig %}{% endif %}Add{% if perms.band.add_gig %}{% endif %}{% if perms.band.change_gig %}{% endif %}Change{% if perms.band.change_gig %}{% endif %}
{% if perms.band.change_article %}{% endif %}Press Items{% if perms.band.change_article %}{% endif %}{% if perms.band.add_article %}{% endif %}Add{% if perms.band.add_article %}{% endif %}{% if perms.band.change_article %}{% endif %}Change{% if perms.band.change_article %}{% endif %}
{% if perms.band.change_mp3_set %}{% endif %}Songs{% if perms.band.change_mp3_set %}{% endif %}{% if perms.band.add_mp3_set %}{% endif %}Add{% if perms.band.add_mp3_set %}{% endif %}{% if perms.band.change_mp3_set %}{% endif %}Change{% if perms.band.change_mp3_set %}{% endif %}
{% if perms.band.change_video_set %}{% endif %}Videos{% if perms.band.change_video_set %}{% endif %}{% if perms.band.add_video_set %}{% endif %}Add{% if perms.band.add_video_set %}{% endif %}{% if perms.band.change_video_set %}{% endif %}Change{% if perms.band.change_video_set %}{% endif %}
{% if perms.band.change_merchandise %}{% endif %}Merchandise{% if perms.band.change_merchandise %}{% endif %}{% if perms.band.add_merchandise %}{% endif %}Add{% if perms.band.add_merchandise %}{% endif %}{% if perms.band.change_merchandise %}{% endif %}Change{% if perms.band.change_merchandise %}{% endif %}
Send Email to Mailing List  
+
+ + + {% for app in app_list %} +
+ + + {% for model in app.models %} + + {% if model.perms.change %} + + {% else %} + + {% endif %} + + {% if model.perms.add %} + + {% else %} + + {% endif %} + + {% if model.perms.change %} + + {% else %} + + {% endif %} + + {% endfor %} +
{% blocktrans with app.name as name %}{{ name }}{% endblocktrans %}
{{ model.name }}{{ model.name }}{% trans 'Add' %} {% trans 'Change' %} 
+
+ {% endfor %} +{% else %} +

{% trans "You don't have permission to edit anything." %}

+{% endif %} +
+{% endblock %} + +{% block sidebar %} + +{% endblock %} diff -r 25e00d1b99bf -r 63e4211628e1 madeira/templates/band/base.html --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/madeira/templates/band/base.html Tue Feb 14 19:15:16 2012 -0600 @@ -0,0 +1,64 @@ + + +{% load url from future %} + +{% block title %}{% endblock %} + + + + + + + + + + + +{% block custom_css %}{% endblock %} +{% block custom_js %}{% endblock %} + + + +
+ + + + + +
+ {% block content %} + {% endblock %} +
+ + + +
+ + diff -r 25e00d1b99bf -r 63e4211628e1 madeira/templates/band/bio.html --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/madeira/templates/band/bio.html Tue Feb 14 19:15:16 2012 -0600 @@ -0,0 +1,26 @@ +{% extends 'band/base.html' %} +{% block title %}The Madeira | Biography{% endblock %} +{% block content %} +

Band Biography

+{% if members %} + {% for member in members %} +

{{ member.name }} - {{ member.instrument }}

+

+ {% if member.photo %} + {{ member.name }} + {{ member.bio|linebreaks }} +

+ {% if member.gear_set.all %} +

Gear:

+
    + {% for item in member.gear_set.all %} +
  • {{ item.item }}
  • + {% endfor %} +
+ {% endif %} + {% endif %} + {% endfor %} +{% else %} +The band has no members. +{% endif %} +{% endblock %} diff -r 25e00d1b99bf -r 63e4211628e1 madeira/templates/band/buy.html --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/madeira/templates/band/buy.html Tue Feb 14 19:15:16 2012 -0600 @@ -0,0 +1,52 @@ +{% extends 'band/base.html' %} +{% block title %}The Madeira | Merchandise{% endblock %} +{% block content %} +

Madeira Merchandise

+{% for album in albums %} +

{{ album.title }}

+

+ {{ album.title }} +

+ {% if album.label_release_set %} +
    + {% for release in album.label_release_set.all %} +
  • {{ release.record_label.name }} + {{ release.catalog_number }}, {{ release.release_date|date:"F d, Y" }}
  • + {% endfor %} +
+ {% endif %} + {{ album.desc|safe|linebreaks }} +

Track listing:

+
    + {% for track in album.album_track_set.all %} +
  1. {{ track.track_name }}
  2. + {% endfor %} +
+ {% if album.album_merchant_set %} +

Buy {{ album.title }} at:

+ + {% endif %} +
+{% endfor %} +{% if merchandise %} +
+ {{ config.ordering_info|safe|linebreaks }} +{% endif %} +{% for item in merchandise %} +

{{ item.name }}

+

+ {{ item.name }} +

+ {{ item.desc|safe|linebreaks }} + {% if item.in_stock %} +

Price: ${{ item.price }}

+ {% else %} +

Price: ${{ item.price }} SOLD OUT!

+ {% endif %} +
+{% endfor %} +{% endblock %} diff -r 25e00d1b99bf -r 63e4211628e1 madeira/templates/band/contact.html --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/madeira/templates/band/contact.html Tue Feb 14 19:15:16 2012 -0600 @@ -0,0 +1,14 @@ +{% extends 'band/base.html' %} +{% block title %}The Madeira | Contact{% endblock %} +{% block content %} +

Madeira Contact Info

+

For general band inquiries, send email to: {{ config.contact_email }}.

+

To contact individual band members:

+
    +{% for member in band %} + {% if member.email %} +
  • {{ member.name }}: {{ member.email }}
  • + {% endif %} +{% endfor %} +
+{% endblock %} diff -r 25e00d1b99bf -r 63e4211628e1 madeira/templates/band/email_subscribe.txt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/madeira/templates/band/email_subscribe.txt Tue Feb 14 19:15:16 2012 -0600 @@ -0,0 +1,19 @@ +Hello, + +We have received a request for this email address to join the mailing list +for {{ band }}. In order for us to process this subscription, we need confirmation from you. + +If you did not request to join this mailing list, you may ignore this message. + +To subscribe to the mailing list, go to the following confirmation URL: + +{{ url }} + +This should take you directly to an email confirmation page. If it does not, +please copy and paste the full URL into your web browser's address box and +hit the "Enter" key on your keyboard. + +Thanks, + +{{ band }} +{{ band_url }} diff -r 25e00d1b99bf -r 63e4211628e1 madeira/templates/band/email_unsubscribe.txt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/madeira/templates/band/email_unsubscribe.txt Tue Feb 14 19:15:16 2012 -0600 @@ -0,0 +1,19 @@ +Hello, + +We have received a request for this email address to unsubscribe from the mailing list +for {{ band }}. In order for us to process this request, we need confirmation from you. + +If you did not request to unsubscribe from this mailing list, you may ignore this message. + +To unsubscribe from the mailing list, go to the following confirmation URL: + +{{ url }} + +This should take you directly to an email confirmation page. If it does not, +please copy and paste the full URL into your web browser's address box and +hit the "Enter" key on your keyboard. + +Thanks, + +{{ band }} +{{ band_url }} diff -r 25e00d1b99bf -r 63e4211628e1 madeira/templates/band/flyers.html --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/madeira/templates/band/flyers.html Tue Feb 14 19:15:16 2012 -0600 @@ -0,0 +1,20 @@ +{% extends 'band/base.html' %} +{% block title %}The Madeira | Flyer Gallery{% endblock %} +{% block content %} +

Show Flyer Gallery

+{% if gigs %} +
+ {% for gig in gigs %} +

+ {% if gig.title %} + {{ gig.title }} + {% else %} + {{ gig.date|date: + {% endif %} +

+ {% endfor %} +
+{% else %} +No flyers available at this time. +{% endif %} +{% endblock %} diff -r 25e00d1b99bf -r 63e4211628e1 madeira/templates/band/gigs.html --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/madeira/templates/band/gigs.html Tue Feb 14 19:15:16 2012 -0600 @@ -0,0 +1,166 @@ +{% extends 'band/base.html' %} +{% load url from future %} +{% block title %}The Madeira | Shows{% endblock %} +{% block custom_css %} + +{% endblock %} +{% block custom_js %} + + + +{% endblock %} +{% block content %} +

Show Dates

+ +

Upcoming Shows

+{% if upcoming %} + {% for show in upcoming %} +

+ {% if show.flyer %} + + + {{ show.flyer.caption }} + {% endif %} + {{ show.date|date:"F d, Y" }} + {% if show.time %}{{ show.time|time:"h:i A" }}{% endif %}
+ + {% if show.title and show.url %} + {{ show.title }}
+ {% else %} + {% if show.title %} + {{ show.title }}
+ {% endif %} + {% endif %} + + {% if show.venue %} + {% if show.venue.url %} + {{ show.venue.name }}, + {% else %} + {{ show.venue }}, + {% endif %} + {% if show.venue.address %} + {{ show.venue.address }}, + {% endif %} + {% if show.venue.city.state %} + {{ show.venue.city.name }}, {{ show.venue.city.state.name }} + {% else %} + {{ show.venue.city.name }} + {% endif %} + {% ifnotequal show.venue.city.country.name "USA" %} + {{ show.venue.city.country.name }} + {% endifnotequal %} +
+ {% if show.venue.phone %} + {{ show.venue.phone }} +
+ {% endif %} + {% endif %} + + {% if show.bands_ %} + With: + {% for band in show.bands_ %} + {% if band.url %} + {{ band.name }} + {% else %} + {{ band.name }} + {% endif %} + {% if not forloop.last %} + • + {% endif %} + {% endfor %} +
+ {% endif %} + + {% if show.notes %} + {{ show.notes|safe }} + {% endif %} +

+ {% endfor %} +{% else %} +None at this time. +{% endif %} +
+ +{% if flyerGigs %} +
+

Flyers

+
+ {% for gig in flyerGigs %} +
+ + + +
{{ gig.venue.name}}, {{ gig.date|date:"F 'y" }}
+ + {{ gig.date|date: +
+
+ {% endfor %} +
+
+

To see all our flyers in full size, check out our show flyer gallery.

+
+{% endif %} + +{% if previous %} +

Previous Shows

+
+ + + {% for show in previous %} + + + + + + {% endfor %} +
DateVenueBands
{{ show.date|date:"M d, Y" }} + {% if show.title and show.url %} + {{ show.title }}, + {% else %} + {% if show.title %} + {{ show.title }}, + {% endif %} + {% endif %} + {% if show.venue.url %} + {{ show.venue.name }}, + {% else %} + {{ show.venue.name }}, + {% endif %} + {{ show.venue.city.name }}, {{ show.venue.city.state.abbrev }} + {% ifnotequal show.venue.city.country.name "USA" %} + {{ show.venue.city.country.name }} + {% endifnotequal %} + + {% for band in show.bands_ %} + {% if band.url %} + {{ band.name }} + {% else %} + {{ band.name }} + {% endif %} + {% if not forloop.last %} + • + {% endif %} + {% endfor %} +
+
+{% endif %} + +{% if stats %} +

Past Show Statistics

+ + + + + + + +
Number of shows:{{ stats.count }}
Number of unique venues:{{ stats.venues }}
Number of unique cities:{{ stats.cities }}
Number of unique states:{{ stats.states }}
Number of unique countries:{{ stats.countries }}
Number of unique bands:{{ stats.bands }}
+{% endif %} + +{% endblock %} diff -r 25e00d1b99bf -r 63e4211628e1 madeira/templates/band/index.html --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/madeira/templates/band/index.html Tue Feb 14 19:15:16 2012 -0600 @@ -0,0 +1,90 @@ +{% extends 'band/base.html' %} +{% load url from future %} +{% block title %}The Madeira{% endblock %} +{% block custom_css %} + +{% endblock %} +{% block custom_js %} + + + +{% endblock %} +{% load markup %} +{% block content %} +

The Madeira

+{{ config.intro_photo.title }} +{{ config.intro_text|textile }} +
+ +{% if upcomingDates %} +
+

Upcoming Shows...

+ +
+{% for gig in upcomingDates %} + {% if gig.flyer %} + + {% endif %} +{% endfor %} +
+ + {{ gig.flyer.caption }} +
{{ gig.flyer.caption }}
+
+ +
    +{% for show in upcomingDates %} +
  • {{ show.date|date:"l, F d" }}: {{ show.venue.name }}, {{ show.venue.city.name }}{% if show.venue.city.state %}, {{ show.venue.city.state.name }} +{% endif %} +{% ifnotequal show.venue.city.country.name "USA" %} +{{ show.venue.city.country.name }} +{% endifnotequal %} +
  • +{% endfor %} +
+
See all upcoming shows...
+
+{% endif %} + +
+
+

Sandstorm from Sound of the Surf

+ +

Another clip from the upcoming film Sound of the Surf has just been released, and it is our performance of Sandstorm! This movie cannot come out soon enough!

+
+
+
+
+

New Song Preview!

+ +

Check out this set of 6 videos from our show at Mahogany's in February. Five of the songs are new originals slated for our new album! Video courtesy of TikiTim.

+
+
+ +
+
+ + + + + + + + + + +

The Madeira Releases:

+ Carpe Noctem Cover +
Available Now: Carpe Noctem!
+
Sandstorm CD Cover
Ruins EP Cover
Sandstorm
Ruins
+
+
+ +{% endblock %} diff -r 25e00d1b99bf -r 63e4211628e1 madeira/templates/band/mail.html --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/madeira/templates/band/mail.html Tue Feb 14 19:15:16 2012 -0600 @@ -0,0 +1,21 @@ +{% extends 'band/base.html' %} +{% block title %}The Madeira | Mailing List{% endblock %} +{% block content %} +

Madeira Mailing List

+

Get on the Madeira mailing list to receive updates about upcoming shows, releases, and website updates. +This is a low volume list. We do not share your email address with anyone.

+
Mailing List +
{% csrf_token %} + + {% for field in form %} + + + + + {% endfor %} + +
{{ field.label_tag }}{% if field.field.required %}*{% endif %}:{{ field }} + {% if field.errors %}{{ field.errors }}{% endif %}
+
+
+{% endblock %} diff -r 25e00d1b99bf -r 63e4211628e1 madeira/templates/band/mail_confirm.html --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/madeira/templates/band/mail_confirm.html Tue Feb 14 19:15:16 2012 -0600 @@ -0,0 +1,6 @@ +{% extends 'band/base.html' %} +{% block title %}The Madeira | Mailing List Confirmation{% endblock %} +{% block content %} +

Madeira Mailing List Confirmation

+

Your email address, {{ email }}, has been successfully {{ action }}.

+{% endblock %} diff -r 25e00d1b99bf -r 63e4211628e1 madeira/templates/band/mail_not_found.html --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/madeira/templates/band/mail_not_found.html Tue Feb 14 19:15:16 2012 -0600 @@ -0,0 +1,8 @@ +{% extends 'band/base.html' %} +{% load url from future %} +{% block title %}The Madeira | Mailing List Confirmation{% endblock %} +{% block content %} +

Madeira Mailing List

+

Sorry, we did not find that email address in our database.

+

Back to contact page.

+{% endblock %} diff -r 25e00d1b99bf -r 63e4211628e1 madeira/templates/band/mail_thanks.html --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/madeira/templates/band/mail_thanks.html Tue Feb 14 19:15:16 2012 -0600 @@ -0,0 +1,9 @@ +{% extends 'band/base.html' %} +{% block title %}The Madeira | Mailing List Confirmation{% endblock %} +{% block content %} +

Madeira Mailing List

+

Thanks for subscribing to our email list! You should shortly receive a confirmation email +with instructions on how to complete the subscription process.

+

Please check your spam folders for this email. Sometimes it ends up in there. +Thanks.

+{% endblock %} diff -r 25e00d1b99bf -r 63e4211628e1 madeira/templates/band/mail_unsubscribe.html --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/madeira/templates/band/mail_unsubscribe.html Tue Feb 14 19:15:16 2012 -0600 @@ -0,0 +1,8 @@ +{% extends 'band/base.html' %} +{% block title %}The Madeira | Mailing List Confirmation{% endblock %} +{% block content %} +

Madeira Mailing List

+

We're sorry to see you unsubscribing from our email list! You should shortly receive a confirmation email +with instructions on how to complete the removal process. Please check your spam folders for +this email.

+{% endblock %} diff -r 25e00d1b99bf -r 63e4211628e1 madeira/templates/band/news.html --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/madeira/templates/band/news.html Tue Feb 14 19:15:16 2012 -0600 @@ -0,0 +1,31 @@ +{% extends 'band/base.html' %} +{% load markup %} +{% block title %}The Madeira | News{% endblock %} +{% block content %} +

News

+{% if news %} + {% for story in news %} +

{{ story.date|date:"F d, Y" }}  + {% if story.title %} + • {{ story.title }} + {% endif %} +

+
+ {% if story.photo %} + {{ story.photo_caption }} + {% endif %} + {% if story.markup_enabled %} + {{ story.text|textile }} + {% else %} + {{ story.text|safe|linebreaks }} + {% endif %} + {% if story.author %} +

-- {{ story.author }}

+ {% endif %} +
+ {% endfor %} +{% else %} +No news at this time. +{% endif %} +{% endblock %} diff -r 25e00d1b99bf -r 63e4211628e1 madeira/templates/band/photo_detail.html --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/madeira/templates/band/photo_detail.html Tue Feb 14 19:15:16 2012 -0600 @@ -0,0 +1,29 @@ +{% extends 'band/base.html' %} +{% load url from future %} +{% load markup %} +{% block title %}The Madeira | Photos: {{ gallery.title }}{% endblock %} +{% block custom_css %} + +{% endblock %} +{% block custom_js %} + + + +{% endblock %} +{% block content %} +

Madeira Photos: {{ gallery.title }}

+{{ gallery.description|textile }} + +
+{% for photo in photos %} + + {{ photo.caption }} +{% endfor %} +
+
Photo gallery index
+ +{% endblock %} diff -r 25e00d1b99bf -r 63e4211628e1 madeira/templates/band/photos.html --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/madeira/templates/band/photos.html Tue Feb 14 19:15:16 2012 -0600 @@ -0,0 +1,36 @@ +{% extends 'band/base.html' %} +{% load url from future %} +{% block title %}The Madeira | Photo Galleries{% endblock %} +{% block custom_css %} + +{% endblock %} +{% block custom_js %} + + + +{% endblock %} +{% block content %} +

Madeira Photo Galleries

+{% if galleries %} + +{% else %} +No photo galleries available at this time. +{% endif %} +{% if randomPhotos %} +
+

Random Photos:

+ {% for photo in randomPhotos %} + + {{ photo.caption }} + {% endfor %} +
+{% endif %} +{% endblock %} diff -r 25e00d1b99bf -r 63e4211628e1 madeira/templates/band/press.html --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/madeira/templates/band/press.html Tue Feb 14 19:15:16 2012 -0600 @@ -0,0 +1,44 @@ +{% extends 'band/base.html' %} +{% load markup %} +{% block title %}The Madeira | Press{% endblock %} +{% block content %} +

Madeira Press, Articles, & Reviews

+{% if articles %} +   +

Contents

+ + + {% for article in articles %} +   +

{{ article.title }}

+ {% if article.markup_enabled %} + {{ article.text|textile }} + {% else %} + {{ article.text|safe|linebreaks }} + {% endif %} +
+ {{ article.source|safe|linebreaks }} +
+ {% if article.url %} + Original article + {% endif %} + {% if article.pdf and article.url %} + | + {% endif %} + {% if article.pdf %} + Original article as PDF + + Adobe Reader + {% endif %} +

Top

+ {% endfor %} + +{% else %} +No articles at this time. +{% endif %} +{% endblock %} diff -r 25e00d1b99bf -r 63e4211628e1 madeira/templates/band/press_detail.html --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/madeira/templates/band/press_detail.html Tue Feb 14 19:15:16 2012 -0600 @@ -0,0 +1,29 @@ +{% extends 'band/base.html' %} +{% load url from future %} +{% load markup %} +{% block title %}The Madeira | Press{% endblock %} +{% block content %} +

Madeira Press, Articles, & Reviews

+

{{ article.title }}

+{% if article.markup_enabled %} + {{ article.text|textile }} +{% else %} + {{ article.text|safe|linebreaks }} +{% endif %} +
+{{ article.source|safe|linebreaks }} +
+{% if article.url %} +Original article +{% endif %} +{% if article.pdf and article.url %} +| +{% endif %} +{% if article.pdf %} +Original article as PDF + + Adobe Reader +{% endif %} +

Press index

+{% endblock %} diff -r 25e00d1b99bf -r 63e4211628e1 madeira/templates/band/songs.html --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/madeira/templates/band/songs.html Tue Feb 14 19:15:16 2012 -0600 @@ -0,0 +1,20 @@ +{% extends 'band/base.html' %} +{% block title %}The Madeira | Songs{% endblock %} +{% block content %} +

Madeira Songs

+{% if mp3Sets %} +

Check out some Madeira MP3 downloads!

+ {% for set in mp3Sets %} +

{{ set.title }}

+ {{ set.text|safe|linebreaks }} +
    + {% for mp3 in set.mp3_set.all %} +
  • {{ mp3.title }} + ({{ mp3.file.size|filesizeformat }}){% if mp3.desc %} - {{ mp3.desc }}{% endif %}
  • + {% endfor %} +
+ {% endfor %} +{% else %} +No downloads available at this time. +{% endif %} +{% endblock %} diff -r 25e00d1b99bf -r 63e4211628e1 madeira/templates/band/video_detail.html --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/madeira/templates/band/video_detail.html Tue Feb 14 19:15:16 2012 -0600 @@ -0,0 +1,18 @@ +{% extends 'band/base.html' %} +{% load url from future %} +{% block title %}The Madeira | Videos: {{ vidset.title }}{% endblock %} +{% block content %} +

Madeira Videos: {{ vidset.title }}

+{{ vidset.text|safe|linebreaks }} + +
+ +{% for video in vidset.video_set.all %} + +{% endfor %} +
{{ video.title }}{{ video.embed_code|safe }}
+
+
+
Videos index
+ +{% endblock %} diff -r 25e00d1b99bf -r 63e4211628e1 madeira/templates/band/videos.html --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/madeira/templates/band/videos.html Tue Feb 14 19:15:16 2012 -0600 @@ -0,0 +1,15 @@ +{% extends 'band/base.html' %} +{% load url from future %} +{% block title %}The Madeira | Videos{% endblock %} +{% block content %} +

Madeira Videos

+{% if vidsets %} + +{% else %} +No videos available at this time. +{% endif %} +{% endblock %} diff -r 25e00d1b99bf -r 63e4211628e1 madeira/templates/flatpages/default.html --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/madeira/templates/flatpages/default.html Tue Feb 14 19:15:16 2012 -0600 @@ -0,0 +1,5 @@ +{% extends 'band/base.html' %} +{% block title %}The Madeira | {{ flatpage.title }}{% endblock %} +{% block content %} +{{ flatpage.content }} +{% endblock %} diff -r 25e00d1b99bf -r 63e4211628e1 madeira/urls.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/madeira/urls.py Tue Feb 14 19:15:16 2012 -0600 @@ -0,0 +1,17 @@ +from django.conf.urls.defaults import * +from django.contrib import admin +from django.conf import settings + +admin.autodiscover() + +urlpatterns = patterns('', + (r'^', include('band.urls')), + (r'^admin/doc/', include('django.contrib.admindocs.urls')), + (r'^admin/', include(admin.site.urls)), + (r'^photologue/', include('photologue.urls')), +) + +if settings.DEBUG: + urlpatterns += patterns('', + (r'^media/(?P.*)$', 'django.views.static.serve', {'document_root': settings.MEDIA_ROOT}), + ) diff -r 25e00d1b99bf -r 63e4211628e1 mysite/apache/madeira.wsgi --- a/mysite/apache/madeira.wsgi Tue Feb 14 19:09:57 2012 -0600 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,37 +0,0 @@ -import os -import sys - -OFFLINE = False - -sys.path.append('/home/var/django-sites/madeira/django-trunk') -sys.path.append('/home/var/django-sites/madeira/madeira-trunk') -sys.path.append('/home/var/django-sites/madeira/madeira-trunk/mysite') -os.environ['PYTHON_EGG_CACHE'] = '/home/var/django-sites/madeira/eggs/' - - -def offline_handler(environ, start_response): - wsgi_dir = os.path.dirname(__file__) - sys.path.append(wsgi_dir) - - offline_file = os.path.join(wsgi_dir, '..', 'templates', 'offline.html') - if os.path.exists(offline_file): - response_headers = [('Content-type','text/html')] - response = open(offline_file).read() - else: - response_headers = [('Content-type','text/plain')] - response = 'themadeira.net website maintenance in progress; please check back soon.' - - if environ['REQUEST_METHOD'] == 'GET': - status = '503 Service Unavailable' - else: - status = '405 Method Not Allowed' - start_response(status, response_headers) - return [response] - - -if not OFFLINE: - os.environ['DJANGO_SETTINGS_MODULE'] = 'mysite.settings.production' - import django.core.handlers.wsgi - application = django.core.handlers.wsgi.WSGIHandler() -else: - application = offline_handler diff -r 25e00d1b99bf -r 63e4211628e1 mysite/band/admin.py --- a/mysite/band/admin.py Tue Feb 14 19:09:57 2012 -0600 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,244 +0,0 @@ -####################################################################### -# -# PyBand Copyright (C) 2008 by Brian Neal -# -####################################################################### - -from django.contrib import admin - -from band.models import Article -from band.models import Album -from band.models import Album_Merchant -from band.models import Album_Track -from band.models import Band -from band.models import City -from band.models import Country -from band.models import Fan -from band.models import Gear -from band.models import Gig -from band.models import Label_Release -from band.models import Member -from band.models import Merchandise -from band.models import Mp3 -from band.models import Mp3_Set -from band.models import News -from band.models import Record_Label -from band.models import SiteConfig -from band.models import State -from band.models import Venue -from band.models import Video -from band.models import Video_Set - -####################################################################### - -admin.site.register(Video) - -####################################################################### - -class SiteConfigAdmin(admin.ModelAdmin): - list_display = ('band_name', 'url', 'contact_email') - fieldsets = ( - (None, { 'fields' : ('band_name', 'url', 'contact_email', 'intro_text', 'ordering_info', - 'intro_photo') }), - ) - -admin.site.register(SiteConfig, SiteConfigAdmin) - -####################################################################### - -class GearInline(admin.TabularInline): - model = Gear - -class GearAdmin(admin.ModelAdmin): - list_display = ('item', 'member') - list_filter = ('member', ) - -admin.site.register(Gear, GearAdmin) - -####################################################################### - -class MemberAdmin(admin.ModelAdmin): - list_display = ('name', 'instrument', 'is_active') - inlines = [ - GearInline, - ] - -admin.site.register(Member, MemberAdmin) - -####################################################################### - -class CityInline(admin.TabularInline): - model = City - -class CityAdmin(admin.ModelAdmin): - list_display = ('name', 'state', 'country') - list_filter = ('state', ) - search_fields = ('name', ) - -admin.site.register(City, CityAdmin) - -####################################################################### - -admin.site.register(Country) - -####################################################################### - -class StateAdmin(admin.ModelAdmin): - inlines = [ - CityInline, - ] - -admin.site.register(State, StateAdmin) - -####################################################################### - -class VenueAdmin(admin.ModelAdmin): - list_filter = ('city', ) - list_display = ('name', 'city', ) - search_fields = ('name', ) - -admin.site.register(Venue, VenueAdmin) - -####################################################################### - -class BandAdmin(admin.ModelAdmin): - search_fields = ('name', ) - -admin.site.register(Band, BandAdmin) - -####################################################################### - -class GigAdmin(admin.ModelAdmin): - list_filter = ('date', 'venue') - save_on_top = True - filter_horizontal = ('bands', ) - -admin.site.register(Gig, GigAdmin) - -####################################################################### - -class NewsAdmin(admin.ModelAdmin): - save_on_top = True - list_filter = ('date', ) - list_display = ('date', 'title') - search_fields = ('text', 'title') - -admin.site.register(News, NewsAdmin) - -####################################################################### - -class ArticleAdmin(admin.ModelAdmin): - save_on_top = True - list_filter = ('date', ) - list_display = ('title', 'date') - search_fields = ('text', 'title') - -admin.site.register(Article, ArticleAdmin) - -####################################################################### - -class Mp3Inline(admin.TabularInline): - model = Mp3 - -class Mp3Admin(admin.ModelAdmin): - prepopulated_fields = {'slug' : ('title', 'desc')} - -admin.site.register(Mp3, Mp3Admin) - -####################################################################### - -class Mp3_SetAdmin(admin.ModelAdmin): - list_filter = ('date', ) - list_display = ('title', 'date') - inlines = [ - Mp3Inline, - ] - -admin.site.register(Mp3_Set, Mp3_SetAdmin) - -####################################################################### - -class VideoInline(admin.TabularInline): - model = Video - -class Video_SetAdmin(admin.ModelAdmin): - list_filter = ('date', ) - list_display = ('title', 'date') - inlines = [ - VideoInline, - ] - -admin.site.register(Video_Set, Video_SetAdmin) - -####################################################################### - -class Album_TrackInline(admin.TabularInline): - model = Album_Track - -class Album_TrackAdmin(admin.ModelAdmin): - list_display = ('track_name', 'album') - list_filter = ('album', ) - -admin.site.register(Album_Track, Album_TrackAdmin) - -####################################################################### - -class Label_ReleaseInline(admin.TabularInline): - model = Label_Release - -class Label_ReleaseAdmin(admin.ModelAdmin): - list_display = ('catalog_number', 'album', 'record_label', 'release_date') - list_filter = ('record_label', 'album') - -admin.site.register(Label_Release, Label_ReleaseAdmin) - -####################################################################### - -class Record_LabelAdmin(admin.ModelAdmin): - inlines = [ - Label_ReleaseInline, - ] - -admin.site.register(Record_Label, Record_LabelAdmin) - -####################################################################### - -class Album_MerchantInline(admin.TabularInline): - model = Album_Merchant - -class Album_MerchantAdmin(admin.ModelAdmin): - list_display = ('name', 'album') - list_filter = ('album', ) - -admin.site.register(Album_Merchant, Album_MerchantAdmin) - -####################################################################### - -class AlbumAdmin(admin.ModelAdmin): - save_on_top = True - inlines = [ - Album_TrackInline, - Label_ReleaseInline, - Album_MerchantInline, - ] - -admin.site.register(Album, AlbumAdmin) - -####################################################################### - -class MerchandiseAdmin(admin.ModelAdmin): - list_display = ('name', 'price', 'in_stock') - list_filter = ('in_stock', ) - -admin.site.register(Merchandise, MerchandiseAdmin) - -####################################################################### - -class FanAdmin(admin.ModelAdmin): - list_display = ('name', 'email', 'current_status') - search_fields = ('name', 'email') - -admin.site.register(Fan, FanAdmin) - -####################################################################### - diff -r 25e00d1b99bf -r 63e4211628e1 mysite/band/admin_views.py --- a/mysite/band/admin_views.py Tue Feb 14 19:09:57 2012 -0600 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,77 +0,0 @@ -####################################################################### -# -# PyBand Copyright (C) 2008 by Brian Neal -# -####################################################################### -from django import forms -from django.core.urlresolvers import reverse -from django.core.mail import EmailMessage -from django.template import RequestContext -from django.http import HttpResponseRedirect -from django.shortcuts import render_to_response -from django.contrib.admin.views.decorators import staff_member_required - -from band.models import SiteConfig -from band.models import Fan - -####################################################################### - -unsubscribeText = ''' - ----- -You are receiving this message because you are subscribed to our mailing list. -If you would like to unsubscribe please visit %s. -''' - -####################################################################### - -class EmailForm(forms.Form): - subject = forms.CharField(max_length = 255, required = True, label = 'Subject:', - widget = forms.TextInput(attrs = {'class' : 'vTextField required', 'size' : '60'})) - message = forms.CharField(label = 'Message:', - widget = forms.Textarea(attrs = {'class' : 'vLargeTextField required'})) - -####################################################################### - -def email_sent(request): - return render_to_response('admin/band/email_sent.html', - {}, - context_instance = RequestContext(request)) - -####################################################################### - -def email(request): - - config = SiteConfig.objects.get(pk = 1) - bandTag = '[%s] ' % (config.band_name, ) - - if request.method == 'POST': - form = EmailForm(request.POST) - if form.is_valid(): - subject = form.cleaned_data['subject'] - message = form.cleaned_data['message'] - - unsubscribeUrl = config.url - if unsubscribeUrl[-1] != '/': - unsubscribeUrl += '/' - unsubscribeUrl += 'mail' - - footer = unsubscribeText % (unsubscribeUrl, ) - message += footer - - fans = Fan.objects.all() - bcc = [fan.email for fan in fans] - - email = EmailMessage(subject, message, config.contact_email, - [config.contact_email], bcc) - email.send() - return HttpResponseRedirect(reverse(email_sent)) - - else: - form = EmailForm(initial = { 'subject' : bandTag }) - - return render_to_response('admin/band/email.html', - { 'form' : form }, - context_instance = RequestContext(request)) - -email = staff_member_required(email) diff -r 25e00d1b99bf -r 63e4211628e1 mysite/band/models.py --- a/mysite/band/models.py Tue Feb 14 19:09:57 2012 -0600 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,393 +0,0 @@ -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 -import datetime -import random -import string - -####################################################################### - -class SiteConfig(models.Model): - band_name = models.CharField(max_length = 50) - url = models.URLField(verify_exists = False, max_length = 200) - contact_email = models.EmailField() - ordering_info = models.TextField(help_text = 'Enter instructions on how to order merchandise here') - intro_text = models.TextField(help_text = 'This text appears on the home page.') - intro_photo = models.ForeignKey(Photo) - - def __unicode__(self): - return self.band_name - - class Meta: - verbose_name = "Site Configuration" - verbose_name_plural = "Site Configuration" - -####################################################################### - -class Member(models.Model): - name = models.CharField(max_length = 50, db_index = True) - nickname = models.CharField(max_length = 50, blank = True) - instrument = models.CharField(max_length = 255) - bio = models.TextField(blank = True) - photo = models.FileField(upload_to = 'images/bio/', blank = True) - order = models.SmallIntegerField(help_text = '''Controls order of display on the bio page, lower numbers displayed - first''') - is_active = models.BooleanField(db_index = True) - start_date = models.DateField() - end_date = models.DateField(blank = True, help_text = 'Only used if the member is not active', - default = datetime.date(1985, 1, 1)) - 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 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 self.name + u', ' + self.state.abbrev - return self.name - -####################################################################### - -class Venue(models.Model): - name = models.CharField(max_length = 50, db_index = True) - url = models.URLField(verify_exists = False, 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(verify_exists = False, 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(verify_exists = False, 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) - - 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 u'' + self.date.strftime('%m/%d/%Y') - - class Meta: - ordering = ('-date', 'time') - -####################################################################### - -class News(models.Model): - title = models.CharField(max_length = 64, blank = True) - date = models.DateField(db_index = True) - author = models.CharField(max_length = 50, blank = True) - text = models.TextField() - markup_enabled = models.BooleanField(default = True, - help_text = 'Check this box to allow Textile style markup in the text field') - photo = models.FileField(upload_to = 'images/news/%Y/%m/%d/', blank = True) - photo_caption = models.CharField(max_length = 50, blank = True) - - def __unicode__(self): - return u'%s %s' % (self.date.strftime('%m/%d/%Y'), self.title) - - class Meta: - ordering = ('-date', ) - verbose_name_plural = "News" - -####################################################################### - -class Article(models.Model): - title = models.CharField(max_length = 64) - date = models.DateField(db_index = True) - text = models.TextField() - markup_enabled = models.BooleanField(default = True, - help_text = 'Check this box to allow Textile style markup in the text field') - 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', ) - -####################################################################### - -class Mp3_Set(models.Model): - date = models.DateField(auto_now_add = True, editable = False) - title = models.CharField(max_length = 64) - text = models.TextField() - - def __unicode__(self): - return self.title - - class Meta: - ordering = ('date', ) - verbose_name = "MP3 Set" - -####################################################################### - -class Mp3(models.Model): - mp3_set = models.ForeignKey(Mp3_Set) - title = models.CharField(max_length = 64) - desc = models.CharField(max_length = 128, blank = True) - file = models.FileField(upload_to = 'mp3s/%Y/%m/%d/') - slug = models.SlugField(unique = True) - - def __unicode__(self): - return self.title - - class Meta: - ordering = ('title', ) - verbose_name = "MP3" - -####################################################################### - -class Video_Set(models.Model): - date = models.DateField(blank=True) - title = models.CharField(max_length = 64) - text = models.TextField() - - def __unicode__(self): - return self.title - - class Meta: - ordering = ('date', ) - verbose_name = "Video Set" - - def save(self, *args, **kwargs): - if not self.id: - self.date = datetime.date.today() - - super(Video_Set, self).save(*args, **kwargs) - -####################################################################### - -class Video(models.Model): - video_set = models.ForeignKey(Video_Set) - title = models.CharField(max_length = 64) - embed_code = models.CharField(max_length = 1024) - - def __unicode__(self): - return self.title - - class Meta: - ordering = ('title', ) - -####################################################################### - -class Record_Label(models.Model): - name = models.CharField(max_length = 64) - url = models.URLField(verify_exists = False, max_length = 200) - - def __unicode__(self): - return self.name - - class Meta: - verbose_name = 'Record Label' - -####################################################################### - -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 Meta: - pass - -####################################################################### - -class Album_Track(models.Model): - album = models.ForeignKey(Album) - 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 Label_Release(models.Model): - record_label = models.ForeignKey(Record_Label) - album = models.ForeignKey(Album) - 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 Album_Merchant(models.Model): - album = models.ForeignKey(Album) - name = models.CharField(max_length = 64) - url = models.URLField(verify_exists = False, 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" - -####################################################################### - -class Fan(models.Model): - statusCodes = (('P', 'Pending'), ('A', 'Active'), ('L', 'Leaving')) - keyLength = 16 - - name = models.CharField(max_length = 32, blank = True) - email = models.EmailField(db_index = True) - location = models.CharField(max_length = 64, blank = True) - status = models.CharField(max_length = 1, choices = statusCodes, default = 'A', - editable = False, db_index = True) - key = models.CharField(max_length = keyLength, editable = False, blank = True, db_index = True) - status_date = models.DateField(default = datetime.date.today, editable = False, db_index = True) - - def __unicode__(self): - if self.name: - return u'%s <%s>' % (self.name, self.email) - return self.email - - class Meta: - ordering = ('name', 'email') - - def setPending(self): - self.status = 'P' - self.status_date = datetime.date.today() - self.genKey() - - def setActive(self): - self.status = 'A' - self.status_date = datetime.date.today() - - def setLeaving(self): - self.status = 'L' - self.status_date = datetime.date.today() - self.genKey() - - def isPending(self): - return self.status == 'P' - - def isLeaving(self): - return self.status == 'L' - - def isActive(self): - return self.status == 'A' - - def current_status(self): - if self.status == 'P': - return 'Pending' - elif self.status == 'L': - return 'Leaving' - elif self.status == 'A': - return 'Active' - else: - return 'Unknown' - - def genKey(self): - self.key = ''.join(random.sample(string.ascii_letters + string.digits, self.keyLength)) - diff -r 25e00d1b99bf -r 63e4211628e1 mysite/band/urls.py --- a/mysite/band/urls.py Tue Feb 14 19:09:57 2012 -0600 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,28 +0,0 @@ -from django.conf.urls.defaults import * - -urlpatterns = patterns('band.views', - (r'^$', 'index'), - (r'^bio/$', 'bio'), - (r'^buy/$', 'buy'), - (r'^contact/$', 'contact'), - (r'^gigs/$', 'gigs'), - (r'^gigs/flyers$', 'flyers'), - (r'^mail/$', 'mail'), - (r'^mail/confirm/([a-zA-Z0-9]+)$', 'mail_confirm'), - (r'^mail/not_found$', 'mail_not_found'), - (r'^mail/thanks$', 'mail_thanks'), - (r'^mail/unsubscribe$', 'mail_unsubscribe'), - (r'^news/$', 'news'), - (r'^photos/$', 'photos_index'), - (r'^photos/(\d+)$', 'photo_detail'), - (r'^press/$', 'press_index'), - (r'^press/(\d+)$', 'press_detail'), - (r'^songs/$', 'songs'), - (r'^videos/$', 'videos_index'), - (r'^videos/(\d+)$', 'video_detail'), -) - -urlpatterns += patterns('band.admin_views', - (r'^admin/band/email/$', 'email'), - (r'^admin/band/email_sent/$', 'email_sent'), -) diff -r 25e00d1b99bf -r 63e4211628e1 mysite/band/views.py --- a/mysite/band/views.py Tue Feb 14 19:09:57 2012 -0600 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,340 +0,0 @@ -####################################################################### -# -# PyBand Copyright (C) 2008 - 2011 by Brian Neal -# -####################################################################### -import collections -import datetime -import random - -from django import forms -from django.core.urlresolvers import reverse -from django.http import HttpResponse -from django.http import HttpResponseRedirect -from django.shortcuts import render_to_response -from django.shortcuts import get_object_or_404 -from django.template import RequestContext -from django.template.loader import render_to_string -from django.core.mail import send_mail -from django.db import connection - -from band.models import Article -from band.models import Album -from band.models import Band -from band.models import Fan -from band.models import Gear -from band.models import Gig -from band.models import Member -from band.models import Merchandise -from band.models import Mp3 -from band.models import Mp3_Set -from band.models import News -from band.models import SiteConfig -from band.models import Video_Set -from photologue.models import Gallery -from photologue.models import Photo - -####################################################################### - -def index(request): - config = SiteConfig.objects.get(pk = 1) - carpe = Photo.objects.get(title_slug = 'carpe-noctem') - sandstorm = Photo.objects.get(title_slug = 'sandstorm-cover') - ruins = Photo.objects.get(title_slug = 'ruins-cover') - - upcomingDates = Gig.objects.filter(date__gte = datetime.date.today).order_by('date')[:5] - - return render_to_response('band/index.html', - { - 'config' : config, - 'carpe' : carpe, - 'sandstorm' : sandstorm, - 'ruins' : ruins, - 'upcomingDates' : upcomingDates, - # 'tourPhotos' : tourPhotos, - }, - context_instance = RequestContext(request)) - -####################################################################### - -def bio(request): - members = Member.objects.exclude(is_active__exact = 0) - - return render_to_response('band/bio.html', - { 'members' : members, }, - context_instance = RequestContext(request)) - -####################################################################### - -def gigs(request): - 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 band_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) - - flyerGigs = Gig.objects.exclude(flyer__isnull = True).select_related( - 'venue', 'flyer').order_by('-date') - - return render_to_response('band/gigs.html', { - 'upcoming' : upcoming, - 'previous' : previous, - 'stats' : stats, - 'flyerGigs' : flyerGigs, - }, - context_instance = RequestContext(request)) - -####################################################################### - -def news(request): - news = News.objects.order_by('-date') - - return render_to_response('band/news.html', - { - 'news' : news - }, - context_instance = RequestContext(request)) - -####################################################################### - -def press_index(request): - articles = Article.objects.order_by('-date') - - return render_to_response('band/press.html', - { - 'articles' : articles - }, - context_instance = RequestContext(request)) - -####################################################################### - -def press_detail(request, id): - article = get_object_or_404(Article, pk = id) - - return render_to_response('band/press_detail.html', - { 'article' : article }, - context_instance = RequestContext(request)) - -####################################################################### - -def songs(request): - mp3Sets = Mp3_Set.objects.order_by('-date', '-id') - - return render_to_response('band/songs.html', - { 'mp3Sets' : mp3Sets }, - context_instance = RequestContext(request)) - -####################################################################### - -def photos_index(request): - galleries = Gallery.objects.values('title', 'id').order_by('-id') - - photos = Photo.objects.filter(is_public__exact = 1) - randomPhotos = random.sample(photos, 4) - - return render_to_response('band/photos.html', - { 'galleries' : galleries, 'randomPhotos' : randomPhotos }, - context_instance = RequestContext(request)) - -####################################################################### - -def photo_detail(request, id): - gallery = get_object_or_404(Gallery, pk = id) - photos = gallery.photos.order_by('id') - return render_to_response('band/photo_detail.html', - {'gallery' : gallery, 'photos': photos }, - context_instance = RequestContext(request)) - -####################################################################### - -def videos_index(request): - vidsets = Video_Set.objects.values('title', 'id').order_by('-date') - return render_to_response('band/videos.html', - { 'vidsets' : vidsets }, - context_instance = RequestContext(request)) - -####################################################################### - -def video_detail(request, id): - vidset = get_object_or_404(Video_Set, pk = id) - - return render_to_response('band/video_detail.html', - { 'vidset' : vidset }, - context_instance = RequestContext(request)) - -####################################################################### - -def buy(request): - albums = Album.objects.all().order_by('-id') - merchandise = Merchandise.objects.all().order_by('-id') - config = SiteConfig.objects.values('ordering_info').get(pk = 1) - return render_to_response('band/buy.html', - { 'albums' : albums, 'merchandise' : merchandise, 'config' : config }, - context_instance = RequestContext(request)) - -####################################################################### - -def confirmEmail(config, to, subscribe, key): - band = config.band_name - fromEmail = config.contact_email - url = config.url - if url[-1] != '/': - url += '/' - url += 'mail/confirm/' + key - - if subscribe: - emailTemplate = 'band/email_subscribe.txt' - else: - emailTemplate = 'band/email_unsubscribe.txt' - - msg = render_to_string(emailTemplate, { 'band' : band, 'url' : url, 'band_url' : config.url }) - - subject = '[' + band + '] Mailing List Confirmation' - - send_mail(subject, msg, fromEmail, [to]) - -####################################################################### - -def contact(request): - config = SiteConfig.objects.get(pk = 1) - band = Member.objects.exclude(is_active__exact = 0).order_by('order') - return render_to_response('band/contact.html', - { 'config' : config, 'band' : band }, - context_instance = RequestContext(request)) - -####################################################################### - -class ContactForm(forms.Form): - name = forms.CharField(max_length = 32, required = False, - widget = forms.TextInput(attrs = {'class' : 'form-box'})) - email = forms.EmailField(widget = forms.TextInput(attrs = {'class' : 'form-box'})) - location = forms.CharField(max_length = 32, required = False, - widget = forms.TextInput(attrs = {'class' : 'form-box'})) - option = forms.ChoiceField(choices = (('subscribe', 'Subscribe'), ('unsubscribe', 'Unsubscribe')), - widget = forms.Select(attrs = {'class' : 'form-box'})) - -def mail(request): - config = SiteConfig.objects.get(pk = 1) - form = ContactForm() - if request.method == 'POST': - form = ContactForm(request.POST) - if form.is_valid(): - if form.cleaned_data['option'] == 'unsubscribe': - try: - fan = Fan.objects.get(email = form.cleaned_data['email']) - except Fan.DoesNotExist: - return HttpResponseRedirect(reverse(mail_not_found)) - - fan.setLeaving() - fan.save() - confirmEmail(config, fan.email, False, fan.key) - return HttpResponseRedirect(reverse(mail_unsubscribe)) - - elif form.cleaned_data['option'] == 'subscribe': - try: - fan = Fan.objects.get(email = form.cleaned_data['email']) - except Fan.DoesNotExist: - fan = Fan(name = form.cleaned_data['name'], - email = form.cleaned_data['email'], - location = form.cleaned_data['location']) - - fan.setPending() - fan.save() - confirmEmail(config, fan.email, True, fan.key) - return HttpResponseRedirect(reverse(mail_thanks)) - - return render_to_response('band/mail.html', - { 'form' : form }, - context_instance = RequestContext(request)) - -####################################################################### - -def mail_not_found(request): - return render_to_response('band/mail_not_found.html', - {}, - context_instance = RequestContext(request)) - -####################################################################### - -def mail_thanks(request): - return render_to_response('band/mail_thanks.html', - {}, - context_instance = RequestContext(request)) - -####################################################################### - -def mail_unsubscribe(request): - return render_to_response('band/mail_unsubscribe.html', - {}, - context_instance = RequestContext(request)) - -####################################################################### - -def mail_confirm(request, key): - fan = get_object_or_404(Fan, key = key) - - email = fan.email - action = 'subscribed' - - if fan.isPending(): - fan.setActive() - fan.save() - elif fan.isLeaving(): - fan.delete() - action = 'unsubscribed' - - return render_to_response('band/mail_confirm.html', - { 'email' : email, 'action' : action }, - context_instance = RequestContext(request)) - -####################################################################### - -def flyers(request): - - gigs = Gig.objects.exclude(flyer__isnull = True).order_by('-date') - - return render_to_response('band/flyers.html', - { 'gigs' : gigs }, - context_instance = RequestContext(request)) diff -r 25e00d1b99bf -r 63e4211628e1 mysite/manage.py --- a/mysite/manage.py Tue Feb 14 19:09:57 2012 -0600 +++ /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) diff -r 25e00d1b99bf -r 63e4211628e1 mysite/photologue/LICENSE.txt --- a/mysite/photologue/LICENSE.txt Tue Feb 14 19:09:57 2012 -0600 +++ /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. diff -r 25e00d1b99bf -r 63e4211628e1 mysite/photologue/README.txt --- a/mysite/photologue/README.txt Tue Feb 14 19:09:57 2012 -0600 +++ /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 diff -r 25e00d1b99bf -r 63e4211628e1 mysite/photologue/admin.py --- a/mysite/photologue/admin.py Tue Feb 14 19:09:57 2012 -0600 +++ /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 diff -r 25e00d1b99bf -r 63e4211628e1 mysite/photologue/locale/pl/LC_MESSAGES/django.mo Binary file mysite/photologue/locale/pl/LC_MESSAGES/django.mo has changed diff -r 25e00d1b99bf -r 63e4211628e1 mysite/photologue/locale/pl/LC_MESSAGES/django.po --- a/mysite/photologue/locale/pl/LC_MESSAGES/django.po Tue Feb 14 19:09:57 2012 -0600 +++ /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 , 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 \n" -"Language-Team: Jakub Wiśniowski \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" - diff -r 25e00d1b99bf -r 63e4211628e1 mysite/photologue/management/__init__.py --- a/mysite/photologue/management/__init__.py Tue Feb 14 19:09:57 2012 -0600 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,1 +0,0 @@ - diff -r 25e00d1b99bf -r 63e4211628e1 mysite/photologue/management/commands/__init__.py --- a/mysite/photologue/management/commands/__init__.py Tue Feb 14 19:09:57 2012 -0600 +++ /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 diff -r 25e00d1b99bf -r 63e4211628e1 mysite/photologue/management/commands/plcache.py --- a/mysite/photologue/management/commands/plcache.py Tue Feb 14 19:09:57 2012 -0600 +++ /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) diff -r 25e00d1b99bf -r 63e4211628e1 mysite/photologue/management/commands/plcreatesize.py --- a/mysite/photologue/management/commands/plcreatesize.py Tue Feb 14 19:09:57 2012 -0600 +++ /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 diff -r 25e00d1b99bf -r 63e4211628e1 mysite/photologue/management/commands/plflush.py --- a/mysite/photologue/management/commands/plflush.py Tue Feb 14 19:09:57 2012 -0600 +++ /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) diff -r 25e00d1b99bf -r 63e4211628e1 mysite/photologue/management/commands/plinit.py --- a/mysite/photologue/management/commands/plinit.py Tue Feb 14 19:09:57 2012 -0600 +++ /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 diff -r 25e00d1b99bf -r 63e4211628e1 mysite/photologue/models.py --- a/mysite/photologue/models.py Tue Feb 14 19:09:57 2012 -0600 +++ /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'' % \ - (self.get_absolute_url(), func()) - else: - return u'' % \ - (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'' % 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) diff -r 25e00d1b99bf -r 63e4211628e1 mysite/photologue/res/sample.jpg Binary file mysite/photologue/res/sample.jpg has changed diff -r 25e00d1b99bf -r 63e4211628e1 mysite/photologue/res/test_landscape.jpg Binary file mysite/photologue/res/test_landscape.jpg has changed diff -r 25e00d1b99bf -r 63e4211628e1 mysite/photologue/res/test_portrait.jpg Binary file mysite/photologue/res/test_portrait.jpg has changed diff -r 25e00d1b99bf -r 63e4211628e1 mysite/photologue/res/test_square.jpg Binary file mysite/photologue/res/test_square.jpg has changed diff -r 25e00d1b99bf -r 63e4211628e1 mysite/photologue/templates/photologue/gallery_archive.html --- a/mysite/photologue/templates/photologue/gallery_archive.html Tue Feb 14 19:09:57 2012 -0600 +++ /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 %} - -

Latest Photo Galleries

- -{% if latest %} - {% for gallery in latest %} - - {% endfor %} -{% else %} -

No galleries were found.

-{% endif %} - -

View all galleries.

- -{% endblock %} diff -r 25e00d1b99bf -r 63e4211628e1 mysite/photologue/templates/photologue/gallery_archive_day.html --- a/mysite/photologue/templates/photologue/gallery_archive_day.html Tue Feb 14 19:09:57 2012 -0600 +++ /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 %} - -

Galleries for {{ day|date }}

- -{% if object_list %} - {% for gallery in object_list %} - - {% endfor %} -{% else %} -

No galleries were found.

-{% endif %} - -

View all galleries.

- -{% endblock %} diff -r 25e00d1b99bf -r 63e4211628e1 mysite/photologue/templates/photologue/gallery_archive_month.html --- a/mysite/photologue/templates/photologue/gallery_archive_month.html Tue Feb 14 19:09:57 2012 -0600 +++ /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 %} - -

Galleries for {{ month|date:"F Y" }}

- -{% if object_list %} - {% for gallery in object_list %} - - {% endfor %} -{% else %} -

No galleries were found.

-{% endif %} - -

View all galleries.

- -{% endblock %} diff -r 25e00d1b99bf -r 63e4211628e1 mysite/photologue/templates/photologue/gallery_archive_year.html --- a/mysite/photologue/templates/photologue/gallery_archive_year.html Tue Feb 14 19:09:57 2012 -0600 +++ /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 %} - -

Galleries for {{ year }}

- - -

View all galleries.

- -{% endblock %} diff -r 25e00d1b99bf -r 63e4211628e1 mysite/photologue/templates/photologue/gallery_detail.html --- a/mysite/photologue/templates/photologue/gallery_detail.html Tue Feb 14 19:09:57 2012 -0600 +++ /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 %} - -

{{ object.title }}

-

Originally published {{ object.date_added|date:"l, F jS, Y" }}

-{% if object.description %}

{{ object.description }}

{% endif %} - -

View all galleries

- -{% endblock %} diff -r 25e00d1b99bf -r 63e4211628e1 mysite/photologue/templates/photologue/gallery_list.html --- a/mysite/photologue/templates/photologue/gallery_list.html Tue Feb 14 19:09:57 2012 -0600 +++ /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 %} - -

All galleries

- -{% if object_list %} - {% for gallery in object_list %} - - {% endfor %} -{% else %} -

No galleries were found.

-{% endif %} - -{% if is_paginated %} -

{{ hits }} galleries total.

-
-

{% if has_previous %}Previous | {% endif %} page {{ page }} of {{ pages }} {% if has_next %}| Next{% endif %}

-
-{% endif %} - -{% endblock %} diff -r 25e00d1b99bf -r 63e4211628e1 mysite/photologue/templates/photologue/photo_archive.html --- a/mysite/photologue/templates/photologue/photo_archive.html Tue Feb 14 19:09:57 2012 -0600 +++ /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 %} - -

Latest Photos

- -{% if latest %} - {% for photo in latest %} - - {% endfor %} -{% else %} -

No photos were found.

-{% endif %} -

View all photographs

- -{% endblock %} diff -r 25e00d1b99bf -r 63e4211628e1 mysite/photologue/templates/photologue/photo_archive_day.html --- a/mysite/photologue/templates/photologue/photo_archive_day.html Tue Feb 14 19:09:57 2012 -0600 +++ /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 %} - -

Photos for {{ day|date }}

- -{% if object_list %} - {% for photo in object_list %} - - {% endfor %} -{% else %} -

No photos were found.

-{% endif %} -

View all photographs

- -{% endblock %} diff -r 25e00d1b99bf -r 63e4211628e1 mysite/photologue/templates/photologue/photo_archive_month.html --- a/mysite/photologue/templates/photologue/photo_archive_month.html Tue Feb 14 19:09:57 2012 -0600 +++ /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 %} - -

Photos for {{ month|date:"F Y" }}

- -{% if object_list %} - {% for photo in object_list %} - - {% endfor %} -{% else %} -

No photos were found.

-{% endif %} -

View all photographs

- -{% endblock %} diff -r 25e00d1b99bf -r 63e4211628e1 mysite/photologue/templates/photologue/photo_archive_year.html --- a/mysite/photologue/templates/photologue/photo_archive_year.html Tue Feb 14 19:09:57 2012 -0600 +++ /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 %} - -

Photos for {{ year }}

- - -{% endblock %} diff -r 25e00d1b99bf -r 63e4211628e1 mysite/photologue/templates/photologue/photo_detail.html --- a/mysite/photologue/templates/photologue/photo_detail.html Tue Feb 14 19:09:57 2012 -0600 +++ /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 %} - -

{{ object.title }}

- -{% if object.public_galleries %} -

This photo is found in the following galleries:

-
    -{% for gallery in object.public_galleries %} -
  1. {{ gallery.title }}
  2. -{% endfor %} -
-{% endif %} - -{% endblock %} diff -r 25e00d1b99bf -r 63e4211628e1 mysite/photologue/templates/photologue/photo_list.html --- a/mysite/photologue/templates/photologue/photo_list.html Tue Feb 14 19:09:57 2012 -0600 +++ /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 %} - -

All Photos

- -{% if object_list %} - {% for photo in object_list %} - - {% endfor %} -{% else %} -

No photos were found.

-{% endif %} - -{% if is_paginated %} -

{{ hits }} photos total.

-
-

{% if has_previous %}Previous | {% endif %} page {{ page }} of {{ pages }} {% if has_next %}| Next{% endif %}

-
-{% endif %} - -{% endblock %} diff -r 25e00d1b99bf -r 63e4211628e1 mysite/photologue/templates/photologue/root.html --- a/mysite/photologue/templates/photologue/root.html Tue Feb 14 19:09:57 2012 -0600 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,1 +0,0 @@ -{% extends "base.html" %} \ No newline at end of file diff -r 25e00d1b99bf -r 63e4211628e1 mysite/photologue/tests.py --- a/mysite/photologue/tests.py Tue Feb 14 19:09:57 2012 -0600 +++ /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) - diff -r 25e00d1b99bf -r 63e4211628e1 mysite/photologue/urls.py --- a/mysite/photologue/urls.py Tue Feb 14 19:09:57 2012 -0600 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,36 +0,0 @@ -from django.conf import settings -from django.conf.urls.defaults 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\d{4})/(?P[a-z]{3})/(?P\w{1,2})/(?P[\-\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\d{4})/(?P[a-z]{3})/(?P\w{1,2})/$', 'archive_day', gallery_args, name='pl-gallery-archive-day'), - url(r'^gallery/(?P\d{4})/(?P[a-z]{3})/$', 'archive_month', gallery_args, name='pl-gallery-archive-month'), - url(r'^gallery/(?P\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[\-\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[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\d{4})/(?P[a-z]{3})/(?P\w{1,2})/(?P[\-\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\d{4})/(?P[a-z]{3})/(?P\w{1,2})/$', 'archive_day', photo_args, name='pl-photo-archive-day'), - url(r'^photo/(?P\d{4})/(?P[a-z]{3})/$', 'archive_month', photo_args, name='pl-photo-archive-month'), - url(r'^photo/(?P\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[\-\d\w]+)/$', 'object_detail', {'slug_field': 'title_slug', 'queryset': Photo.objects.filter(is_public=True)}, name='pl-photo'), - url(r'^photo/page/(?P[0-9]+)/$', 'object_list', {'queryset': Photo.objects.filter(is_public=True), 'allow_empty': True, 'paginate_by': 20}, name='pl-photo-list'), -) - - diff -r 25e00d1b99bf -r 63e4211628e1 mysite/photologue/utils/EXIF.py --- a/mysite/photologue/utils/EXIF.py Tue Feb 14 19:09:57 2012 -0600 +++ /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 - diff -r 25e00d1b99bf -r 63e4211628e1 mysite/photologue/utils/reflection.py --- a/mysite/photologue/utils/reflection.py Tue Feb 14 19:09:57 2012 -0600 +++ /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 diff -r 25e00d1b99bf -r 63e4211628e1 mysite/photologue/utils/watermark.py --- a/mysite/photologue/utils/watermark.py Tue Feb 14 19:09:57 2012 -0600 +++ /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() diff -r 25e00d1b99bf -r 63e4211628e1 mysite/pl-admin.py --- a/mysite/pl-admin.py Tue Feb 14 19:09:57 2012 -0600 +++ /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() diff -r 25e00d1b99bf -r 63e4211628e1 mysite/settings/base.py --- a/mysite/settings/base.py Tue Feb 14 19:09:57 2012 -0600 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,133 +0,0 @@ -# Base Django settings for madeira project. - -import os -import django.utils.simplejson as json - -PROJECT_PATH = os.path.abspath(os.path.join(os.path.split(__file__)[0], '..')) - -DEBUG = True -TEMPLATE_DEBUG = DEBUG - -ADMINS = [ - ('Brian Neal', 'admin@surfguitar101.com'), -] - -MANAGERS = ADMINS - -INTERNAL_IPS = ['127.0.0.1'] - -# Local time zone for this installation. Choices can be found here: -# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name -# although not all choices may be available on all operating systems. -# If running in a Windows environment this must be set to the same as your -# system time zone. -TIME_ZONE = 'America/Chicago' - -# Language code for this installation. All choices can be found here: -# http://www.i18nguy.com/unicode/language-identifiers.html -LANGUAGE_CODE = 'en-us' - -SITE_ID = 1 - -# If you set this to False, Django will make some optimizations so as not -# to load the internationalization machinery. -USE_I18N = False - -# Absolute path to the directory that holds media. -# Example: "/home/media/media.lawrence.com/" -MEDIA_ROOT = os.path.abspath(os.path.join(PROJECT_PATH, '..', 'media')) - -# URL that handles the media served from MEDIA_ROOT. Make sure to use a -# trailing slash if there is a path component (optional in other cases). -# Examples: "http://media.lawrence.com", "http://example.com/media/" -MEDIA_URL = '/media/' - -# URL prefix for admin media -- CSS, JavaScript and images. Make sure to use a -# trailing slash. -# Examples: "http://foo.com/media/", "/media/". -ADMIN_MEDIA_PREFIX = '/static/admin/' - -# Staticfiles settings: -STATICFILES_DIRS = [ - os.path.abspath(os.path.join(PROJECT_PATH, '..', 'static')), -] -STATIC_ROOT = '/tmp/test_madeira_static_root' -STATIC_URL = '/static/' - -# Make this unique, and don't share it with anybody. -SECRETS = json.load(open(os.path.join(PROJECT_PATH, 'settings', 'secrets.json'))) -SECRET_KEY = SECRETS['SECRET_KEY'] - -TEMPLATE_LOADERS = [ - 'django.template.loaders.filesystem.Loader', - 'django.template.loaders.app_directories.Loader', -# 'django.template.loaders.eggs.Loader', -] - -MIDDLEWARE_CLASSES = [ - 'django.middleware.common.CommonMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.middleware.doc.XViewMiddleware', - 'django.contrib.flatpages.middleware.FlatpageFallbackMiddleware', -] - -ROOT_URLCONF = 'mysite.urls' - -# Put strings here, like "/home/html/django_templates" or "C:/www/django/templates". -# Always use forward slashes, even on Windows. -# Don't forget to use absolute paths, not relative paths. -TEMPLATE_DIRS = [ - os.path.join(PROJECT_PATH, 'templates'), - os.path.join(PROJECT_PATH, 'templates', 'band'), - os.path.join(PROJECT_PATH, 'photologue', 'templates'), -] - -TEMPLATE_CONTEXT_PROCESSORS = [ - "django.contrib.auth.context_processors.auth", - "django.core.context_processors.debug", - "django.core.context_processors.request", - "django.core.context_processors.media", - "django.core.context_processors.static", - "django.contrib.messages.context_processors.messages", -] - -INSTALLED_APPS = [ - 'django.contrib.admin', - 'django.contrib.admindocs', - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.flatpages', - 'django.contrib.markup', - 'django.contrib.messages', - 'django.contrib.sessions', - 'django.contrib.sites', - 'django.contrib.staticfiles', - 'band', - 'photologue', -] - -####################################################################### -# Messages -####################################################################### -MESSAGE_STORAGE = 'django.contrib.messages.storage.session.SessionStorage' - -####################################################################### -# Email -####################################################################### -EMAIL_HOST = 'localhost' -EMAIL_PORT = 1025 - -####################################################################### -# Sessions -####################################################################### -SESSION_ENGINE = "django.contrib.sessions.backends.cached_db" -SESSION_COOKIE_AGE = 2 * 7 * 24 * 60 * 60 # 2 weeks in seconds -SESSION_COOKIE_DOMAIN = None -SESSION_COOKIE_NAME = 'madeira_sessionid' -SESSION_COOKIE_PATH = '/' -SESSION_COOKIE_SECURE = False -SESSION_EXPIRE_AT_BROWSER_CLOSE = False -SESSION_SAVE_EVERY_REQUEST = False diff -r 25e00d1b99bf -r 63e4211628e1 mysite/settings/local.py --- a/mysite/settings/local.py Tue Feb 14 19:09:57 2012 -0600 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,77 +0,0 @@ -""" -Local Django settings for The Madeira site. -The contents of this file will vary depending on the local installation. - -""" -from settings.base import * - -DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.mysql', - 'NAME': SECRETS['DB_NAME'], - 'USER': SECRETS['DB_USER'], - 'PASSWORD': SECRETS['DB_PASSWORD'], - }, -} - -# Django Debug Toolbar support -if DEBUG: - try: - import debug_toolbar - except ImportError: - pass - else: - i = MIDDLEWARE_CLASSES.index('django.middleware.common.CommonMiddleware') - MIDDLEWARE_CLASSES.insert(i + 1, - 'debug_toolbar.middleware.DebugToolbarMiddleware') - INSTALLED_APPS.append('debug_toolbar') - DEBUG_TOOLBAR_CONFIG = { - 'INTERCEPT_REDIRECTS': True, - } - -# Logging configuration - -LOGGING = { - 'version': 1, - 'disable_existing_loggers': True, - 'formatters': { - 'verbose': { - 'format': '%(asctime)s %(levelname)s %(module)s %(process)d %(thread)d %(message)s' - }, - 'simple': { - 'format': '%(asctime)s %(levelname)s %(message)s' - }, - }, - 'handlers': { - 'console': { - 'class': 'logging.StreamHandler', - 'level': 'DEBUG', - 'formatter': 'simple', - }, - 'file': { - 'class': 'logging.handlers.RotatingFileHandler', - 'level': 'DEBUG', - 'formatter': 'simple', - 'filename': os.path.join(PROJECT_PATH, 'logs', 'madeira.log'), - 'mode': 'a', - 'maxBytes': 100 * 1024, - 'backupCount': 10, - }, - 'mail_admins': { - 'class': 'django.utils.log.AdminEmailHandler', - 'level': 'ERROR', - 'formatter': 'simple', - }, - }, - 'loggers': { - 'django':{ - 'level': 'WARNING', - 'propagate': False, - 'handlers': ['file'], - }, - }, - 'root': { - 'level': 'DEBUG', - 'handlers': ['file'], - }, -} diff -r 25e00d1b99bf -r 63e4211628e1 mysite/settings/production.py --- a/mysite/settings/production.py Tue Feb 14 19:09:57 2012 -0600 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,91 +0,0 @@ -# Django production settings for the madeira project. - -from settings.base import * - -DEBUG = False -TEMPLATE_DEBUG = DEBUG - -DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.mysql', - 'NAME': 'madeira_django', - 'USER': SECRETS['DB_USER'], - 'PASSWORD': SECRETS['DB_PASSWORD'], - }, -} - -STATIC_ROOT = os.path.abspath(os.path.join(PROJECT_PATH, '..', 'static_serve')) - -TEMPLATE_LOADERS = [ - ('django.template.loaders.cached.Loader', ( - 'django.template.loaders.filesystem.Loader', - 'django.template.loaders.app_directories.Loader', - )), -] - -CACHES = { - 'default': { - 'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache', - 'LOCATION': '127.0.0.1:11211', - 'TIMEOUT': 600, - }, -} -CACHE_MIDDLEWARE_ANONYMOUS_ONLY = True -CACHE_MIDDLEWARE_SECONDS = 600 -CACHE_MIDDLEWARE_KEY_PREFIX = '' - -MIDDLEWARE_CLASSES.insert(0, 'django.middleware.cache.UpdateCacheMiddleware') -MIDDLEWARE_CLASSES.append('django.middleware.cache.FetchFromCacheMiddleware') - -EMAIL_HOST = 'localhost' -EMAIL_PORT = 25 - -LOGGING = { - 'version': 1, - 'disable_existing_loggers': True, - 'formatters': { - 'verbose': { - 'format': '%(asctime)s %(levelname)s %(module)s %(process)d %(thread)d %(message)s' - }, - 'simple': { - 'format': '%(asctime)s %(levelname)s %(message)s' - }, - }, - 'handlers': { - 'console': { - 'class': 'logging.StreamHandler', - 'level': 'DEBUG', - 'formatter': 'simple', - }, - 'file': { - 'class': 'logging.handlers.RotatingFileHandler', - 'level': 'DEBUG', - 'formatter': 'simple', - 'filename': os.path.join(PROJECT_PATH, 'logs', 'madeira.log'), - 'mode': 'a', - 'maxBytes': 100 * 1024, - 'backupCount': 10, - }, - 'mail_admins': { - 'class': 'django.utils.log.AdminEmailHandler', - 'level': 'ERROR', - 'formatter': 'simple', - }, - }, - 'loggers': { - 'django':{ - 'level': 'WARNING', - 'propagate': False, - 'handlers': ['file'], - }, - 'django.request':{ - 'level': 'ERROR', - 'propagate': True, - 'handlers': ['mail_admins'], - }, - }, - 'root': { - 'level': 'INFO', - 'handlers': ['file'], - }, -} diff -r 25e00d1b99bf -r 63e4211628e1 mysite/templates/404.html --- a/mysite/templates/404.html Tue Feb 14 19:09:57 2012 -0600 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,16 +0,0 @@ - - - -Page Not Found - - - - - -

Not Found

- -

The requested URL {{ request.path|escape }} was not found on this server.

- - - diff -r 25e00d1b99bf -r 63e4211628e1 mysite/templates/500.html --- a/mysite/templates/500.html Tue Feb 14 19:09:57 2012 -0600 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,15 +0,0 @@ - - - -Internal Server Error - - - -

Internal Server Error

- -

We're sorry, that page is currently unavailable due to a server misconfiguration.

-

The server administrator has been notified, and we apologise for any inconvenience.

- - - diff -r 25e00d1b99bf -r 63e4211628e1 mysite/templates/admin/band/email.html --- a/mysite/templates/admin/band/email.html Tue Feb 14 19:09:57 2012 -0600 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,34 +0,0 @@ -{% extends 'admin/base_site.html' %} -{% load adminmedia %} -{% block title %}The Madeira | Mailing List Email Form{% endblock %} -{% block extrastyle %} - -{% endblock %} -{% block bodyclass %}change-form{% endblock %} -{% block breadcrumbs %} - -{% endblock %} -{% block content %} -

Madeira Mailing List Email Form

-
-

Use this form to send an email to all subscribers of the The Madeira mailing list.

-
{% csrf_token %} -
-
- {% for field in form %} -
- {% if field.errors %}{{ field.errors }}{% endif %} - {{ field.label_tag }} - {{ field }} -
- {% endfor %} -
-
- -
-
-
-
-{% endblock %} diff -r 25e00d1b99bf -r 63e4211628e1 mysite/templates/admin/band/email_sent.html --- a/mysite/templates/admin/band/email_sent.html Tue Feb 14 19:09:57 2012 -0600 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,17 +0,0 @@ -{% extends 'admin/base_site.html' %} -{% load adminmedia %} -{% block title %}The Madeira | Mailing List Email Sent{% endblock %} -{% block extrastyle %} - -{% endblock %} -{% block breadcrumbs %} - -{% endblock %} -{% block content %} -

Madeira Mailing List Email Sent

-
-

Your email has been sent.

-
-{% endblock %} diff -r 25e00d1b99bf -r 63e4211628e1 mysite/templates/admin/base_site.html --- a/mysite/templates/admin/base_site.html Tue Feb 14 19:09:57 2012 -0600 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,13 +0,0 @@ -{% extends "admin/base.html" %} -{% load i18n %} - -{% block title %}{{ title }} | {% trans 'Madeira site admin' %}{% endblock %} -{% block extrahead %} - -{% endblock %} - -{% block branding %} -

{% trans 'Madeira site administration' %}

-{% endblock %} - -{% block nav-global %}{% endblock %} diff -r 25e00d1b99bf -r 63e4211628e1 mysite/templates/admin/index.html --- a/mysite/templates/admin/index.html Tue Feb 14 19:09:57 2012 -0600 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,129 +0,0 @@ -{% extends "admin/base_site.html" %} -{% load i18n %} - -{% block extrastyle %}{{ block.super }}{% endblock %} - -{% block coltype %}colMS{% endblock %} -{% block bodyclass %}dashboard{% endblock %} -{% block breadcrumbs %}{% endblock %} -{% block content %} -
- -{% if app_list %} -
- - - - {% if perms.band.add_news or perms.band.change_news %} - - - - - - {% endif %} - {% if perms.band.add_member or perms.band.change_member %} - - - - - - {% endif %} - {% if perms.band.add_gig or perms.band.change_gig %} - - - - - - {% endif %} - {% if perms.band.add_article or perms.band.change_article %} - - - - - - {% endif %} - {% if perms.band.add_mp3_set or perms.band.change_mp3_set %} - - - - - - {% endif %} - {% if perms.band.add_video_set or perms.band.change_video_set %} - - - - - - {% endif %} - {% if perms.band.add_merchandise or perms.band.change_merchandise %} - - - - - - {% endif %} - - - - - - - -
Madeira Quick Links
{% if perms.band.change_news %}{% endif %}News Items{% if perms.band.change_news %}{% endif %}{% if perms.band.add_news %}{% endif %}Add{% if perms.band.add_news %}{% endif %}{% if perms.band.change_news %}{% endif %}Change{% if perms.band.change_news %}{% endif %}
{% if perms.band.change_member %}{% endif %}Biography & Gear{% if perms.band.change_member %}{% endif %}{% if perms.band.add_member %}{% endif %}Add{% if perms.band.add_member %}{% endif %}{% if perms.band.change_member %}{% endif %}Change{% if perms.band.change_member %}{% endif %}
{% if perms.band.change_gig %}{% endif %}Gigs{% if perms.band.change_gig %}{% endif %}{% if perms.band.add_gig %}{% endif %}Add{% if perms.band.add_gig %}{% endif %}{% if perms.band.change_gig %}{% endif %}Change{% if perms.band.change_gig %}{% endif %}
{% if perms.band.change_article %}{% endif %}Press Items{% if perms.band.change_article %}{% endif %}{% if perms.band.add_article %}{% endif %}Add{% if perms.band.add_article %}{% endif %}{% if perms.band.change_article %}{% endif %}Change{% if perms.band.change_article %}{% endif %}
{% if perms.band.change_mp3_set %}{% endif %}Songs{% if perms.band.change_mp3_set %}{% endif %}{% if perms.band.add_mp3_set %}{% endif %}Add{% if perms.band.add_mp3_set %}{% endif %}{% if perms.band.change_mp3_set %}{% endif %}Change{% if perms.band.change_mp3_set %}{% endif %}
{% if perms.band.change_video_set %}{% endif %}Videos{% if perms.band.change_video_set %}{% endif %}{% if perms.band.add_video_set %}{% endif %}Add{% if perms.band.add_video_set %}{% endif %}{% if perms.band.change_video_set %}{% endif %}Change{% if perms.band.change_video_set %}{% endif %}
{% if perms.band.change_merchandise %}{% endif %}Merchandise{% if perms.band.change_merchandise %}{% endif %}{% if perms.band.add_merchandise %}{% endif %}Add{% if perms.band.add_merchandise %}{% endif %}{% if perms.band.change_merchandise %}{% endif %}Change{% if perms.band.change_merchandise %}{% endif %}
Send Email to Mailing List  
-
- - - {% for app in app_list %} -
- - - {% for model in app.models %} - - {% if model.perms.change %} - - {% else %} - - {% endif %} - - {% if model.perms.add %} - - {% else %} - - {% endif %} - - {% if model.perms.change %} - - {% else %} - - {% endif %} - - {% endfor %} -
{% blocktrans with app.name as name %}{{ name }}{% endblocktrans %}
{{ model.name }}{{ model.name }}{% trans 'Add' %} {% trans 'Change' %} 
-
- {% endfor %} -{% else %} -

{% trans "You don't have permission to edit anything." %}

-{% endif %} -
-{% endblock %} - -{% block sidebar %} - -{% endblock %} diff -r 25e00d1b99bf -r 63e4211628e1 mysite/templates/band/base.html --- a/mysite/templates/band/base.html Tue Feb 14 19:09:57 2012 -0600 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,64 +0,0 @@ - - -{% load url from future %} - -{% block title %}{% endblock %} - - - - - - - - - - - -{% block custom_css %}{% endblock %} -{% block custom_js %}{% endblock %} - - - -
- - - - - -
- {% block content %} - {% endblock %} -
- - - -
- - diff -r 25e00d1b99bf -r 63e4211628e1 mysite/templates/band/bio.html --- a/mysite/templates/band/bio.html Tue Feb 14 19:09:57 2012 -0600 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,26 +0,0 @@ -{% extends 'band/base.html' %} -{% block title %}The Madeira | Biography{% endblock %} -{% block content %} -

Band Biography

-{% if members %} - {% for member in members %} -

{{ member.name }} - {{ member.instrument }}

-

- {% if member.photo %} - {{ member.name }} - {{ member.bio|linebreaks }} -

- {% if member.gear_set.all %} -

Gear:

-
    - {% for item in member.gear_set.all %} -
  • {{ item.item }}
  • - {% endfor %} -
- {% endif %} - {% endif %} - {% endfor %} -{% else %} -The band has no members. -{% endif %} -{% endblock %} diff -r 25e00d1b99bf -r 63e4211628e1 mysite/templates/band/buy.html --- a/mysite/templates/band/buy.html Tue Feb 14 19:09:57 2012 -0600 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,52 +0,0 @@ -{% extends 'band/base.html' %} -{% block title %}The Madeira | Merchandise{% endblock %} -{% block content %} -

Madeira Merchandise

-{% for album in albums %} -

{{ album.title }}

-

- {{ album.title }} -

- {% if album.label_release_set %} -
    - {% for release in album.label_release_set.all %} -
  • {{ release.record_label.name }} - {{ release.catalog_number }}, {{ release.release_date|date:"F d, Y" }}
  • - {% endfor %} -
- {% endif %} - {{ album.desc|safe|linebreaks }} -

Track listing:

-
    - {% for track in album.album_track_set.all %} -
  1. {{ track.track_name }}
  2. - {% endfor %} -
- {% if album.album_merchant_set %} -

Buy {{ album.title }} at:

- - {% endif %} -
-{% endfor %} -{% if merchandise %} -
- {{ config.ordering_info|safe|linebreaks }} -{% endif %} -{% for item in merchandise %} -

{{ item.name }}

-

- {{ item.name }} -

- {{ item.desc|safe|linebreaks }} - {% if item.in_stock %} -

Price: ${{ item.price }}

- {% else %} -

Price: ${{ item.price }} SOLD OUT!

- {% endif %} -
-{% endfor %} -{% endblock %} diff -r 25e00d1b99bf -r 63e4211628e1 mysite/templates/band/contact.html --- a/mysite/templates/band/contact.html Tue Feb 14 19:09:57 2012 -0600 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,14 +0,0 @@ -{% extends 'band/base.html' %} -{% block title %}The Madeira | Contact{% endblock %} -{% block content %} -

Madeira Contact Info

-

For general band inquiries, send email to: {{ config.contact_email }}.

-

To contact individual band members:

-
    -{% for member in band %} - {% if member.email %} -
  • {{ member.name }}: {{ member.email }}
  • - {% endif %} -{% endfor %} -
-{% endblock %} diff -r 25e00d1b99bf -r 63e4211628e1 mysite/templates/band/email_subscribe.txt --- a/mysite/templates/band/email_subscribe.txt Tue Feb 14 19:09:57 2012 -0600 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,19 +0,0 @@ -Hello, - -We have received a request for this email address to join the mailing list -for {{ band }}. In order for us to process this subscription, we need confirmation from you. - -If you did not request to join this mailing list, you may ignore this message. - -To subscribe to the mailing list, go to the following confirmation URL: - -{{ url }} - -This should take you directly to an email confirmation page. If it does not, -please copy and paste the full URL into your web browser's address box and -hit the "Enter" key on your keyboard. - -Thanks, - -{{ band }} -{{ band_url }} diff -r 25e00d1b99bf -r 63e4211628e1 mysite/templates/band/email_unsubscribe.txt --- a/mysite/templates/band/email_unsubscribe.txt Tue Feb 14 19:09:57 2012 -0600 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,19 +0,0 @@ -Hello, - -We have received a request for this email address to unsubscribe from the mailing list -for {{ band }}. In order for us to process this request, we need confirmation from you. - -If you did not request to unsubscribe from this mailing list, you may ignore this message. - -To unsubscribe from the mailing list, go to the following confirmation URL: - -{{ url }} - -This should take you directly to an email confirmation page. If it does not, -please copy and paste the full URL into your web browser's address box and -hit the "Enter" key on your keyboard. - -Thanks, - -{{ band }} -{{ band_url }} diff -r 25e00d1b99bf -r 63e4211628e1 mysite/templates/band/flyers.html --- a/mysite/templates/band/flyers.html Tue Feb 14 19:09:57 2012 -0600 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,20 +0,0 @@ -{% extends 'band/base.html' %} -{% block title %}The Madeira | Flyer Gallery{% endblock %} -{% block content %} -

Show Flyer Gallery

-{% if gigs %} -
- {% for gig in gigs %} -

- {% if gig.title %} - {{ gig.title }} - {% else %} - {{ gig.date|date: - {% endif %} -

- {% endfor %} -
-{% else %} -No flyers available at this time. -{% endif %} -{% endblock %} diff -r 25e00d1b99bf -r 63e4211628e1 mysite/templates/band/gigs.html --- a/mysite/templates/band/gigs.html Tue Feb 14 19:09:57 2012 -0600 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,166 +0,0 @@ -{% extends 'band/base.html' %} -{% load url from future %} -{% block title %}The Madeira | Shows{% endblock %} -{% block custom_css %} - -{% endblock %} -{% block custom_js %} - - - -{% endblock %} -{% block content %} -

Show Dates

- -

Upcoming Shows

-{% if upcoming %} - {% for show in upcoming %} -

- {% if show.flyer %} - - - {{ show.flyer.caption }} - {% endif %} - {{ show.date|date:"F d, Y" }} - {% if show.time %}{{ show.time|time:"h:i A" }}{% endif %}
- - {% if show.title and show.url %} - {{ show.title }}
- {% else %} - {% if show.title %} - {{ show.title }}
- {% endif %} - {% endif %} - - {% if show.venue %} - {% if show.venue.url %} - {{ show.venue.name }}, - {% else %} - {{ show.venue }}, - {% endif %} - {% if show.venue.address %} - {{ show.venue.address }}, - {% endif %} - {% if show.venue.city.state %} - {{ show.venue.city.name }}, {{ show.venue.city.state.name }} - {% else %} - {{ show.venue.city.name }} - {% endif %} - {% ifnotequal show.venue.city.country.name "USA" %} - {{ show.venue.city.country.name }} - {% endifnotequal %} -
- {% if show.venue.phone %} - {{ show.venue.phone }} -
- {% endif %} - {% endif %} - - {% if show.bands_ %} - With: - {% for band in show.bands_ %} - {% if band.url %} - {{ band.name }} - {% else %} - {{ band.name }} - {% endif %} - {% if not forloop.last %} - • - {% endif %} - {% endfor %} -
- {% endif %} - - {% if show.notes %} - {{ show.notes|safe }} - {% endif %} -

- {% endfor %} -{% else %} -None at this time. -{% endif %} -
- -{% if flyerGigs %} -
-

Flyers

-
- {% for gig in flyerGigs %} -
- - - -
{{ gig.venue.name}}, {{ gig.date|date:"F 'y" }}
- - {{ gig.date|date: -
-
- {% endfor %} -
-
-

To see all our flyers in full size, check out our show flyer gallery.

-
-{% endif %} - -{% if previous %} -

Previous Shows

-
- - - {% for show in previous %} - - - - - - {% endfor %} -
DateVenueBands
{{ show.date|date:"M d, Y" }} - {% if show.title and show.url %} - {{ show.title }}, - {% else %} - {% if show.title %} - {{ show.title }}, - {% endif %} - {% endif %} - {% if show.venue.url %} - {{ show.venue.name }}, - {% else %} - {{ show.venue.name }}, - {% endif %} - {{ show.venue.city.name }}, {{ show.venue.city.state.abbrev }} - {% ifnotequal show.venue.city.country.name "USA" %} - {{ show.venue.city.country.name }} - {% endifnotequal %} - - {% for band in show.bands_ %} - {% if band.url %} - {{ band.name }} - {% else %} - {{ band.name }} - {% endif %} - {% if not forloop.last %} - • - {% endif %} - {% endfor %} -
-
-{% endif %} - -{% if stats %} -

Past Show Statistics

- - - - - - - -
Number of shows:{{ stats.count }}
Number of unique venues:{{ stats.venues }}
Number of unique cities:{{ stats.cities }}
Number of unique states:{{ stats.states }}
Number of unique countries:{{ stats.countries }}
Number of unique bands:{{ stats.bands }}
-{% endif %} - -{% endblock %} diff -r 25e00d1b99bf -r 63e4211628e1 mysite/templates/band/index.html --- a/mysite/templates/band/index.html Tue Feb 14 19:09:57 2012 -0600 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,90 +0,0 @@ -{% extends 'band/base.html' %} -{% load url from future %} -{% block title %}The Madeira{% endblock %} -{% block custom_css %} - -{% endblock %} -{% block custom_js %} - - - -{% endblock %} -{% load markup %} -{% block content %} -

The Madeira

-{{ config.intro_photo.title }} -{{ config.intro_text|textile }} -
- -{% if upcomingDates %} -
-

Upcoming Shows...

- -
-{% for gig in upcomingDates %} - {% if gig.flyer %} - - {% endif %} -{% endfor %} -
- - {{ gig.flyer.caption }} -
{{ gig.flyer.caption }}
-
- -
    -{% for show in upcomingDates %} -
  • {{ show.date|date:"l, F d" }}: {{ show.venue.name }}, {{ show.venue.city.name }}{% if show.venue.city.state %}, {{ show.venue.city.state.name }} -{% endif %} -{% ifnotequal show.venue.city.country.name "USA" %} -{{ show.venue.city.country.name }} -{% endifnotequal %} -
  • -{% endfor %} -
-
See all upcoming shows...
-
-{% endif %} - -
-
-

Sandstorm from Sound of the Surf

- -

Another clip from the upcoming film Sound of the Surf has just been released, and it is our performance of Sandstorm! This movie cannot come out soon enough!

-
-
-
-
-

New Song Preview!

- -

Check out this set of 6 videos from our show at Mahogany's in February. Five of the songs are new originals slated for our new album! Video courtesy of TikiTim.

-
-
- -
-
- - - - - - - - - - -

The Madeira Releases:

- Carpe Noctem Cover -
Available Now: Carpe Noctem!
-
Sandstorm CD Cover
Ruins EP Cover
Sandstorm
Ruins
-
-
- -{% endblock %} diff -r 25e00d1b99bf -r 63e4211628e1 mysite/templates/band/mail.html --- a/mysite/templates/band/mail.html Tue Feb 14 19:09:57 2012 -0600 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,21 +0,0 @@ -{% extends 'band/base.html' %} -{% block title %}The Madeira | Mailing List{% endblock %} -{% block content %} -

Madeira Mailing List

-

Get on the Madeira mailing list to receive updates about upcoming shows, releases, and website updates. -This is a low volume list. We do not share your email address with anyone.

-
Mailing List -
{% csrf_token %} - - {% for field in form %} - - - - - {% endfor %} - -
{{ field.label_tag }}{% if field.field.required %}*{% endif %}:{{ field }} - {% if field.errors %}{{ field.errors }}{% endif %}
-
-
-{% endblock %} diff -r 25e00d1b99bf -r 63e4211628e1 mysite/templates/band/mail_confirm.html --- a/mysite/templates/band/mail_confirm.html Tue Feb 14 19:09:57 2012 -0600 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,6 +0,0 @@ -{% extends 'band/base.html' %} -{% block title %}The Madeira | Mailing List Confirmation{% endblock %} -{% block content %} -

Madeira Mailing List Confirmation

-

Your email address, {{ email }}, has been successfully {{ action }}.

-{% endblock %} diff -r 25e00d1b99bf -r 63e4211628e1 mysite/templates/band/mail_not_found.html --- a/mysite/templates/band/mail_not_found.html Tue Feb 14 19:09:57 2012 -0600 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,8 +0,0 @@ -{% extends 'band/base.html' %} -{% load url from future %} -{% block title %}The Madeira | Mailing List Confirmation{% endblock %} -{% block content %} -

Madeira Mailing List

-

Sorry, we did not find that email address in our database.

-

Back to contact page.

-{% endblock %} diff -r 25e00d1b99bf -r 63e4211628e1 mysite/templates/band/mail_thanks.html --- a/mysite/templates/band/mail_thanks.html Tue Feb 14 19:09:57 2012 -0600 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,9 +0,0 @@ -{% extends 'band/base.html' %} -{% block title %}The Madeira | Mailing List Confirmation{% endblock %} -{% block content %} -

Madeira Mailing List

-

Thanks for subscribing to our email list! You should shortly receive a confirmation email -with instructions on how to complete the subscription process.

-

Please check your spam folders for this email. Sometimes it ends up in there. -Thanks.

-{% endblock %} diff -r 25e00d1b99bf -r 63e4211628e1 mysite/templates/band/mail_unsubscribe.html --- a/mysite/templates/band/mail_unsubscribe.html Tue Feb 14 19:09:57 2012 -0600 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,8 +0,0 @@ -{% extends 'band/base.html' %} -{% block title %}The Madeira | Mailing List Confirmation{% endblock %} -{% block content %} -

Madeira Mailing List

-

We're sorry to see you unsubscribing from our email list! You should shortly receive a confirmation email -with instructions on how to complete the removal process. Please check your spam folders for -this email.

-{% endblock %} diff -r 25e00d1b99bf -r 63e4211628e1 mysite/templates/band/news.html --- a/mysite/templates/band/news.html Tue Feb 14 19:09:57 2012 -0600 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,31 +0,0 @@ -{% extends 'band/base.html' %} -{% load markup %} -{% block title %}The Madeira | News{% endblock %} -{% block content %} -

News

-{% if news %} - {% for story in news %} -

{{ story.date|date:"F d, Y" }}  - {% if story.title %} - • {{ story.title }} - {% endif %} -

-
- {% if story.photo %} - {{ story.photo_caption }} - {% endif %} - {% if story.markup_enabled %} - {{ story.text|textile }} - {% else %} - {{ story.text|safe|linebreaks }} - {% endif %} - {% if story.author %} -

-- {{ story.author }}

- {% endif %} -
- {% endfor %} -{% else %} -No news at this time. -{% endif %} -{% endblock %} diff -r 25e00d1b99bf -r 63e4211628e1 mysite/templates/band/photo_detail.html --- a/mysite/templates/band/photo_detail.html Tue Feb 14 19:09:57 2012 -0600 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,29 +0,0 @@ -{% extends 'band/base.html' %} -{% load url from future %} -{% load markup %} -{% block title %}The Madeira | Photos: {{ gallery.title }}{% endblock %} -{% block custom_css %} - -{% endblock %} -{% block custom_js %} - - - -{% endblock %} -{% block content %} -

Madeira Photos: {{ gallery.title }}

-{{ gallery.description|textile }} - -
-{% for photo in photos %} - - {{ photo.caption }} -{% endfor %} -
-
Photo gallery index
- -{% endblock %} diff -r 25e00d1b99bf -r 63e4211628e1 mysite/templates/band/photos.html --- a/mysite/templates/band/photos.html Tue Feb 14 19:09:57 2012 -0600 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,36 +0,0 @@ -{% extends 'band/base.html' %} -{% load url from future %} -{% block title %}The Madeira | Photo Galleries{% endblock %} -{% block custom_css %} - -{% endblock %} -{% block custom_js %} - - - -{% endblock %} -{% block content %} -

Madeira Photo Galleries

-{% if galleries %} - -{% else %} -No photo galleries available at this time. -{% endif %} -{% if randomPhotos %} -
-

Random Photos:

- {% for photo in randomPhotos %} - - {{ photo.caption }} - {% endfor %} -
-{% endif %} -{% endblock %} diff -r 25e00d1b99bf -r 63e4211628e1 mysite/templates/band/press.html --- a/mysite/templates/band/press.html Tue Feb 14 19:09:57 2012 -0600 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,44 +0,0 @@ -{% extends 'band/base.html' %} -{% load markup %} -{% block title %}The Madeira | Press{% endblock %} -{% block content %} -

Madeira Press, Articles, & Reviews

-{% if articles %} -   -

Contents

- - - {% for article in articles %} -   -

{{ article.title }}

- {% if article.markup_enabled %} - {{ article.text|textile }} - {% else %} - {{ article.text|safe|linebreaks }} - {% endif %} -
- {{ article.source|safe|linebreaks }} -
- {% if article.url %} - Original article - {% endif %} - {% if article.pdf and article.url %} - | - {% endif %} - {% if article.pdf %} - Original article as PDF - - Adobe Reader - {% endif %} -

Top

- {% endfor %} - -{% else %} -No articles at this time. -{% endif %} -{% endblock %} diff -r 25e00d1b99bf -r 63e4211628e1 mysite/templates/band/press_detail.html --- a/mysite/templates/band/press_detail.html Tue Feb 14 19:09:57 2012 -0600 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,29 +0,0 @@ -{% extends 'band/base.html' %} -{% load url from future %} -{% load markup %} -{% block title %}The Madeira | Press{% endblock %} -{% block content %} -

Madeira Press, Articles, & Reviews

-

{{ article.title }}

-{% if article.markup_enabled %} - {{ article.text|textile }} -{% else %} - {{ article.text|safe|linebreaks }} -{% endif %} -
-{{ article.source|safe|linebreaks }} -
-{% if article.url %} -Original article -{% endif %} -{% if article.pdf and article.url %} -| -{% endif %} -{% if article.pdf %} -Original article as PDF - - Adobe Reader -{% endif %} -

Press index

-{% endblock %} diff -r 25e00d1b99bf -r 63e4211628e1 mysite/templates/band/songs.html --- a/mysite/templates/band/songs.html Tue Feb 14 19:09:57 2012 -0600 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,20 +0,0 @@ -{% extends 'band/base.html' %} -{% block title %}The Madeira | Songs{% endblock %} -{% block content %} -

Madeira Songs

-{% if mp3Sets %} -

Check out some Madeira MP3 downloads!

- {% for set in mp3Sets %} -

{{ set.title }}

- {{ set.text|safe|linebreaks }} -
    - {% for mp3 in set.mp3_set.all %} -
  • {{ mp3.title }} - ({{ mp3.file.size|filesizeformat }}){% if mp3.desc %} - {{ mp3.desc }}{% endif %}
  • - {% endfor %} -
- {% endfor %} -{% else %} -No downloads available at this time. -{% endif %} -{% endblock %} diff -r 25e00d1b99bf -r 63e4211628e1 mysite/templates/band/video_detail.html --- a/mysite/templates/band/video_detail.html Tue Feb 14 19:09:57 2012 -0600 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,18 +0,0 @@ -{% extends 'band/base.html' %} -{% load url from future %} -{% block title %}The Madeira | Videos: {{ vidset.title }}{% endblock %} -{% block content %} -

Madeira Videos: {{ vidset.title }}

-{{ vidset.text|safe|linebreaks }} - -
- -{% for video in vidset.video_set.all %} - -{% endfor %} -
{{ video.title }}{{ video.embed_code|safe }}
-
-
-
Videos index
- -{% endblock %} diff -r 25e00d1b99bf -r 63e4211628e1 mysite/templates/band/videos.html --- a/mysite/templates/band/videos.html Tue Feb 14 19:09:57 2012 -0600 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,15 +0,0 @@ -{% extends 'band/base.html' %} -{% load url from future %} -{% block title %}The Madeira | Videos{% endblock %} -{% block content %} -

Madeira Videos

-{% if vidsets %} - -{% else %} -No videos available at this time. -{% endif %} -{% endblock %} diff -r 25e00d1b99bf -r 63e4211628e1 mysite/templates/flatpages/default.html --- a/mysite/templates/flatpages/default.html Tue Feb 14 19:09:57 2012 -0600 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,5 +0,0 @@ -{% extends 'band/base.html' %} -{% block title %}The Madeira | {{ flatpage.title }}{% endblock %} -{% block content %} -{{ flatpage.content }} -{% endblock %} diff -r 25e00d1b99bf -r 63e4211628e1 mysite/urls.py --- a/mysite/urls.py Tue Feb 14 19:09:57 2012 -0600 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,17 +0,0 @@ -from django.conf.urls.defaults import * -from django.contrib import admin -from django.conf import settings - -admin.autodiscover() - -urlpatterns = patterns('', - (r'^', include('band.urls')), - (r'^admin/doc/', include('django.contrib.admindocs.urls')), - (r'^admin/', include(admin.site.urls)), - (r'^photologue/', include('photologue.urls')), -) - -if settings.DEBUG: - urlpatterns += patterns('', - (r'^media/(?P.*)$', 'django.views.static.serve', {'document_root': settings.MEDIA_ROOT}), - )