├── photoblog ├── __init__.py ├── migrations │ ├── __init__.py │ ├── 0002_auto__add_field_photo_description_as_meta_tag_only.py │ └── 0001_initial.py ├── templatetags │ ├── __init__.py │ └── photoblog.py ├── urls.py ├── util.py ├── templates │ └── photoblog │ │ ├── base.html │ │ ├── archive.html │ │ └── index.html ├── admin.py ├── static │ ├── photoblog.css │ └── photoblog.less ├── views.py └── models.py ├── todo.md ├── docs └── settings.md ├── README.md └── setup.py /photoblog/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- -------------------------------------------------------------------------------- /photoblog/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- -------------------------------------------------------------------------------- /photoblog/templatetags/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- -------------------------------------------------------------------------------- /todo.md: -------------------------------------------------------------------------------- 1 | # Todo list 2 | * Create views 3 | * Frontpage - with n images 4 | * Add comments support 5 | * Add hdpi-related warnings 6 | * Drink :coffee: 7 | -------------------------------------------------------------------------------- /photoblog/templatetags/photoblog.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | from django.template import Library 3 | 4 | register = Library() 5 | 6 | @register.filter 7 | def format_exif_date(value, arg=None): 8 | date = value.split(' ')[0].split(':') 9 | return "%s.%s.%s" % (date[2], date[1], date[0]) -------------------------------------------------------------------------------- /photoblog/urls.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from django.conf.urls import patterns, url 3 | 4 | urlpatterns = patterns('photoblog.views', 5 | url(r'^$', 'index', name='photoblog_index'), 6 | url(r'^archive/$', 'archive', name='photoblog_index'), 7 | url(r'^(?P\d+)/$', 'view_photo', name='photoblog_view_photo'), 8 | ) -------------------------------------------------------------------------------- /docs/settings.md: -------------------------------------------------------------------------------- 1 | # Settings 2 | 3 | ## PHOTOBLOG_DATE_AS_TITLE 4 | **Default:** *False* 5 | 6 | If *PHOTOBLOG_DATE_AS_TITLE* is set to True. Publish date will be showed instead of the title. 7 | 8 | ## PHOTOBLOG_EXTRA_EXIF 9 | **Default:** *False* 10 | 11 | If it is set to *True* shutterspeed, aperture and iso will be shown on view_photo and and index. 12 | 13 | ## PHOTOBLOG_ARCHIVE_PER_PAGE 14 | **Default:** 30 15 | 16 | Number of photos per page in the archive -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # django-photoblog 2 | ## Install 3 | git clone git@github.com:relekang/django-photoblog.git 4 | cd django-photoblog 5 | python setup.py install 6 | 7 | #### Add photoblog to installed apps 8 | INSTALLED_APPS = ( 9 | ... 10 | 'photoblog', 11 | ) 12 | 13 | #### Add to urlspattern in urls.py 14 | url(r'^photos/', include('photoblog.urls')), 15 | 16 | ## Dependecies 17 | * sorl-thumbnail 18 | 19 | 20 | ## This is work in progress 21 | Check out the [todo](https://github.com/relekang/django-photoblog/blob/master/todo.md)-list or 22 | add a issue with a feature or bug. -------------------------------------------------------------------------------- /photoblog/util.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from django.core.cache import cache 3 | from django.core.urlresolvers import reverse 4 | from django.http import HttpRequest 5 | from django.utils.cache import get_cache_key 6 | 7 | def expire_page_cache(view, args=None): 8 | """ 9 | Removes cache created by cache_page functionality. 10 | Parameters are used as they are in reverse() 11 | """ 12 | 13 | if args is None: 14 | path = reverse(view) 15 | else: 16 | path = reverse(view, args=args) 17 | 18 | request = HttpRequest() 19 | request.path = path 20 | key = get_cache_key(request) 21 | if cache.has_key(key): 22 | cache.delete(key) -------------------------------------------------------------------------------- /photoblog/templates/photoblog/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {% block title %}Photoblog{% endblock %} 7 | {% block extra_head %}{% endblock %} 8 | 9 | 10 |
11 | {% block content %}{% endblock %} 12 |
13 | {% block js %} 14 | 15 | 16 | {% endblock %} 17 | 18 | -------------------------------------------------------------------------------- /photoblog/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from photoblog.models import Category, Location, Photo, Tag 3 | 4 | 5 | class CategoryAdmin (admin.ModelAdmin): 6 | list_display = ('name', 'number_of_photos') 7 | 8 | class LocationAdmin (admin.ModelAdmin): 9 | list_display = ('name', 'number_of_photos') 10 | 11 | class PhotoAdmin (admin.ModelAdmin): 12 | list_display = ( 13 | 'thumb_small_as_html', 14 | 'title', 15 | 'date_published', 16 | 'category', 17 | 'location', 18 | 'list_of_tags', 19 | 'description_as_meta_tag_only', 20 | ) 21 | list_filter = ('category', 'location', 'tags') 22 | date_hierarchy = 'date_published' 23 | 24 | class TagAdmin (admin.ModelAdmin): 25 | list_display = ('title', 'number_of_photos') 26 | 27 | admin.site.register(Category, CategoryAdmin) 28 | admin.site.register(Location, LocationAdmin) 29 | admin.site.register(Photo, PhotoAdmin) 30 | admin.site.register(Tag, TagAdmin) -------------------------------------------------------------------------------- /photoblog/templates/photoblog/archive.html: -------------------------------------------------------------------------------- 1 | {% extends "photoblog/base.html" %} 2 | 3 | {% block title %}Archive - {{ block.super }}{% endblock %} 4 | 5 | {% block content %} 6 | 7 |
8 |

Archive

9 |
10 | {% for photo in photos %} 11 | {% with thumb=photo.thumb_small %} 12 | 20 | {% endwith %} 21 | {% endfor %} 22 |
23 | {% if photos.paginator.num_pages > 1 %} 24 | 35 | {% endif %} 36 |
37 | {% endblock %} -------------------------------------------------------------------------------- /photoblog/static/photoblog.css: -------------------------------------------------------------------------------- 1 | #photoblog .one-photo{margin:50px auto;}#photoblog .one-photo img{margin:0 auto;}#photoblog .one-photo img.portrait{height:800px;width:auto;} 2 | #photoblog .one-photo img.landscape{height:auto;width:800px;} 3 | #photoblog .one-photo img.square{width:800px;} 4 | #photoblog .one-photo .photo-meta{max-width:800px;margin:4px auto;font-size:14px;font-family:"Helvetica Neue",Helvetica,sans-serif;} 5 | #photoblog .archive{width:200px;margin:0 auto;}#photoblog .archive .photo{width:200px;height:200px;overflow:hidden;float:left;position:relative;}#photoblog .archive .photo .photo-meta{width:100%;display:none;position:absolute;bottom:0;text-align:right;background:rgba(0, 0, 0, 0.5);color:white;}#photoblog .archive .photo .photo-meta .lead{margin:4px 8px;} 6 | #photoblog .archive .photo:hover .photo-meta{display:block;} 7 | .centered-text{text-align:center;} 8 | @media screen and (max-width:600px){#photoblog .one-photo img{margin:-4px;}#photoblog .one-photo img.portrait{width:100%;height:auto !important;} #photoblog .one-photo .photo-meta{width:100%;}}@media screen and (max-height:900px){#photoblog .one-photo img.portrait{height:600px;} #photoblog .one-photo img.square{width:600px;} #photoblog .one-photo .photo-meta{max-width:600px;}}@media screen and (min-width:420px){#photoblog .archive{width:200px;}}@media screen and (min-width:620px){#photoblog .archive{width:400px;}}@media screen and (min-width:820px){#photoblog .archive{width:600px;}}@media screen and (min-width:1020px){#photoblog .archive{width:800px;}}@media screen and (min-width:1220px){#photoblog .archive{width:1000px;}}@media screen and (min-width:1420px){#photoblog .archive{width:1200px;}}@media screen and (min-width:1620px){#photoblog .archive{width:1400px;}}@media screen and (min-width:1820px){#photoblog .archive{width:1600px;}}@media screen and (min-width:2020px){#photoblog .archive{width:1800px;}}@media screen and (min-width:2220px){#photoblog .archive{width:2000px;}}@media screen and (min-width:2420px){#photoblog .archive{width:2200px;}} 9 | -------------------------------------------------------------------------------- /photoblog/views.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from datetime import datetime 3 | from django.conf import settings 4 | from django.core.paginator import Paginator, PageNotAnInteger, EmptyPage 5 | from django.http import Http404 6 | from django.shortcuts import render, get_object_or_404 7 | from django.views.decorators.cache import cache_page 8 | from photoblog.models import Photo 9 | 10 | def index (request): 11 | try: 12 | photo = Photo.objects.filter(date_published__lte=datetime.now()).prefetch_related('tags').select_related('category', 'location').order_by('-date_published')[0] 13 | except: 14 | raise Http404 15 | 16 | return render(request, 'photoblog/index.html', { 17 | 'photo': photo, 18 | 'index': True, 19 | 'date_as_title': getattr(settings, 'PHOTOBLOG_DATE_AS_TITLE', False), 20 | 'extra_exif': getattr(settings, 'PHOTOBLOG_EXTRA_EXIF', False), 21 | }) 22 | 23 | @cache_page 24 | def view_photo (request, id): 25 | photo = get_object_or_404( 26 | Photo.objects.prefetch_related('tags').select_related('category', 'location'), 27 | pk=id, 28 | date_published__lte=datetime.now() 29 | ) 30 | 31 | return render(request, 'photoblog/index.html', { 32 | 'photo': photo, 33 | 'date_as_title': getattr(settings, 'PHOTOBLOG_DATE_AS_TITLE', False), 34 | 'extra_exif': getattr(settings, 'PHOTOBLOG_EXTRA_EXIF', False), 35 | }) 36 | 37 | def archive (request): 38 | photos = Photo.objects.filter(date_published__lte=datetime.now()).prefetch_related('tags').select_related('category', 'location') 39 | 40 | per_page = getattr(settings, 'PHOTOBLOG_ARCHIVE_PER_PAGE', 30) 41 | orphans = per_page/3 42 | 43 | paginator = Paginator(photos, per_page=per_page, orphans=orphans) 44 | 45 | page = request.GET.get('page') 46 | try: 47 | photos = paginator.page(page) 48 | except PageNotAnInteger: 49 | photos = paginator.page(1) 50 | except EmptyPage: 51 | photos = paginator.page(paginator.num_pages) 52 | 53 | return render(request, 'photoblog/archive.html', { 54 | 'photos': photos, 55 | }) -------------------------------------------------------------------------------- /photoblog/migrations/0002_auto__add_field_photo_description_as_meta_tag_only.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import datetime 3 | from south.db import db 4 | from south.v2 import SchemaMigration 5 | from django.db import models 6 | 7 | 8 | class Migration(SchemaMigration): 9 | 10 | def forwards(self, orm): 11 | # Adding field 'Photo.description_as_meta_tag_only' 12 | db.add_column('photoblog_photo', 'description_as_meta_tag_only', 13 | self.gf('django.db.models.fields.BooleanField')(default=True), 14 | keep_default=False) 15 | 16 | 17 | def backwards(self, orm): 18 | # Deleting field 'Photo.description_as_meta_tag_only' 19 | db.delete_column('photoblog_photo', 'description_as_meta_tag_only') 20 | 21 | 22 | models = { 23 | 'photoblog.category': { 24 | 'Meta': {'object_name': 'Category'}, 25 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 26 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) 27 | }, 28 | 'photoblog.location': { 29 | 'Meta': {'object_name': 'Location'}, 30 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 31 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '80'}) 32 | }, 33 | 'photoblog.photo': { 34 | 'Meta': {'object_name': 'Photo'}, 35 | 'category': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'photos'", 'null': 'True', 'to': "orm['photoblog.Category']"}), 36 | 'date_published': ('django.db.models.fields.DateTimeField', [], {}), 37 | 'date_uploaded': ('django.db.models.fields.DateField', [], {'auto_now_add': 'True', 'blank': 'True'}), 38 | 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}), 39 | 'description_as_meta_tag_only': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), 40 | 'file': ('django.db.models.fields.files.ImageField', [], {'max_length': '100'}), 41 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 42 | 'location': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'photos'", 'null': 'True', 'to': "orm['photoblog.Location']"}), 43 | 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'photos'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['photoblog.Tag']"}), 44 | 'title': ('django.db.models.fields.CharField', [], {'max_length': '200', 'blank': 'True'}) 45 | }, 46 | 'photoblog.tag': { 47 | 'Meta': {'object_name': 'Tag'}, 48 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 49 | 'title': ('django.db.models.fields.CharField', [], {'max_length': '80'}) 50 | } 51 | } 52 | 53 | complete_apps = ['photoblog'] -------------------------------------------------------------------------------- /photoblog/templates/photoblog/index.html: -------------------------------------------------------------------------------- 1 | {% extends "photoblog/base.html" %} 2 | {% load photoblog %} 3 | 4 | {% block title %}{% if photo.title %}{{ photo.title }} - {% endif %}{{ block.super }}{% endblock %} 5 | 6 | {% block extra_head %} 7 | {{ block.super }} 8 | {% if photo.description and not index %} 9 | 10 | {% endif %} 11 | {% endblock %} 12 | 13 | {% block content %} 14 | 15 | {% if photo %} 16 |
17 | {% with thumb=photo.thumb_big %} 18 | {{ photo.title }} 20 |
21 | {% if date_as_title %} 22 |

{{ photo.date_published|date:"d.m.y" }}

23 | {% elif photo.title %} 24 |

{{ photo.title }}

25 | {% endif %} 26 | 27 | {% if photo.description and not photo.description_as_meta_tag_only %} 28 |

{{ photo.description }}

29 | {% endif %} 30 | 31 |

32 | {% if photo.location %} {{ photo.location.name }}{% endif %} 33 | {% if photo.exif.Model %} 34 | {{ photo.exif.Model }} 35 | {% if photo.exif.42036 %} - {{ photo.exif.42036 }}{% endif %} 36 | {% endif %} 37 | {% if photo.exif.DateTimeOriginal %} {{ photo.exif.DateTimeOriginal|format_exif_date }} {% endif %} 38 |

39 | {% if extra_exif %} 40 |

41 | {% if photo.exif.ExposureTime %}Shutterspeed {% if photo.exif.ExposureTime.0 == 1 %}1/{% endif %}{{ photo.exif.ExposureTime.1 }}{% endif %} 42 | {% if photo.exif.FNumber %}F-number: {{ photo.exif.FNumber.0 }} {% endif %} 43 | {% if photo.exif.ISOSpeedRatings %}ISO: {{ photo.exif.ISOSpeedRatings }} {% endif %} 44 |

45 | {% endif %} 46 | 47 | {% if photo.tags.all %} 48 |

49 | Tags: 50 | {% for tag in photo.tags.all %}{{ tag.title }}{% if forloop.last %}.{% else %}, {% endif %}{% endfor %} 51 |

52 | {% endif %} 53 |
54 | {% endwith %} 55 |
56 | {% endif %} 57 | {% endblock %} 58 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """ 2 | Based on django's setup.py 3 | """ 4 | 5 | from distutils.core import setup 6 | from distutils.command.install_data import install_data 7 | from distutils.command.install import INSTALL_SCHEMES 8 | from distutils.sysconfig import get_python_lib 9 | import os 10 | import sys 11 | 12 | 13 | class osx_install_data(install_data): 14 | # On MacOS, the platform-specific lib dir is /System/Library/Framework/Python/.../ 15 | # which is wrong. Python 2.5 supplied with MacOS 10.5 has an Apple-specific fix 16 | # for this in distutils.command.install_data#306. It fixes install_lib but not 17 | # install_data, which is why we roll our own install_data class. 18 | 19 | def finalize_options(self): 20 | # By the time finalize_options is called, install.install_lib is set to the 21 | # fixed directory, so we set the installdir to install_lib. The 22 | # install_data class uses ('install_data', 'install_dir') instead. 23 | self.set_undefined_options('install', ('install_lib', 'install_dir')) 24 | install_data.finalize_options(self) 25 | 26 | if sys.platform == "darwin": 27 | cmdclasses = {'install_data': osx_install_data} 28 | else: 29 | cmdclasses = {'install_data': install_data} 30 | 31 | def fullsplit(path, result=None): 32 | """ 33 | Split a pathname into components (the opposite of os.path.join) in a 34 | platform-neutral way. 35 | """ 36 | if result is None: 37 | result = [] 38 | head, tail = os.path.split(path) 39 | if head == '': 40 | return [tail] + result 41 | if head == path: 42 | return result 43 | return fullsplit(head, [tail] + result) 44 | 45 | # Tell distutils not to put the data_files in platform-specific installation 46 | # locations. See here for an explanation: 47 | # http://groups.google.com/group/comp.lang.python/browse_thread/thread/35ec7b2fed36eaec/2105ee4d9e8042cb 48 | for scheme in INSTALL_SCHEMES.values(): 49 | scheme['data'] = scheme['purelib'] 50 | 51 | # Compile the list of packages available, because distutils doesn't have 52 | # an easy way to do this. 53 | packages, data_files = [], [] 54 | root_dir = os.path.dirname(__file__) 55 | if root_dir != '': 56 | os.chdir(root_dir) 57 | project_dir = 'photoblog' 58 | 59 | for dirpath, dirnames, filenames in os.walk(project_dir): 60 | # Ignore dirnames that start with '.' 61 | for i, dirname in enumerate(dirnames): 62 | if dirname.startswith('.'): del dirnames[i] 63 | if '__init__.py' in filenames: 64 | packages.append('.'.join(fullsplit(dirpath))) 65 | elif filenames: 66 | data_files.append([dirpath, [os.path.join(dirpath, f) for f in filenames]]) 67 | 68 | # Small hack for working with bdist_wininst. 69 | # See http://mail.python.org/pipermail/distutils-sig/2004-August/004134.html 70 | if len(sys.argv) > 1 and sys.argv[1] == 'bdist_wininst': 71 | for file_info in data_files: 72 | file_info[0] = '\\PURELIB\\%s' % file_info[0] 73 | 74 | 75 | setup( 76 | name = "django-photoblog", 77 | version = '0.1', 78 | url='http://github.com/relekang/django-photoblog', 79 | author = 'Rolf Erik Lekang', 80 | author_email = 'me@rolflekang.com', 81 | description = 'a photblog-app for django', 82 | packages = packages, 83 | cmdclass = cmdclasses, 84 | data_files = data_files, 85 | ) -------------------------------------------------------------------------------- /photoblog/static/photoblog.less: -------------------------------------------------------------------------------- 1 | #photoblog{ 2 | .one-photo{ 3 | margin: 50px auto; 4 | 5 | img{ 6 | margin: 0 auto; 7 | 8 | &.portrait{ 9 | height: 800px; 10 | width: auto; 11 | } 12 | &.landscape{ 13 | height: auto; 14 | width: 800px; 15 | } 16 | &.square{ 17 | width: 800px; 18 | } 19 | } 20 | .photo-meta{ 21 | max-width: 800px; 22 | margin: 4px auto; 23 | font-size: 14px; 24 | font-family: "Helvetica Neue", Helvetica, sans-serif; 25 | } 26 | } 27 | .archive{ 28 | width: 200px; 29 | margin: 0 auto; 30 | .photo{ 31 | width: 200px; 32 | height: 200px; 33 | overflow: hidden; 34 | float:left; 35 | position: relative; 36 | .photo-meta{ 37 | width: 100%; 38 | display: none; 39 | position: absolute; 40 | bottom: 0; 41 | text-align: right; 42 | background: rgba(0,0,0, 0.5); 43 | color: white; 44 | .lead{ 45 | margin: 4px 8px; 46 | } 47 | } 48 | &:hover{ 49 | .photo-meta{ 50 | display: block; 51 | } 52 | } 53 | } 54 | } 55 | } 56 | .centered-text{ 57 | text-align: center; 58 | } 59 | @media screen and (max-width: 600px){ 60 | #photoblog{ 61 | .one-photo{ 62 | img{ 63 | margin: -4px; 64 | 65 | &.portrait{ 66 | width: 100%; 67 | height: auto !important; 68 | } 69 | &.landscape{ 70 | 71 | } 72 | &.square{ 73 | 74 | } 75 | } 76 | .photo-meta{ 77 | width: 100%; 78 | } 79 | } 80 | } 81 | } 82 | @media screen and (max-height: 900px){ 83 | #photoblog{ 84 | .one-photo{ 85 | img{ 86 | &.portrait{ 87 | height: 600px; 88 | } 89 | &.landscape{ 90 | 91 | } 92 | &.square{ 93 | width: 600px; 94 | } 95 | } 96 | .photo-meta{ 97 | max-width: 600px; 98 | } 99 | } 100 | } 101 | } 102 | 103 | @media screen and (min-width: 420px){ 104 | #photoblog{ 105 | .archive{ 106 | width: 200px; 107 | } 108 | } 109 | } 110 | @media screen and (min-width: 620px){ 111 | #photoblog{ 112 | .archive{ 113 | width: 400px; 114 | } 115 | } 116 | } 117 | @media screen and (min-width: 820px){ 118 | #photoblog{ 119 | .archive{ 120 | width: 600px; 121 | } 122 | } 123 | } 124 | @media screen and (min-width: 1020px){ 125 | #photoblog{ 126 | .archive{ 127 | width: 800px; 128 | } 129 | } 130 | } 131 | @media screen and (min-width: 1220px){ 132 | #photoblog{ 133 | .archive{ 134 | width: 1000px; 135 | } 136 | } 137 | } 138 | @media screen and (min-width: 1420px){ 139 | #photoblog{ 140 | .archive{ 141 | width: 1200px; 142 | } 143 | } 144 | } 145 | @media screen and (min-width: 1620px){ 146 | #photoblog{ 147 | .archive{ 148 | width: 1400px; 149 | } 150 | } 151 | } 152 | @media screen and (min-width: 1820px){ 153 | #photoblog{ 154 | .archive{ 155 | width: 1600px; 156 | } 157 | } 158 | } 159 | @media screen and (min-width: 2020px){ 160 | #photoblog{ 161 | .archive{ 162 | width: 1800px; 163 | } 164 | } 165 | } 166 | @media screen and (min-width: 2220px){ 167 | #photoblog{ 168 | .archive{ 169 | width: 2000px; 170 | } 171 | } 172 | } 173 | @media screen and (min-width: 2420px){ 174 | #photoblog{ 175 | .archive{ 176 | width: 2200px; 177 | } 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /photoblog/models.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from django.core.cache import cache 3 | from django.core.urlresolvers import reverse 4 | from django.db import models 5 | from django.utils.safestring import mark_safe 6 | from django.utils.translation import ugettext_lazy as _ 7 | from sorl.thumbnail import get_thumbnail 8 | from sorl.thumbnail.templatetags.thumbnail import is_portrait 9 | from photoblog.util import expire_page_cache 10 | 11 | try: 12 | from PIL import Image 13 | from PIL.ExifTags import TAGS 14 | except ImportError: 15 | import Image 16 | from ExifTags import TAGS 17 | 18 | class Category (models.Model): 19 | name = models.CharField(max_length=50, verbose_name=_('name')) 20 | 21 | class Meta: 22 | verbose_name = 'Category' 23 | verbose_name_plural = 'Categories' 24 | 25 | def __unicode__(self): 26 | return self.name 27 | 28 | def number_of_photos(self): 29 | return self.photos.all().count() 30 | 31 | class Location (models.Model): 32 | name = models.CharField(max_length=80, verbose_name=_('name')) 33 | 34 | def __unicode__(self): 35 | return self.name 36 | 37 | def number_of_photos(self): 38 | return self.photos.all().count() 39 | 40 | class Tag (models.Model): 41 | title = models.CharField(max_length=80, verbose_name=_('name')) 42 | 43 | def __unicode__(self): 44 | return self.title 45 | 46 | def number_of_photos(self): 47 | return self.photos.all().count() 48 | 49 | class Photo (models.Model): 50 | title = models.CharField(max_length=200, blank=True, verbose_name=_('title')) 51 | date_uploaded = models.DateField(editable=False, auto_now_add=True) 52 | date_published = models.DateTimeField(verbose_name=_('publish date')) 53 | description = models.TextField(blank=True, verbose_name=_('description')) 54 | description_as_meta_tag_only = models.BooleanField(default=True, verbose_name=_('view description only as meta-tag')) 55 | file = models.ImageField(upload_to='photoblog/', verbose_name=_('file')) 56 | category = models.ForeignKey(Category, null=True, blank=True, related_name='photos', verbose_name=_('category')) 57 | location = models.ForeignKey(Location, null=True, blank=True, related_name='photos', verbose_name=_('location')) 58 | tags = models.ManyToManyField(Tag, null=True, blank=True, related_name='photos', verbose_name=_('tags')) 59 | 60 | class Meta: 61 | ordering = ('-date_published',) 62 | 63 | def save(self, *args, **kwargs): 64 | super(Photo, self).save(*args, **kwargs) 65 | expire_page_cache('photoblog_view_photo', args=[self.pk]) 66 | 67 | def get_absolute_url(self): 68 | return reverse('photoblog_view_photo', args=[self.pk]) 69 | 70 | def thumb(self, geometry_string, crop=None): 71 | thumb = get_thumbnail(self.file, geometry_string, crop=crop) 72 | return thumb 73 | 74 | def thumb_big(self): 75 | if is_portrait(self.file): 76 | return self.thumb(geometry_string='x800') 77 | else: 78 | return self.thumb(geometry_string='800') 79 | 80 | def thumb_medium(self): 81 | if is_portrait(self.file): 82 | return self.thumb(geometry_string='x400') 83 | else: 84 | return self.thumb(geometry_string='400') 85 | 86 | def thumb_small(self): 87 | return self.thumb(geometry_string='200x200', crop='center') 88 | 89 | def thumb_small_as_html(self): 90 | return mark_safe('' % self.thumb_small().url) 91 | thumb_small_as_html.allow_tags = True 92 | thumb_small_as_html.short_description = 'image' 93 | 94 | def exif(self): 95 | exif_data = cache.get(self.exif_cache_key()) 96 | if exif_data is None: 97 | data = {} 98 | try: 99 | photo = Image.open(self.file) 100 | info = photo._getexif() 101 | for tag, value in info.items(): 102 | decoded = TAGS.get(tag, tag) 103 | data[decoded] = value 104 | 105 | cache.set(self.exif_cache_key(), data) 106 | exif_data = data 107 | except: 108 | exif_data = None 109 | 110 | return exif_data 111 | 112 | def exif_cache_key(self): 113 | return "exif%s" % self.pk 114 | 115 | def list_of_tags(self): 116 | return ', '.join([tag.title for tag in self.tags.all()]) 117 | -------------------------------------------------------------------------------- /photoblog/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import datetime 3 | from south.db import db 4 | from south.v2 import SchemaMigration 5 | from django.db import models 6 | 7 | 8 | class Migration(SchemaMigration): 9 | 10 | def forwards(self, orm): 11 | # Adding model 'Category' 12 | db.create_table('photoblog_category', ( 13 | ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), 14 | ('name', self.gf('django.db.models.fields.CharField')(max_length=50)), 15 | )) 16 | db.send_create_signal('photoblog', ['Category']) 17 | 18 | # Adding model 'Location' 19 | db.create_table('photoblog_location', ( 20 | ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), 21 | ('name', self.gf('django.db.models.fields.CharField')(max_length=80)), 22 | )) 23 | db.send_create_signal('photoblog', ['Location']) 24 | 25 | # Adding model 'Tag' 26 | db.create_table('photoblog_tag', ( 27 | ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), 28 | ('title', self.gf('django.db.models.fields.CharField')(max_length=80)), 29 | )) 30 | db.send_create_signal('photoblog', ['Tag']) 31 | 32 | # Adding model 'Photo' 33 | db.create_table('photoblog_photo', ( 34 | ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), 35 | ('title', self.gf('django.db.models.fields.CharField')(max_length=200, blank=True)), 36 | ('date_uploaded', self.gf('django.db.models.fields.DateField')(auto_now_add=True, blank=True)), 37 | ('date_published', self.gf('django.db.models.fields.DateTimeField')()), 38 | ('description', self.gf('django.db.models.fields.TextField')(blank=True)), 39 | ('file', self.gf('django.db.models.fields.files.ImageField')(max_length=100)), 40 | ('category', self.gf('django.db.models.fields.related.ForeignKey')(blank=True, related_name='photos', null=True, to=orm['photoblog.Category'])), 41 | ('location', self.gf('django.db.models.fields.related.ForeignKey')(blank=True, related_name='photos', null=True, to=orm['photoblog.Location'])), 42 | )) 43 | db.send_create_signal('photoblog', ['Photo']) 44 | 45 | # Adding M2M table for field tags on 'Photo' 46 | db.create_table('photoblog_photo_tags', ( 47 | ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)), 48 | ('photo', models.ForeignKey(orm['photoblog.photo'], null=False)), 49 | ('tag', models.ForeignKey(orm['photoblog.tag'], null=False)) 50 | )) 51 | db.create_unique('photoblog_photo_tags', ['photo_id', 'tag_id']) 52 | 53 | 54 | def backwards(self, orm): 55 | # Deleting model 'Category' 56 | db.delete_table('photoblog_category') 57 | 58 | # Deleting model 'Location' 59 | db.delete_table('photoblog_location') 60 | 61 | # Deleting model 'Tag' 62 | db.delete_table('photoblog_tag') 63 | 64 | # Deleting model 'Photo' 65 | db.delete_table('photoblog_photo') 66 | 67 | # Removing M2M table for field tags on 'Photo' 68 | db.delete_table('photoblog_photo_tags') 69 | 70 | 71 | models = { 72 | 'photoblog.category': { 73 | 'Meta': {'object_name': 'Category'}, 74 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 75 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) 76 | }, 77 | 'photoblog.location': { 78 | 'Meta': {'object_name': 'Location'}, 79 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 80 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '80'}) 81 | }, 82 | 'photoblog.photo': { 83 | 'Meta': {'object_name': 'Photo'}, 84 | 'category': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'photos'", 'null': 'True', 'to': "orm['photoblog.Category']"}), 85 | 'date_published': ('django.db.models.fields.DateTimeField', [], {}), 86 | 'date_uploaded': ('django.db.models.fields.DateField', [], {'auto_now_add': 'True', 'blank': 'True'}), 87 | 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}), 88 | 'file': ('django.db.models.fields.files.ImageField', [], {'max_length': '100'}), 89 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 90 | 'location': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'photos'", 'null': 'True', 'to': "orm['photoblog.Location']"}), 91 | 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'photos'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['photoblog.Tag']"}), 92 | 'title': ('django.db.models.fields.CharField', [], {'max_length': '200', 'blank': 'True'}) 93 | }, 94 | 'photoblog.tag': { 95 | 'Meta': {'object_name': 'Tag'}, 96 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 97 | 'title': ('django.db.models.fields.CharField', [], {'max_length': '80'}) 98 | } 99 | } 100 | 101 | complete_apps = ['photoblog'] --------------------------------------------------------------------------------