From: Simon Cozens Date: Tue, 6 Feb 2007 18:23:49 +0000 (+0000) Subject: Memories as at 1.3 X-Git-Tag: 1.2+svn X-Git-Url: https://git.decadent.org.uk/gitweb/?p=memories.git;a=commitdiff_plain;h=b9d021d4af1018ac2c60640678c1b790251190e9 Memories as at 1.3 git-svn-id: http://svn.simon-cozens.org/memories/trunk@2 041978f6-d955-411f-a9d7-1d8545c9c3c7 --- diff --git a/COPYING b/COPYING new file mode 100644 index 0000000..86bd182 --- /dev/null +++ b/COPYING @@ -0,0 +1,6 @@ +This software is licensed under the GPL version 2 or higher, or the Perl +Artistic License, at your choice. The text of these two licenses can be +found in /usr/share/common-licenses/ on a Debian system, or at: + GPL: http://www.gnu.org/licenses/gpl.txt + Areistic: http://www.perl.com/pub/a/language/misc/Artistic.html + diff --git a/Changes b/Changes new file mode 100644 index 0000000..7d4c62b --- /dev/null +++ b/Changes @@ -0,0 +1,18 @@ +1.2 - Tue Jan 3 12:50:39 GMT 2006 + + - Slight calendar fix + - Tidy up display of large datasets. + - Rejig tag searching. + - Edit/delete photos + - Tag hierarchy computed over whole dataset, not just current page. + - RSS feeds available on most pages + +1.1 - Mon Oct 31 23:59:52 GMT 2005 + + - RSS feeds available from [%base%]/recent.rss + - Javascript suggestion of tags + - Calendaring and system tags + - Visual cleanups + - Concept of "enclosing tag" as well as "inside this tag" + - Minor fixes (zap cache on upload, URI escaping, etc.) + - Pager uses gifs diff --git a/MANIFEST b/MANIFEST new file mode 100644 index 0000000..04f7441 --- /dev/null +++ b/MANIFEST @@ -0,0 +1,70 @@ +Memories/Photo.pm +Memories/Tag.pm +Memories/DBI.pm +Memories/User.pm +Memories/Album.pm +Memories/Comment.pm +Memories/Config.pm +Memories/SystemTag.pm +Memories.pm +README +COPYING +templates/login_box +templates/nav +templates/macros +templates/frontpage +templates/static/memories.png +templates/static/memories.css +templates/static/1-s.gif +templates/static/1.gif +templates/static/2.gif +templates/static/3.gif +templates/static/4.gif +templates/static/5.gif +templates/static/6.gif +templates/static/7.gif +templates/static/8.gif +templates/static/9.gif +templates/static/2-s.gif +templates/static/3-s.gif +templates/static/4-s.gif +templates/static/5-s.gif +templates/static/6-s.gif +templates/static/7-s.gif +templates/static/8-s.gif +templates/static/9-s.gif +templates/static/upload.js +templates/header +templates/photo/recent +templates/photo/tagedit +templates/photo/comment +templates/photo/upload +templates/photo/view +templates/photo/exif +templates/rhs +templates/footer +templates/pager +templates/album_adder +templates/album/denied +templates/album/view +templates/album/list +templates/tagcloud +templates/custom/list +templates/tag/view +templates/tag/list +templates/tag/list_js +templates/user/view +templates/user/list +templates/user/album_list +templates/calendar +templates/memories.css +templates/memories.png +templates/picture/view +templates/systemtag/view +templates/systemtag/list +templates/recent.rss +memories.sql +MANIFEST +tag_dates +Changes +META.yml Module meta-data (added by MakeMaker) diff --git a/META.yml b/META.yml new file mode 100644 index 0000000..44f500b --- /dev/null +++ b/META.yml @@ -0,0 +1,24 @@ +# http://module-build.sourceforge.net/META-spec.html +#XXXXXXX This is a prototype!!! It will change in the future!!! XXXXX# +name: Memories +version: 1.2 +version_from: Memories.pm +installdirs: site +requires: + Cache::MemoryCache: 0 + Calendar::Simple: 0 + Class::DBI::Plugin::AbstractCount: 0 + Class::DBI::Plugin::Pager: 0 + File::Path: 0 + HTML::TagCloud: 0 + Image::ExifTool: 0 + Image::Imlib2: 0 + Image::Info: 0 + Maypole: 1.1 + Text::Balanced: 0 + Time::Piece: 0 + URI::Escape: 0 + XML::RSS: 0 + +distribution_type: module +generated_by: ExtUtils::MakeMaker version 6.17 diff --git a/Makefile.PL b/Makefile.PL new file mode 100644 index 0000000..5737856 --- /dev/null +++ b/Makefile.PL @@ -0,0 +1,21 @@ +use ExtUtils::MakeMaker; +WriteMakefile( + 'NAME' => 'Memories', + 'VERSION_FROM' => 'Memories.pm', # finds $VERSION + 'PREREQ_PM' => { + Maypole => 1.1, + HTML::TagCloud => 0, + URI::Escape => 0, + Calendar::Simple => 0, + XML::RSS => 0, + Time::Piece => 0, + Class::DBI::Plugin::Pager => 0, + Class::DBI::Plugin::AbstractCount => 0, + Cache::MemoryCache => 0, + Image::Info => 0, + Image::ExifTool => 0, + File::Path => 0, + Image::Imlib2 => 0, + Text::Balanced => 0, + } +); diff --git a/Memories/Album.pm b/Memories/Album.pm new file mode 100644 index 0000000..b4f869d --- /dev/null +++ b/Memories/Album.pm @@ -0,0 +1,77 @@ +package Memories::Album; +use base qw(Memories::DBI Maypole::Model::CDBI::Plain); +__PACKAGE__->columns(Essential => qw/id name user privacy/); +Memories::Album->has_a(user => "Memories::User"); +Memories::User->has_many(albums => "Memories::Album"); + +sub view :Exported { + my ($self, $r) = @_; + my $album = $r->objects->[0]; + if ($album->privacy && $album->user != $r->user) { + $r->template("denied"); return; + } + $r->template_args->{photos} = [ $album->photos ]; +} + +sub list :Exported { + my ($self, $r) = @_; + my $page = $r->{params}{page} || 1; + my $pager = Memories::Album->pager( + Memories->config->{photos_per_page}, $page); + $r->{objects} = [$pager->search(privacy => 0)]; + $r->{template_args}{pager} = $pager; +} + +sub edit :Exported { + my ($self, $r) = @_; + use Data::Dumper; warn Dumper($r->{params}); + my $album; + if ($r->{params}{album}) { # We're adding to an album + $album = $self->retrieve($r->{params}{album}); + if (!$album or $album->user != $r->user) { + $r->template("denied"); return; + } + for (map /(\d+)/,grep /add\d+/, keys %{$r->{params}}) { + Memories::AlbumEntry->create({ + album => $album->id, + photo => $_ + }); + } + } else { + $album = $r->{objects}[0]; + if (!$album or $album->user != $r->user) { + $r->template("denied"); return; + } + for (map /(\d+)/,grep /delete\d+/, keys %{$r->{params}}) { + my ($ae) = Memories::AlbumEntry->search({ + album => $album->id, + photo => $_ + }); + $ae->delete if $ae; + } + } + $r->objects([ $album ]); + $r->template("view"); $self->view($r); +} + +package Memories::AlbumEntry; +use base qw(Memories::DBI); +__PACKAGE__->table("album_entry"); +__PACKAGE__->columns(TEMP => qw/count/); +__PACKAGE__->columns(Essential => qw/id album photo/); +__PACKAGE__->set_sql(summary => qq/ +SELECT id, album, count(*) AS count +FROM album_entry; +GROUP BY album +ORDER BY count DESC +LIMIT 50 + /); +Memories::AlbumEntry->has_a("photo" => "Memories::Photo"); +Memories::AlbumEntry->has_a("album" => "Memories::Album"); + +Memories::Photo->has_many(albums => ["Memories::AlbumEntry" => "album"]); +Memories::Photo->has_many(albumentries => "Memories::AlbumEntry"); +Memories::Album->has_many(photos => ["Memories::AlbumEntry" => "photo"] ); +Memories::Album->has_many(albumentries => "Memories::AlbumEntry"); + +1; diff --git a/Memories/Comment.pm b/Memories/Comment.pm new file mode 100644 index 0000000..2b7f0ca --- /dev/null +++ b/Memories/Comment.pm @@ -0,0 +1,7 @@ +package Memories::Comment; +use base qw(Memories::DBI); +__PACKAGE__->columns(Essential => qw/id name photo/); +__PACKAGE__->columns(Others => qw/content/); +Memories::Comment->has_a("photo" => "Memories::Photo"); + +1; diff --git a/Memories/Config.pm b/Memories/Config.pm new file mode 100644 index 0000000..9796ab0 --- /dev/null +++ b/Memories/Config.pm @@ -0,0 +1,58 @@ +package Memories::Config; +# PATHS AND URLS + +# This parameter should be the external address of your Memories +# installation +Memories->config->uri_base("http://memories.simon-cozens.org/"); + +# This is where your templates will live. +Memories->config->{template_root} = "/home/simon/maypole-sites/memories/templates"; + +# Here is where uploaded photos will be stored. Your web server user +# should own this directory. +Memories->config->{data_store} = "/web/photostore/"; + +# You also need to configure your web server so that it serves files out +# of the data store; this URL should be where that data store directory +# is exposed on the web. +Memories->config->{data_store_external} = "http://memories.simon-cozens.org/store/"; + +# Your database server: the first part should always be "dbi"; the +# second, the name of the DBD driver you're using (usually mysql unless +# you want to do your own thing); the final part, the name of the +# database. +# +# Initialize this database from memories.sql and give www-data (or +# equivalent) read/write access to all tables. +Memories->config->{dsn} = "dbi:mysql:memories"; + +# SESSION MANAGEMENT +# +# The name of any cookies this application should give out +Memories->config->{auth}{cookie_name} = "memories"; + +# Session file storage. Create these directories and have them owned by +# www-data +Memories->config->{auth}{session_args} = { + Directory => "/var/lib/memories/sessions", + LockDirectory => "/var/lib/memories/sessionlock", + }; + +# This is where your Image::Seek library will be stored. +Memories->config->{image_seek} = "/var/lib/memories/imageseek.db"; + +# DISPLAY PARAMETERS +# +# It's OK to leave these as they are. +# +# The size of thumbnails to generate +Memories->config->{thumb_size} = "90x90"; + +# Sizes you want to scale to. "full" is special. +Memories->config->{sizes} = + [ qw/ 150x100 300x200 640x480 800x600 1024x768 1280x1024 full /]; + +# Number of photos on a page. It's best if this is a multiple of three +Memories->config->{photos_per_page} = 21; + +1; diff --git a/Memories/DBI.pm b/Memories/DBI.pm new file mode 100644 index 0000000..594d91e --- /dev/null +++ b/Memories/DBI.pm @@ -0,0 +1,8 @@ +package Memories::DBI; +#use base qw(Class::DBI::Sweet Class::DBI::mysql); +use base qw(Class::DBI::mysql); +Memories::DBI->connection(Memories->config->{dsn}); +Memories::DBI->autoupdate(1); +use Carp qw(carp); + +1; diff --git a/Memories/Photo.pm b/Memories/Photo.pm new file mode 100644 index 0000000..4e2a59d --- /dev/null +++ b/Memories/Photo.pm @@ -0,0 +1,290 @@ +package Memories::Photo; +use strict; +use Carp qw(cluck confess); +use base qw(Memories::DBI Maypole::Model::CDBI::Plain); +use Time::Piece; +use Image::Seek; +use constant PAGER_SYNTAX => "LimitXY"; +__PACKAGE__->columns(Essential => qw(id title uploader uploaded x y)); +__PACKAGE__->untaint_columns(printable => [qw/title/]); +__PACKAGE__->columns(TEMP => qw/exif_object/); +__PACKAGE__->set_sql(recent => q{ +SELECT __ESSENTIAL__ +FROM __TABLE__ +ORDER BY uploaded DESC +LIMIT 4 +}); + +__PACKAGE__->has_many(comments => "Memories::Comment"); +__PACKAGE__->has_a(uploader => "Memories::User"); +__PACKAGE__->has_a(uploaded => "Time::Piece", + inflate => sub { Time::Piece->strptime(shift, "%Y-%m-%d %H:%M:%S") }, + deflate => 'datetime', +); + +sub do_upload :Exported { + my ($self, $r) = @_; + my %upload = $r->upload("photo"); + + # XXX Stop anonymous uploads! + my $photo = $self->create({ + uploader => $r->user, + uploaded => Time::Piece->new(), + title => ($r->params->{title} || $upload{filename}) + }); + + # Dump content + if (!open OUT, ">". $photo->path("file")) { + die "Can't write ".$photo->path("file")." because $!"; + } + # XXX Check it's a JPEG, etc. + # XXX Unzip ZIP file + print OUT $upload{content}; + close OUT; + my ($x, $y) = dim(image_info($photo->path)); + $photo->x($x); $photo->y($y); + + # Rotate? + $photo->unrotate(); + + $photo->make_thumb; + $photo->add_tags($r->{params}{tags}); + $photo->add_to_imageseek_library; + Memories->zap_cache(); + + # Add system tags here + my $tag = "date:".$photo->shot->ymd; + $photo->add_to_system_tags({tag => Memories::SystemTag->find_or_create({name =>$tag}) }); + + # Set it up to go again + $r->objects([$photo]); + $r->template("view"); + $r->message("Thanks for the upload! ". + ($r->{params}{tags} ? "" + : "Don't forget to config->uri_base."photo/view/".$photo->id."?active=tagedit\">tag your photo" + ) + ); +} + +sub view :Exported { + my ($self, $r) = @_; + if ($r->{session}{last_search}) { + my $photo = $r->{objects}[0]; + # This is slightly inefficient + my @search = split/,/, $r->{session}{last_search}; + my $found = -1; + for my $i (0..$#search) { + next unless $photo->id == $search[$i]; + $found = $i; + } + return unless $found > -1; + $r->{template_args}{next} = $self->retrieve($search[$found+1]) + if $found+1 <= $#search; + $r->{template_args}{prev} = $self->retrieve($search[$found-1]) + if $found-1 >= 0; + } +} +sub upload :Exported {} + +use Class::DBI::Plugin::Pager; +use Class::DBI::Plugin::AbstractCount; + +sub recent :Exported { + my ($self, $r) = @_; + my $page = $r->params->{page} || 1; + my $pager = $self->pager( + per_page => Memories->config->{photos_per_page}, + page => $page, + syntax => PAGER_SYNTAX, + order_by => "uploaded desc" + ); + $r->objects([$pager->retrieve_all ]); + $r->{template_args}{pager} = $pager; + $r->last_search; +} + +sub add_comment :Exported { + my ($self, $r, $photo) = @_; + $r->template("view"); + $r->objects->[0]->add_to_comments({ + name => $r->params->{name}, + content => $r->params->{content} + }); +} + +sub format { + "jpg" # For now +} + +use Cache::MemoryCache; +use Image::Info qw(dim image_info); +use Image::ExifTool; +my $cache = new Cache::MemoryCache( { 'namespace' => 'MemoriesInfo' }); + +sub add_to_imageseek_library { + my $self = shift; + Image::Seek::cleardb(); + my $img = Image::Imlib2->load($self->path("file")); + + Image::Seek::add_image($img, $self->id); + # Merge this new one into the main database; there is a bit of a + # race condition here. XXX + Image::Seek::loaddb(Memories->config->{image_seek}); + Image::Seek::savedb(Memories->config->{image_seek}); +} + +sub recommended_tags { + my $self = shift; + my %tags = map { $_->name => $_ } + map { $_->tags } + $self->find_similar(3); + values %tags; +} + +sub find_similar { + my ($self, $count) = @_; + Image::Seek::cleardb(); + Image::Seek::loaddb(Memories->config->{image_seek}); + my @res = map {$_->[0] } Image::Seek::query_id($self->id, $count); + shift @res; # $self + map { $self->retrieve($_) } @res; +} + +sub unrotate { + my $self = shift; + my $orient = $self->exif_info->{EXIF}->{Orientation}; + return if $orient !~/Rotate (\d+)/i; + my $steps = $1/90; + my $img = Image::Imlib2->load($self->path("file")); + $img->image_orientate($steps); + $img->save($self->path("file")); +} + +sub exif_info { + my $self = shift; + my $info = $self->exif_object; + return $info if $info; + # Get it from the Cache + if (!($info = $cache->get($self->id))) { + # Create it + $info = $self->_exif_info; + $cache->set($self->id, $info); + } + $self->exif_object($info); + $info; +} + +sub _exif_info { + my $exifTool = new Image::ExifTool; + $exifTool->Options(Group0 => ['EXIF', 'MakerNotes', 'Composite']); + my $info = $exifTool->ImageInfo(shift->path); + my $hash = {}; + foreach my $tag ($exifTool->GetFoundTags('Group0')) { + my $group = $exifTool->GetGroup($tag); + my $val = $info->{$tag}; + next if ref $val eq 'SCALAR'; + next if $val =~ /^[0\s]*$/; + $hash->{$group}->{$exifTool->GetDescription($tag)} = $val; + } + return $hash; +} + +# XXX Cache this +sub dimensions { join "x", $_[0]->x, $_[0]->y } + +sub is_bigger { + my ($self, $size) = @_; + return 1 if $size eq "full"; + my ($w, $h) = ($self->x, $self->y); + my ($w2, $h2) = split /x/, $size; + return 1 if $w > $w2 or $h > $h2; + return 0; +} + +sub sized_url { # Use this rather than ->path from TT + my ($self, $size) = @_; + my $url = Memories->config->{data_store_external}; + my $resized = Memories->config->{sizes}->[$size]; + if (!$resized) { cluck "Asked for crazy size $size"; return; } + if ($resized eq "full") { return $self->path("url") } + $self->scale($resized) + unless -e $self->path( file => $resized ); + return $self->path(url => $resized); +} + +sub path { + my ($self, $is_url, $scale) = @_; + my $path = + Memories->config->{$is_url eq "url" ? "data_store_external" : "data_store" }; + if ($scale) { $path .= "$scale/" } + # Make dir if it doesn't exist, save trouble later + use File::Path; + if ($is_url ne "url") {mkpath($path);} + $path .= $self->id.".".$self->format; + return $path; +} + +sub thumb_url { shift->path(url => Memories->config->{thumb_size}); } +sub make_thumb { shift->scale(Memories->config->{thumb_size}, 1); } + +use Image::Imlib2; +sub scale { + my ($self, $scale, $swap) = @_; + my ($x, $y) = split /x/, $scale; + # Find out smaller axis + my ($cur_x, $cur_y) = ($self->x, $self->y); + if (!$swap) { + if ($cur_x < $cur_y) { $y = 0 } else { $x = 0 } + } else { + if ($cur_x > $cur_y) { $y = 0 } else { $x = 0 } + } + my $img = Image::Imlib2->load($self->path("file")); + unless ($img) { + cluck "Couldn't open image file ".$self->path("file"); + return; + } + $img = $img->create_scaled_image($x, $y); + $img->image_set_format("jpeg"); + my $file = $self->path( file => $scale ); + $img->save($file); + if ($!) { + cluck "Couldn't write image file $file ($!)"; + return; + } +} + +use Text::Balanced qw(extract_multiple extract_quotelike); +sub edit_tags :Exported { + my ($self, $r) = @_; + my $photo = $r->objects->[0]; + my %params = %{$r->params}; + for (keys %params) { + next unless /delete_(\d+)/; + my $tagging = Memories::Tagging->retrieve($1) or next; + next unless $tagging->photo->id == $photo->id; + $tagging->delete; + } + $photo->add_tags($params{newtags}); + $r->template("view"); +} + +sub add_tags { + my ($photo, $tagstring) = @_; + + for my $tag (map { s/^"|"$//g; $_} extract_multiple(lc $tagstring, [ \&extract_quotelike, qr/([^\s]+)/ ], undef,1)) { + $photo->add_to_tags({tag => Memories::Tag->find_or_create({name =>$tag}) }) + } +} + +sub shot { + my $self = shift; + my $exif = $self->exif_info->{EXIF}; + my ($dt) = + grep {$_ and /[^ 0:]/} + ($exif->{ 'Shooting Date/Time' }, + $exif->{ 'Date/Time Of Digitization' }, + $exif->{ 'Date/Time Of Last Modification' }); + if (!$dt) { return $self->uploaded } + return Time::Piece->strptime($dt, "%Y:%m:%d %T") || $self->uploaded; +} +1; diff --git a/Memories/SystemTag.pm b/Memories/SystemTag.pm new file mode 100644 index 0000000..cdb8e14 --- /dev/null +++ b/Memories/SystemTag.pm @@ -0,0 +1,63 @@ +package Memories::SystemTag; +use strict; +use base qw(Memories::DBI Maypole::Model::CDBI::Plain); +__PACKAGE__->columns(Essential => qw/id name/); +__PACKAGE__->table("system_tag"); + +Memories::Photo->set_sql(sorted_by_system_tag => q/ +SELECT photo.id as id, title, uploader, uploaded, x, y +FROM photo, system_tag, system_tagging +WHERE system_tagging.photo = photo.id + AND system_tagging.tag = system_tag.id + AND system_tag.id = ? +ORDER BY photo.uploaded DESC +/ +); +sub view :Exported { + my ($self, $r) = @_; + my $tag; + my $page = $r->params->{page} || 1; + my $pager = Class::DBI::Pager::_pager("Memories::Photo", + Memories->config->{photos_per_page}, $page); + $r->{template_args}{pager} = $pager; + + if (!$r->objects) { + $tag = $self->search(name => $r->{args}->[0])->first; + } else { + $tag = $r->objects->[0]; # Should hardly happen + } + $r->{template_args}{tag} = $tag; + $r->{template_args}{tags} = [$tag]; # For selector + $r->{template_args}{photos} = + [$pager->search_sorted_by_system_tag($tag->id)]; + $r->last_search; +} + +package Memories::SystemTagging; +use base qw(Memories::DBI); +use Class::DBI::Pager; +__PACKAGE__->table("system_tagging"); +__PACKAGE__->columns(TEMP => qw/count/); +__PACKAGE__->columns(Essential => qw/id tag photo/); +__PACKAGE__->set_sql(summary => qq/ +SELECT id, system_tag, count(*) AS count +FROM system_tagging +GROUP BY system_tag +ORDER BY count DESC +LIMIT 50 + /); +__PACKAGE__->set_sql(all => qq/ +SELECT id, tag, count(*) AS count +FROM system_tagging +GROUP BY system_tag +ORDER BY count DESC + /); +Memories::SystemTagging->has_a("photo" => "Memories::Photo"); +Memories::SystemTagging->has_a("tag" => "Memories::SystemTag"); + +Memories::Photo->has_many(system_tags => ["Memories::SystemTagging" => "system_tag"]); +Memories::Photo->has_many(system_taggings => "Memories::SystemTagging"); +Memories::SystemTag->has_many(photos => ["Memories::SystemTagging" => "photo"] ); +Memories::SystemTag->has_many(system_taggings => "Memories::SystemTagging"); + +1; diff --git a/Memories/Tag.pm b/Memories/Tag.pm new file mode 100644 index 0000000..1140185 --- /dev/null +++ b/Memories/Tag.pm @@ -0,0 +1,110 @@ +package Memories::Tag; +use strict; +use base qw(Memories::DBI Maypole::Model::CDBI::Plain); +__PACKAGE__->columns(Essential => qw/id name/); + +Memories::Photo->set_sql(sorted_by_tag => q/ +SELECT photo.id as id, title, uploader, uploaded, x, y +FROM photo, tag, tagging +WHERE tagging.photo = photo.id + AND tagging.tag = tag.id + AND tag.id = ? +ORDER BY photo.uploaded DESC +/ +); + +sub view :Exported { + my ($self, $r) = @_; + my $tag; + my $page = $r->params->{page} || 1; + my $pager = Class::DBI::Pager::_pager("Memories::Photo", + Memories->config->{photos_per_page}, $page); + $r->{template_args}{pager} = $pager; + my @tags = map { $self->search(name => $_)->first } @{$r->args}; + + if (@{$r->args} > 1) { # This is actually an n-level search + my $sth = $self->multi_search(@tags); + $r->{template_args}{photos} = [ $r->{template_args}{pager}->sth_to_objects($sth) ]; + $sth->finish; + $r->{template_args}{tags} = \@tags; + } else { + if (!$r->objects) { + $tag = $tags[0]; + } else { + $tag = $r->objects->[0]; + } + $r->{template_args}{tag} = $tag; + $r->{template_args}{tags} = [$tag]; # For selector + $r->{template_args}{photos} = + [$pager->search_sorted_by_tag($tag->id)]; + } + $r->last_search(); +} + +sub multi_search { + my ($self, @tags) = @_; + my $counter = "tagaaa"; + my @stuff; + for my $tag (@tags) { + if (!$tag) { return } + push @stuff, { tag => $tag, id => $tag->id, counter => $counter++ }; + } + my $sql = "select photo.id as id, photo.title as title, uploader, uploaded, x, y +from photo, ". (join ",", map{ "tagging ".$_->{counter} } @stuff). +" where ". (join " AND ", map { "$_->{counter}.tag=$_->{id} and photo.id += $_->{counter}.photo" } @stuff); + +$sql .= " order by photo.uploaded desc"; + my $sth = $self->db_Main->prepare($sql); +} + +sub list :Exported { + my ($self, $r) = @_; + my $page = $r->params->{page} || 1; + my $pager = Memories::Tagging->pager( + Memories->config->{photos_per_page}, $page + ); + $r->{template_args}{pager} = $pager; + $r->objects([map {$_->tag} $pager->search_all]); +} + +sub list_js :Exported { + my ($self, $r) = @_; + $r->objects([ $self->retrieve_all ]); +} + +package Memories::Tagging; +use base qw(Memories::DBI); +use Class::DBI::Pager; +__PACKAGE__->columns(TEMP => qw/count/); +__PACKAGE__->columns(Essential => qw/id tag photo/); +__PACKAGE__->set_sql(summary => qq/ +SELECT id, tag, count(*) AS count +FROM tagging +GROUP BY tag +ORDER BY count DESC +LIMIT 75 + /); +__PACKAGE__->set_sql(all => qq/ +SELECT id, tag, count(*) AS count +FROM tagging +GROUP BY tag +ORDER BY count DESC + /); +__PACKAGE__->set_sql(user_summary => qq/ +SELECT tagging.id id, tag, count(*) AS count +FROM tagging, photo +WHERE tagging.photo = photo.id AND photo.uploader = ? +GROUP BY tag +ORDER BY count DESC +/); + +Memories::Tagging->has_a("photo" => "Memories::Photo"); +Memories::Tagging->has_a("tag" => "Memories::Tag"); + +Memories::Photo->has_many(tags => ["Memories::Tagging" => "tag"]); +Memories::Photo->has_many(taggings => "Memories::Tagging"); +Memories::Tag->has_many(photos => ["Memories::Tagging" => "photo"] ); +Memories::Tag->has_many(taggings => "Memories::Tagging"); + +1; diff --git a/Memories/User.pm b/Memories/User.pm new file mode 100644 index 0000000..b0020a5 --- /dev/null +++ b/Memories/User.pm @@ -0,0 +1,77 @@ +package Memories::User; +use base qw(Memories::DBI Maypole::Model::CDBI::Plain); +__PACKAGE__->columns(All => qw/id name password/); +__PACKAGE__->has_many(photos => "Memories::Photo"); + +sub register :Exported { + my ($class, $r) = @_; + my $login = $r->params->{name}; + my $pass = $r->params->{password}; + $r->template("frontpage"); + return if $r->user; + + # Do we already exist? + if (!$login) { $r->template("login"); $r->message("You didn't enter a user name"); return; } + my ($user) = $class->search(name => $login); + if ($user) { $r->template("login"); $r->message("User name already exists"); return; } + $user = $class->create({name => $login, password => $pass }); + $r->user($user, 0); + $r->login_user($user->id); + +} + +sub view :Exported { + my ($self, $r) = @_; + my $user = $r->objects->[0]; + my $page = $r->params->{page} || 1; + my $pager = Class::DBI::Pager::_pager("Memories::Photo", + Memories->config->{photos_per_page}, $page); + $r->{template_args}{photos} = [$pager->search(uploader => + $user->id,{order_by => "uploaded desc"}) ]; + $r->{template_args}{pager} = $pager; + $r->{template_args}{albums} = [$user->albums(privacy => 0)]; + $r->last_search; +} + +# Album support! + +sub edit_albums :Exported { + my ($self, $r) = @_; + $r->{template} = "view"; + $self->view($r); + my $user = $r->objects->[0]; + return unless $r->user and $r->user->id == $user->id; + if ($r->{params}{create}) { + $user->add_to_albums({ + privacy => !!($r->{params}{new_privacy}), + name => $r->{params}{new_name} + }); + } elsif ($r->{params}{changes}) { + for my $album ($user->albums) { #XXX + warn "Setting $album privacy to ". + $r->{params}{"privacy_".$album->id}; + $album->privacy(!!$r->{params}{"privacy_".$album->id}); + } + } else { + my @to_delete = map /(\d+)/, + grep /delete_\d+/, keys %{$r->{params}}; + for (@to_delete) { + my $album = Memories::Album->retrieve($_); + if ($album and $album->user->id == $user->id) { + $album->delete + } + } + } + +} + +sub api_taglist :Exported { + my ($self, $r) = @_; + my $user = $r->objects->[0]; + $r->{output} .= $_->{tag}.":".$_->{count}."\n" + for Memories::Tagging->search_user_summary($user->id); + $r->{output}.= "\n"; + $r->{content_type} = "text/plain"; +} + +1; diff --git a/README b/README new file mode 100644 index 0000000..5b1fc00 --- /dev/null +++ b/README @@ -0,0 +1,64 @@ +Updating from an earlier release? Read the bit at the bottom! + +Installing the Memories photo sharing application +------------------------------------------------- + +First, Perl modules. This is the hard part. You will need: + Maypole (good luck) + Class::DBI + DBI + DBD::mysql + Class::DBI::mysql + Image::Info + Image::Imlib2 (requires libimlib2 and libimlib2-dev packages) + Time::Piece + Cache::Cache + Maypole::Plugin::Upload + Maypole::Plugin::Authentication::UserSessionCookie + HTML::TagCloud + XML::RSS + +Apache mod_perl is recommended. Memories can also be run as a CGI +application, but you're on your own. + +You will also need a MySQL database. Again, in theory other databases +can be used, but in practice, you're on your own again. + +Configure /etc/memories/Config.pm to your site, and follow the instructions in +there - it will require you to set other things up as well. + +Test that everything works: + + perl -MMemories -e1 + +If this produces no errors, you're good to configure Apache. I have +Memories living in "/home/simon/maypole-sites/memories", and my photo +storage in "/opt/store/", and so my configuration looks like this: + + + use lib qw( /home/simon/maypole-sites/memories/ ); + + + PerlModule Memories + Alias /memories/static/ /home/simon/maypole-sites/memories/templates/static/ + Alias /memories/store/ /opt/store/ + + + PerlHandler Memories + SetHandler perl-script + + +Restart Apache, register yourself and get uploading! + + +Updating Memories 1.0 to 1.1 +---------------------------- + +1) Create the system_tags and system_taggings table as in memories.sql + +2) Run the tag_dates script. + +Updating Memories 1.1 to 1.2 +---------------------------- + +No changes. diff --git a/Tagtools.pm b/Tagtools.pm new file mode 100644 index 0000000..0fc2529 --- /dev/null +++ b/Tagtools.pm @@ -0,0 +1,76 @@ +package Tagtools; +use HTML::TagCloud; +use Carp; +use Cache::FileCache; +use Storable qw(freeze); use MIME::Base64; +use Calendar::Simple; +sub import { + my $whence = caller; + my ($class) = @_; + my %cache_options = ( 'namespace' => $whence.'TagTools', + 'default_expires_in' => 600 ); + my $cache = + new Cache::FileCache( \%cache_options ) or + croak( "Couldn't instantiate FileCache" ); + *{$whence."::zap_cache"} = sub { $cache->Clear }; + *{$whence."::do_cached"} = sub { + my ($self, $codeblock,$arg) = @_; + my $key = 0+$codeblock; if ($arg) { $key .=":".encode_base64(freeze(\$arg)); } + my $c = $cache->get(0+$codeblock); return @$c if $c; + my @stuff = $codeblock->($arg); + $cache->set(0+$codeblock, [ @stuff ]); + return @stuff; + }; + *{$whence."::_tagcloud"} = sub { + my $cloud = HTML::TagCloud->new(); + my $base = $whence->config->uri_base."tag/view/"; + for my $tagging (($whence."::Tagging")->search_summary) { + my $name = $tagging->tag->name; + $cloud->add($name, $base.uri_escape($name), $tagging->{count}) + } + $cloud + }; + *{$whence."::_calendar"} = sub { + my $self = shift; + my $arg = shift; + my ($y, $m) = split /-/, ($arg || Time::Piece->new->ymd); + my @m = Calendar::Simple::calendar($m, $y); + my @month; + foreach my $week (@m) { + my @weekdays; + foreach my $day (@$week) { + my $d = { day => $day }; + if ($day) { + my $tag = "date:$y-$m-".sprintf("%02d", $day); + my ($x) = ($whence."::SystemTag")->search(name => $tag); + if ($x) { $d->{tag} = "/system_tag/view/$tag" } + } + push(@weekdays, $d); + } + push(@month, \@weekdays); + } + return \@month; + }; + for my $thing (qw(tagcloud calendar)) { + *{$whence."::$thing"} = sub { shift->do_cached($thing, @_) } + } + +} + + +# THIS IS A HACK + +use Time::Seconds; +sub Time::Piece::next_month { + my $tp = shift; + my $month = $tp + ONE_MONTH; + return if $month > Time::Piece->new; + return $month +} +sub Time::Piece::prev_month { + my $tp = shift; + my $month = $tp - ONE_MONTH; + return $month +} + +1; diff --git a/debian/apache.conf b/debian/apache.conf new file mode 100644 index 0000000..36cef61 --- /dev/null +++ b/debian/apache.conf @@ -0,0 +1,18 @@ +PerlModule Memories + +Alias /memories/static/ /etc/memories/templates/static/ +Alias /memories/store/ /var/www/memories/store/ + + + PerlHandler Memories + SetHandler perl-script + + +# +# Alias /static/ /etc/memories/templates/static/ +# Alias /store/ /var/www/memories/store/ +# +# PerlHandler Memories +# SetHandler perl-script +# +# diff --git a/debian/changelog b/debian/changelog new file mode 100644 index 0000000..4398e70 --- /dev/null +++ b/debian/changelog @@ -0,0 +1,5 @@ +memories (1.1-1) unstable; urgency=low + + * Initial release - closes: #345277 + + -- Ben Hutchings Sat, 31 Dec 2005 02:59:59 +0000 diff --git a/debian/compat b/debian/compat new file mode 100644 index 0000000..b8626c4 --- /dev/null +++ b/debian/compat @@ -0,0 +1 @@ +4 diff --git a/debian/control b/debian/control new file mode 100644 index 0000000..0aee180 --- /dev/null +++ b/debian/control @@ -0,0 +1,31 @@ +Source: memories +Maintainer: Ben Hutchings +Section: web +Priority: extra +Build-Depends-Indep: debhelper (>= 4), perl (>= 5.6.0-16) +Standards-Version: 3.6.2 + +Package: memories +Architecture: all +Depends: ${perl:Depends}, + libapache-session-perl, + libcache-cache-perl, + libcalendar-simple-perl, + libclass-dbi-perl, + libclass-dbi-plugin-pager-perl, + libhtml-tagcloud-perl, + libimage-exiftool-perl, + libimage-imlib2-perl, + libimage-info-perl, + libmaypole-perl, + libmaypole-plugin-authentication-usersessioncookie-perl, + libmaypole-plugin-upload-perl, + libtime-piece-perl +Recommends: libapache-mod-perl | libapache2-mod-perl2, + libclass-dbi-mysql-perl, + libdbd-mysql-perl +Description: a web-based photo sharing application + Memories allows multiple users to browse and share photos. Each + user's photos can be seen in blog format or as albums. Each photo can + be tagged with names, places, and other descriptive terms, and found + through these tags. diff --git a/debian/copyright b/debian/copyright new file mode 100644 index 0000000..84b7ac4 --- /dev/null +++ b/debian/copyright @@ -0,0 +1,8 @@ +This is the Debian-packaged version of Memories, put together by Ben +Hutchings using source from: + + http://maypole.perl.org/?Applications + +The author of Memories is Simon Cozens . + +TODO: LICENCE TEXT diff --git a/debian/memories.docs b/debian/memories.docs new file mode 100644 index 0000000..5b4892f --- /dev/null +++ b/debian/memories.docs @@ -0,0 +1,2 @@ +Changes +README diff --git a/debian/rules b/debian/rules new file mode 100644 index 0000000..c34b398 --- /dev/null +++ b/debian/rules @@ -0,0 +1,36 @@ +#!/usr/bin/make -f + +build : + +binary : binary-arch binary-indep + +binary-arch : + +binary-indep : + dh_testroot + install -d debian/memories/usr/share/perl5/Memories + install Memories.pm debian/memories/usr/share/perl5/ + install Memories/*.pm debian/memories/usr/share/perl5/Memories/ + install -d debian/memories/etc/memories + mv debian/memories/usr/share/perl5/Memories/Config.pm debian/memories/etc/memories/ + cp -dR templates debian/memories/etc/memories/ + ln -s /etc/memories/Config.pm debian/memories/usr/share/perl5/Memories/ + install debian/apache.conf debian/memories/etc/memories/ + install -d debian/memories/etc/apache/conf.d + ln -s ../../memories/apache.conf debian/memories/etc/apache/conf.d/memories + install -d debian/memories/usr/share/memories + install memories.sql tag_dates debian/memories/usr/share/memories/ + install -d debian/memories/var/lib/memories + install -d -o www-data -g www-data -m 770 debian/memories/var/lib/memories/sessions debian/memories/var/lib/memories/sessionlock + dh_perl + dh_installchangelogs + dh_installdocs + dh_compress + dh_fixperms -X /var/lib/memories/session + dh_gencontrol + dh_builddeb + +clean : + dh_clean + +.PHONY : build binary binary-arch binary-dep clean diff --git a/memories.sql b/memories.sql new file mode 100644 index 0000000..9ebe256 --- /dev/null +++ b/memories.sql @@ -0,0 +1,58 @@ +CREATE TABLE photo ( + id integer not null auto_increment primary key, + title varchar(255), + uploader integer, + uploaded datetime, + x integer, + y integer +); + +CREATE TABLE comment ( + id integer not null auto_increment primary key, + name varchar(255), + content text, + photo integer +); + +CREATE TABLE user ( + id integer not null auto_increment primary key, + name varchar(255), + password varchar(255) +); + +CREATE TABLE tag ( + id integer not null auto_increment primary key, + name varchar(255) +); + +CREATE TABLE tagging ( + id integer not null auto_increment primary key, + tag integer, + photo integer +); + +CREATE TABLE album ( + id integer not null auto_increment primary key, + name varchar(255), + user integer, + privacy integer +); + +CREATE TABLE album_entry ( + id integer not null auto_increment primary key, + album integer, + photo integer +); + +CREATE TABLE system_tag ( + id integer not null auto_increment primary key, + name varchar(255) +); + +CREATE TABLE system_tagging ( + id integer not null auto_increment primary key, + tag integer, + photo integer +); + + diff --git a/templates/album/denied b/templates/album/denied new file mode 100644 index 0000000..7b641a9 --- /dev/null +++ b/templates/album/denied @@ -0,0 +1,7 @@ +[% INCLUDE header %] +
+This album has been marked private, and you do not have permission to +view it. +
+ +[% INCLUDE footer; %] diff --git a/templates/album/list b/templates/album/list new file mode 100644 index 0000000..0229289 --- /dev/null +++ b/templates/album/list @@ -0,0 +1,25 @@ +[% INCLUDE header %] +[% PROCESS macros %] + + +[% WHILE objects.size > 0 %] + + [% SET minilist = objects.splice(0,3) %] + [% FOR album = minilist %] + + [% END %] + +[% END %] +
+ [% album %]
+

+ + [%album.photos.size %] + photo[%-"s" IF album.photos.size != 1 %] +
+ [%album.user%] +
+
+[% INCLUDE pager %] +[% INCLUDE footer %] diff --git a/templates/album/view b/templates/album/view new file mode 100644 index 0000000..a8ad02d --- /dev/null +++ b/templates/album/view @@ -0,0 +1,9 @@ +[% INCLUDE header %] +[% PROCESS macros %] +

[% album.name %]

+[% IF album.user == request.user %] + [% view_page_of(photos, "delete") %] +[% ELSE %] +[% view_page_of(photos) %] +[% END %] +[% INCLUDE footer %] diff --git a/templates/album_adder b/templates/album_adder new file mode 100644 index 0000000..2c3db73 --- /dev/null +++ b/templates/album_adder @@ -0,0 +1,9 @@ +[% IF request.user.albums.size > 0 %] + Add to: + + +[% END %] diff --git a/templates/calendar b/templates/calendar new file mode 100644 index 0000000..c71bf82 --- /dev/null +++ b/templates/calendar @@ -0,0 +1,43 @@ +
+[% IF request.action == "view" %] + [% SET shot = photo.shot %] +[% END %] +[% SET date = view_cal || shot || now %] +[% SET imp_date = shot || now %] +[% SET next = date.next_month %] +[% SET prev = date.prev_month %] +[% SET calendar = request.calendar(date.ymd) %] + + + + + + + + + +[% FOR week = calendar %] + + +[% FOR day = week %] + [% IF day.tag %] + [% IF shot AND day.day == imp_date.mday %] + + [% ELSE %] + + [% END %] +[% END %] + +[% END %] +
« + [% date.strftime("%Y-%m") %] + [% IF next %] + » + [% END %] +
S M + TWTFS
+ [% ELSE %] + + [% END %] + [% day.day %] [% day.day %]
+
diff --git a/templates/custom/list b/templates/custom/list new file mode 100644 index 0000000..3dd50b3 --- /dev/null +++ b/templates/custom/list @@ -0,0 +1,20 @@ +[% INCLUDE header %] +[% PROCESS macros %] + + +[% WHILE objects.size > 0 %] + + [% SET minilist = objects.splice(0,3) %] + [% FOR object = minilist %] + + [% END %] + +[% END %] +
+ [% object %]
+

+ + [%object.photos.size %] + photo[%-"s" IF object.photos.size != 1 %] +
+[% INCLUDE footer %] diff --git a/templates/footer b/templates/footer new file mode 100644 index 0000000..64186ac --- /dev/null +++ b/templates/footer @@ -0,0 +1,9 @@ + + + + [% INCLUDE rhs %] + + + + + diff --git a/templates/frontpage b/templates/frontpage new file mode 100644 index 0000000..9bd9576 --- /dev/null +++ b/templates/frontpage @@ -0,0 +1,34 @@ +[% INCLUDE header %] +

Welcome to Memories

+ +

+ Memories is a site where you can upload and share your photos. +

+

+ To view other people's photos, look at the user list or the album list. +

+

+ Alternatively, Memories supports tagging photos with names, places, or + other descriptive terms. Browse based on the tags on the right, or look + at the full tag list. +

+
+[% IF request.user %] +

+Now that you're logged in, you can start uploading photos. Create some albums or +check out what you've uploaded at your home page. +

+[% ELSE %] +

+To get started uploading your own photos, you'll need to log in. If you +don't have an account, just go to the log in page anyway, choose a +username and password, and click "Register". So long as nobody else has that name, +we'll create an account for you immediately. +

+[% END %] +[% INCLUDE footer %] diff --git a/templates/header b/templates/header new file mode 100644 index 0000000..3241c67 --- /dev/null +++ b/templates/header @@ -0,0 +1,28 @@ + + + Memories - Photo Sharing [% IF photo %] - [% photo.title; END%] + + + [% IF photos %] + +[% END %] +[% IF request.params.active == "tagedit" %] + + + + +[% ELSE %] + + +[% END %] +[% INCLUDE nav %] + + +
+ [% IF messages %] +
+
    [% FOR m = messages %]
  • [%m%]
  • [% END %] +
+ [% END %] +
diff --git a/templates/header.orig b/templates/header.orig new file mode 100644 index 0000000..2ac00f4 --- /dev/null +++ b/templates/header.orig @@ -0,0 +1,27 @@ + + + Memories - ANCC Photo Sharing + + [% IF photos %] + +[% END %] +[% IF request.params.active == "tagedit" %] + + + + +[% ELSE %] + + +[% END %] +[% INCLUDE nav %] + + +
+ [% IF messages %] +
+
    [% FOR m = messages %]
  • [%m%]
  • [% END %] +
+ [% END %] +
diff --git a/templates/login b/templates/login new file mode 100644 index 0000000..b875129 --- /dev/null +++ b/templates/login @@ -0,0 +1,34 @@ + [% INCLUDE header %] + +
+ [% IF request.user %] + Welcome, [% request.user.name %] + [% ELSE %] + + [% IF login_error %] +
[% login_error %]
+ [% END %] +
+
+ Login or register as a new user + + + + +
+ Username: + + +
+ Password: + + +
+ + +
+
+ [% END %] +
+ + [% INCLUDE footer %] diff --git a/templates/login_box b/templates/login_box new file mode 100644 index 0000000..b875129 --- /dev/null +++ b/templates/login_box @@ -0,0 +1,34 @@ + [% INCLUDE header %] + +
+ [% IF request.user %] + Welcome, [% request.user.name %] + [% ELSE %] + + [% IF login_error %] +
[% login_error %]
+ [% END %] +
+
+ Login or register as a new user + + + + +
+ Username: + + +
+ Password: + + +
+ + +
+
+ [% END %] +
+ + [% INCLUDE footer %] diff --git a/templates/macros b/templates/macros new file mode 100644 index 0000000..05824ef --- /dev/null +++ b/templates/macros @@ -0,0 +1,67 @@ +[% MACRO minithumb(photo) BLOCK %] + + [%photo.title|html%] + +[% END; MACRO thumb(photo, album) BLOCK %] + + + + + [% IF request.user %] + [% IF album == 1 %] + + [% ELSIF album == 2 %] + + [% END %] + [% END %] +
+ [% minithumb(photo) %] +
+ + [% photo.title |html%] + + +
+ [% FOR tag = photo.tags %] [%tag %] [% END %] +
Uploaded by + + [% photo.uploader.name |html%] +
at [% photo.uploaded %]
Add to album:
Delete from album:
+[% END %] + +[% MACRO view_page_of(photos) BLOCK; %] +[% IF request.table == "album" AND request.template == "view" %] +[% SET editing_album = 1%] +[% END %] +[% IF editing_album %] +
+[% ELSE %] + +[% END %] +[% IF !photos OR ! photos.size %] +
+ This [% request.table %] is empty! +
+[% ELSE %] + +[% WHILE photos.size > 0 %] +[% SET triple = photos.splice(0,3) %] + + [% FOR photo = triple %] + + [% END %] + +[% END %] +
+ [% IF editing_album; thumb(photo, 2); ELSE; thumb(photo, 1); END %] +
+[% END %] +[% IF editing_album %] + +[% ELSE %] +[% INCLUDE album_adder %] +[% END %] +[% INCLUDE pager %] +
+[% END %] diff --git a/templates/memories.css b/templates/memories.css new file mode 100644 index 0000000..ac5608e --- /dev/null +++ b/templates/memories.css @@ -0,0 +1,8 @@ +body { background: #afa; } + +.recentuploads { + float:right; + background: #ddd; + padding: 10px; + border: 1px solid black; +} diff --git a/templates/memories.png b/templates/memories.png new file mode 100644 index 0000000..c602c08 Binary files /dev/null and b/templates/memories.png differ diff --git a/templates/nav b/templates/nav new file mode 100644 index 0000000..f42bb06 --- /dev/null +++ b/templates/nav @@ -0,0 +1,11 @@ + diff --git a/templates/pager b/templates/pager new file mode 100644 index 0000000..3b6211d --- /dev/null +++ b/templates/pager @@ -0,0 +1,32 @@ +[% MACRO link_page(num) BLOCK; %] + [% IF num == pager.current_page %] + [% IF num < 10 %] + + [% ELSE %] + [[% num %]] + [% END %] + [% ELSE %] + + [% IF num < 10 %] + + [% ELSE %] + [[% num %]] + [% END %] + + [% END %] +[% END %] +[% IF pager.last_page > 1; + SET begin_page = pager.current_page - 5; + IF begin_page < 1; + SET begin_page = pager.first_page; + END; + SET end_page = pager.current_page + 5; + IF pager.last_page < end_page; + SET end_page = pager.last_page; + END; + IF begin_page != 1; link_page(1); " ... "; END; + FOREACH num = [begin_page .. end_page]; + link_page(num); + END; + IF end_page != pager.last_page; "..."; link_page(pager.last_page); END; +END %] diff --git a/templates/photo/comment b/templates/photo/comment new file mode 100644 index 0000000..498e5b6 --- /dev/null +++ b/templates/photo/comment @@ -0,0 +1,18 @@ +
+[% FOR comment = photo.comments %] +
+ [% comment.name | html %] writes...
+ [% comment.content %] +
+[% END %] +
+
+ Name:
+ +

+ +

+
+
+
diff --git a/templates/photo/exif b/templates/photo/exif new file mode 100644 index 0000000..b60ddbc --- /dev/null +++ b/templates/photo/exif @@ -0,0 +1,16 @@ +[% SET exif = photo.exif_info %] +[% FOR group = exif.keys %] + [% SET counter = 0 %] +

[% group %]

+ + [% FOR key = exif.$group.keys %] + + [% IF counter % 2 == 0 %] [% END %] + + + [% IF counter % 2 == 1 %] + [% ELSE %] [% END %] + [% SET counter = counter + 1 %] + [% END %] +
[% key %] [% exif.$group.$key %]
 
+[% END %] diff --git a/templates/photo/recent b/templates/photo/recent new file mode 100644 index 0000000..37d1ae3 --- /dev/null +++ b/templates/photo/recent @@ -0,0 +1,18 @@ +[% INCLUDE header %] +[% PROCESS macros %] +

Recent photos

+ +[% WHILE photos.size > 0 %] +[% SET triple = photos.splice(0,3) %] + + [% FOR photo = triple %] + + [% END %] + +[% END %] +
+ [% thumb(photo, 1) %] +
+ +[% INCLUDE pager %] +[% INCLUDE footer %] diff --git a/templates/photo/similar b/templates/photo/similar new file mode 100644 index 0000000..2743353 --- /dev/null +++ b/templates/photo/similar @@ -0,0 +1,9 @@ +

+[% PROCESS macros; FOR sim = photo.find_similar(4); minithumb(sim); END %] +

+ +

+Suggested tags: [% FOR tag = photo.recommended_tags; %] +[%tag.name%] +[% END %] +

diff --git a/templates/photo/tagedit b/templates/photo/tagedit new file mode 100644 index 0000000..a34d8bd --- /dev/null +++ b/templates/photo/tagedit @@ -0,0 +1,21 @@ +
+Tagging advice: Tags should be words, (portrait, henry) or +phrases surrounded by double quotes. ("tall buildings") You can +put any number of tags in the "add tags" box, like this: +

+ landscape cambridge "tall buildings" +

+
+

Suggested tags

+
+
+ +Delete tags:
+
    +[% FOR tagging = photo.taggings %] +
  • [% tagging.tag %] +[% END %] +
+

Add tags:

+
diff --git a/templates/photo/upload b/templates/photo/upload new file mode 100644 index 0000000..ad88ff8 --- /dev/null +++ b/templates/photo/upload @@ -0,0 +1,63 @@ + + + Memories - Photo Sharing + + + + + +[% INCLUDE nav %] + + +
+ [% IF messages %] +
+
    [% FOR m = messages %]
  • [%m%]
  • [% END %] +
+ [% END %] +
+

Upload a photo

+

+This is where you can upload your photographs. At the moment, you must +upload them one at a time; in the near future, you will be able to +upload a Zip file containing several photos. +

+

+Please note that it may take a while to transfer your photograph to the +server, so don't press stop or reload after pressing the Upload button. +

+ +
+ + + + + + + + + + + + +
Title:
Tags:
  +
+Tagging advice: Tags should be words, (portrait, henry) or +phrases surrounded by double quotes. ("tall buildings") You can +put any number of tags in the "add tags" box, like this: +

+ landscape cambridge "tall buildings" +

+You don't have to do this at this stage, and you can always re-tag +photos later, but if do it now, it saves you forgetting later! Try to +re-use tags that other people have used, then all the photos can be +found under the same tag. Use the tag list to find existing tags. +
+

+Suggested tags +

+
+
File
+
+[% INCLUDE footer %] diff --git a/templates/photo/view b/templates/photo/view new file mode 100644 index 0000000..b8a06bf --- /dev/null +++ b/templates/photo/view @@ -0,0 +1,104 @@ +[% INCLUDE header %] +[% SET tab = request.params.active || "comment" %] +[% SET url = base _ "/photo/view/" _ photo.id; %] + + + + + + + + + +
+ [% IF prev %] + << [% prev %] + [% END %] +

[% photo.title %]

+ [% IF next %] + [% next %] >> + [% END %] +[% IF request.user == photo.uploader %] +

Delete this + photo

+
+ + +
+[% END %] +
+[% FOR tag = photo.tags; + ""; + tag; + " ("; + tag.taggings.size; ")"; + ", " UNLESS tag == photo.tags.last; +END %] +
+[% IF photo.albums %] +In albums: +[% FOR album = photo.albums; %] +[%album.name%] ([%album.photos.size%]) +[% ";" UNLESS album == photo.albums.last %] +[% END %] +[% END %] + + +Photo shot on [% photo.shot.ymd %]
+Uploaded by [% +photo.uploader %] +
+
+ [% SET sizes = request.config.sizes %] + [% IF request.params.exists("scale") %] + [% SET size = request.params.scale %] + [% ELSIF request.session.scale %] + [% SET size = request.session.scale %] + [% ELSE; SET size = 1; END; %] + +

(Original size [%photo.dimensions %])

+

+ Size: + [% SET i = 0; WHILE i < sizes.size %] + [% IF photo.is_bigger(sizes.$i); %] + [% IF i == size %] + [% sizes.$i %] + [% ELSIF sizes.$i == "full" %] + full + [% ELSE %] + [% sizes.$i %] + [% END %] + [% END; %] + [% SET i = i + 1 %] + [% END %] +

+ [%# Now put it back in the session %] + [% SET request.session.scale = size %] +
+ +[% MACRO do_tab(tabname, label) BLOCK; %] +
  • + [% IF tab == tabname %] + [% ELSE %] + [% END %] + [%label%] +
  • +[% END %] + +
      + [%do_tab("comment", "Comments") %] + [%do_tab("exif", "Photo info") %] + [%do_tab("tagedit", "Edit tags") %] + [%do_tab("similar", "Similar photos") %] +
    +
    +[% +IF request.params.active == "tagedit"; INCLUDE tagedit; +ELSIF request.params.active == "exif"; INCLUDE exif; +ELSIF request.params.active == "similar"; INCLUDE similar; +ELSE; INCLUDE comment; END; +%] +
    +[% +INCLUDE footer; +%] diff --git a/templates/picture/view b/templates/picture/view new file mode 100644 index 0000000..976b786 --- /dev/null +++ b/templates/picture/view @@ -0,0 +1,7 @@ +[% INCLUDE header %] + +[%# Scale to 800 across by default, else scale to appropriate size %] + + diff --git a/templates/recent.rss b/templates/recent.rss new file mode 100644 index 0000000..51be582 --- /dev/null +++ b/templates/recent.rss @@ -0,0 +1,22 @@ + + + + Memories photostream + [%base%] + Recent photos from [%base %] +[% FOR photo = request.recent_uploads %] + + [%photo.title|html%] + [%photo.shot.strftime("%a, %d %b %Y %H:%M:%S %z") %] + + +[% FILTER html %] + + [%photo.title|html%] + +[% END %] + + +[% END %] + + diff --git a/templates/rhs b/templates/rhs new file mode 100644 index 0000000..2816a73 --- /dev/null +++ b/templates/rhs @@ -0,0 +1,49 @@ + +[% IF ! request.user %] + + + +[% ELSE %] + + +[% END %] + + + + + + + + + + + + + +[% IF request.user %] + + + + + + +[% END %] +
    Login
    [% request.user.name %]
    List users
    View all tags
    Recently uploaded
    List albums
    Home
    Upload
    +[% INCLUDE calendar %] +[% IF photos %] +

    + +RSS + +

    +[% END %] + [% INCLUDE tagcloud %] diff --git a/templates/robots.txt b/templates/robots.txt new file mode 100644 index 0000000..7e053ec --- /dev/null +++ b/templates/robots.txt @@ -0,0 +1,5 @@ +User-agent: NaverBot +Disallow: / + +User-agent: * +Disallow: /tag/view/ diff --git a/templates/static/1-s.gif b/templates/static/1-s.gif new file mode 100644 index 0000000..304f6ce Binary files /dev/null and b/templates/static/1-s.gif differ diff --git a/templates/static/1.gif b/templates/static/1.gif new file mode 100644 index 0000000..0eb416f Binary files /dev/null and b/templates/static/1.gif differ diff --git a/templates/static/2-s.gif b/templates/static/2-s.gif new file mode 100644 index 0000000..e6c405c Binary files /dev/null and b/templates/static/2-s.gif differ diff --git a/templates/static/2.gif b/templates/static/2.gif new file mode 100644 index 0000000..251becc Binary files /dev/null and b/templates/static/2.gif differ diff --git a/templates/static/3-s.gif b/templates/static/3-s.gif new file mode 100644 index 0000000..07a4ddf Binary files /dev/null and b/templates/static/3-s.gif differ diff --git a/templates/static/3.gif b/templates/static/3.gif new file mode 100644 index 0000000..dc90510 Binary files /dev/null and b/templates/static/3.gif differ diff --git a/templates/static/4-s.gif b/templates/static/4-s.gif new file mode 100644 index 0000000..5e640e4 Binary files /dev/null and b/templates/static/4-s.gif differ diff --git a/templates/static/4.gif b/templates/static/4.gif new file mode 100644 index 0000000..b42183c Binary files /dev/null and b/templates/static/4.gif differ diff --git a/templates/static/5-s.gif b/templates/static/5-s.gif new file mode 100644 index 0000000..00f59be Binary files /dev/null and b/templates/static/5-s.gif differ diff --git a/templates/static/5.gif b/templates/static/5.gif new file mode 100644 index 0000000..e488fdb Binary files /dev/null and b/templates/static/5.gif differ diff --git a/templates/static/6-s.gif b/templates/static/6-s.gif new file mode 100644 index 0000000..226cc05 Binary files /dev/null and b/templates/static/6-s.gif differ diff --git a/templates/static/6.gif b/templates/static/6.gif new file mode 100644 index 0000000..4b38e4d Binary files /dev/null and b/templates/static/6.gif differ diff --git a/templates/static/7-s.gif b/templates/static/7-s.gif new file mode 100644 index 0000000..d60763a Binary files /dev/null and b/templates/static/7-s.gif differ diff --git a/templates/static/7.gif b/templates/static/7.gif new file mode 100644 index 0000000..f22734d Binary files /dev/null and b/templates/static/7.gif differ diff --git a/templates/static/8-s.gif b/templates/static/8-s.gif new file mode 100644 index 0000000..b648bdd Binary files /dev/null and b/templates/static/8-s.gif differ diff --git a/templates/static/8.gif b/templates/static/8.gif new file mode 100644 index 0000000..036dbb6 Binary files /dev/null and b/templates/static/8.gif differ diff --git a/templates/static/9-s.gif b/templates/static/9-s.gif new file mode 100644 index 0000000..d3ea79c Binary files /dev/null and b/templates/static/9-s.gif differ diff --git a/templates/static/9.gif b/templates/static/9.gif new file mode 100644 index 0000000..c345bc5 Binary files /dev/null and b/templates/static/9.gif differ diff --git a/templates/static/memories.css b/templates/static/memories.css new file mode 100644 index 0000000..45752c9 --- /dev/null +++ b/templates/static/memories.css @@ -0,0 +1,141 @@ +BODY { background: #fff; border: 0; margin: 0; padding: 0 } +a { text-decoration: none; color: #000; } +p a { font-weight: bold } +.tagbrowse a { font-weight: bold } +a:hover { text-decoration: underline } +a img { border: 0 } +#nav { background: #994; padding: 2px; } +#rhs { background: #fff; height: 100%; width:150px; +padding: 0 5px 0 5px; border-left: 1px dotted black } +#suggestionlist { background:#ffd; padding: 10px; margin:10px; +font-weight: bold; font-family: sans-serif;} + +#login { margin-top: 10px; +font-weight: bold; font-family: sans-serif; background: #eee} +.loginfield { font-weight: bold; font-size: 80%; } + +.messages { background: #fff; border: 1px dotted black; margin: 0px 10px + 5px 10px; padding: 2px; } +#main { border: 1px solid black; background: #eee; padding: 5px; margin: 10px 0 0 10px; } + +h1 { font-family: sans-serif; } +p { font-family: sans-serif; } + +.commands { width: 100%; + font-weight: bold; font-family: sans-serif; font-size: 80%; text-align: center} +.commands a { color: #222 } + +#recentuploads { background: #aaa; border: 1px solid black; width: 400px } +.thumb { + width: 100%; + text-align:center; font-size: 80%; font-family:sans-serif; + border-bottom: 1px solid black; } + +.photoview { + text-align:center; + } +.userlist { + padding: 5px; + text-align:center; font-weight:bold; font-family:sans-serif; + } +.userlist td { background: #bbb; padding: 10px; } +.userlist td a { font-weight: bold; } +.comment { + background: #ccc; padding: 10px; + font-family:sans-serif; + margin: 5px 50px 5px 50px +} +.info { font-weight: normal; font-size: 70% } + +#tabmenu { border-bottom:2px solid black; margin: 12px 0 0 0; + padding: 0; z-index:1; padding-left:10px; +} + +#tabmenu li { display:inline; overflow:hidden; list-style-type: none; } + +#tabmenu a, a.active { + background: #ffa; + color: #000; + font-size: 10pt; + font-weight:bold; font-family:sans-serif; + margin: 0; padding: 2px 5px 0px 5px; + border-right: 1px solid #cc7; + border-top: 1px solid #cc7; + text-decoration:none; +} + +#tabmenu a.active { + background: #dd8; border-bottom:3px solid #dd8; + border-left: 1px solid black; +} + +#tabmenu a:hover { color: #fff; background: #ac9; text-decoration:none;} +#tabmenu a.active:hover { color: #000; background: #dd8; } + +#content { background: #dd8; border:2px solid black; border-top: none; + z-index: 2;margin:0; padding:20px } + +.taglistheader { text-align:center; font-family: sans-serif; } +.taglistheader a { text-decoration: none; color: #000; } + +.calendar { font-family: sans-serif; font-size: 90%; text-align:center } +.caltagged { font-weight:bold; background: #ff9; } +.caltoday { font-weight:bold; background: #f99; text-align:center; } +.calempty { color: #444; text-align:center; } +.related { background: #ff9; margin: 5px; padding: 2px; font-family:sans-serif; } +.exiftag { font-size: 8pt; background: #ffa; } +.exifvalue { font-size: 9pt; background: #fff } + +.albums { background: #ffc; padding-left: 15px; border: 1px dotted black; } +.htmltagcloud { text-align: center; font-family: sans-serif; } +span.tagcloud0 { font-size: 9px;} +span.tagcloud1 { font-size: 10px;} +span.tagcloud2 { font-size: 10px;} +span.tagcloud3 { font-size: 11px;} +span.tagcloud4 { font-size: 11px;} +span.tagcloud5 { font-size: 12px;} +span.tagcloud6 { font-size: 12px;} +span.tagcloud7 { font-size: 13px;} +span.tagcloud8 { font-size: 13px;} +span.tagcloud9 { font-size: 14px;} +span.tagcloud10 { font-size: 14px;} +span.tagcloud11 { font-size: 15px;} +span.tagcloud12 { font-size: 15px;} +span.tagcloud13 { font-size: 16px;} +span.tagcloud14 { font-size: 16px;} +span.tagcloud15 { font-size: 17px;} +span.tagcloud16 { font-size: 17px;} +span.tagcloud17 { font-size: 18px;} +span.tagcloud18 { font-size: 18px;} +span.tagcloud19 { font-size: 19px;} +span.tagcloud20 { font-size: 19px;} +span.tagcloud21 { font-size: 20px;} +span.tagcloud22 { font-size: 20px;} +span.tagcloud23 { font-size: 21px;} +span.tagcloud24 { font-size: 21px;} + +span.tagcloud0 a {text-decoration: none; color: #000;} +span.tagcloud1 a {text-decoration: none; color: #000;} +span.tagcloud2 a {text-decoration: none; color: #000;} +span.tagcloud3 a {text-decoration: none; color: #000;} +span.tagcloud10 a {text-decoration: none; color: #000;} +span.tagcloud20 a {text-decoration: none; color: #000;} +span.tagcloud11 a {text-decoration: none; color: #000;} +span.tagcloud21 a {text-decoration: none; color: #000;} +span.tagcloud12 a {text-decoration: none; color: #000;} +span.tagcloud22 a {text-decoration: none; color: #000;} +span.tagcloud13 a {text-decoration: none; color: #000;} +span.tagcloud23 a {text-decoration: none; color: #000;} +span.tagcloud14 a {text-decoration: none; color: #000;} +span.tagcloud24 a {text-decoration: none; color: #000;} +span.tagcloud4 a {text-decoration: none; color: #000;} +span.tagcloud15 a {text-decoration: none; color: #000;} +span.tagcloud5 a {text-decoration: none; color: #000;} +span.tagcloud16 a {text-decoration: none; color: #000;} +span.tagcloud6 a {text-decoration: none; color: #000;} +span.tagcloud17 a {text-decoration: none; color: #000;} +span.tagcloud7 a {text-decoration: none; color: #000;} +span.tagcloud18 a {text-decoration: none; color: #000;} +span.tagcloud8 a {text-decoration: none; color: #000;} +span.tagcloud19 a {text-decoration: none; color: #000;} +span.tagcloud9 a {text-decoration: none; color: #000;} diff --git a/templates/static/memories.png b/templates/static/memories.png new file mode 100644 index 0000000..c9207c0 Binary files /dev/null and b/templates/static/memories.png differ diff --git a/templates/static/sitemap.gz b/templates/static/sitemap.gz new file mode 100644 index 0000000..1f3d4d3 Binary files /dev/null and b/templates/static/sitemap.gz differ diff --git a/templates/static/upload.js b/templates/static/upload.js new file mode 100644 index 0000000..0b033c6 --- /dev/null +++ b/templates/static/upload.js @@ -0,0 +1,82 @@ +String.prototype.trim = function(){ return this.replace(/^\s+|\s+$/g,'') } +String.prototype.escRegExp = function(){ return this.replace(/[\\$*+?()=!|,{}\[\]\.^]/g,'\\$&') } +String.prototype.unescHtml = function(){ var i,t=this; for(i in e) t=t.replace(new RegExp(i,'g'),e[i]); return t } +function Suggestions() { this.length=0; this.picked=0 } +var suggestions = new Suggestions(); +var tagSearch='', lastEdit=''; +var h={}, sections=[{},{},{},{},{},{}], selected={}, currentTag={}, +e={'<':'<','>':'>','&':'&','"':'"'}; + +function init () { + document.onkeydown = document.onkeypress = document.onkeyup = handler + h.suggest = document.getElementById("suggestionlist"); + h.tags = document.getElementById("tags"); +} + +function handler(event) { var e=(event||window.event) //w3||ie + if (e.type == 'keyup') { + switch(e.keyCode) { + //case 8: //backspace + //case 46: //delete + case 35: //end + case 36: //home + case 39: // right + case 37: // left + case 32: // space + hideSuggestions(); break + case 38: case 40: break; + case 9: break; + case 13: break; + default: + updateSuggestions() +}} + else if (e.type == "keypress") { lastEdit = h.tags.value } +} + +function makeTag(parent, tag, js) { + parent.appendChild(document.createTextNode(" "+ tag)) +} + +function updateSuggestions() { + while (h.suggest.hasChildNodes()) h.suggest.removeChild(h.suggest.firstChild) + if(!getCurrentTag() || !currentTag.text) { + hideSuggestions(); return false + } + var tagArray = h.tags.value.toLowerCase().split(' '), + txt=currentTag.text.trim().escRegExp(), tagHash={}, t + for(t in tagArray) tagHash[tagArray[t]] = true; + + var search = tagList.match(new RegExp(("(?:^| )("+txt+"[^ ]+)"), "gi")) + if(search){ + var i; + for (i=0; i 0) { showSuggestions() } + else { hideSuggestions(); } +} + +function getCurrentTag() { + if(h.tags.value == lastEdit) return true // no edit + if(h.tags == '') return false + currentTag = {} + var tagArray=h.tags.value.toLowerCase().split(' '), oldArray=lastEdit.toLowerCase().split(' '), currentTags = [], matched=false, t,o + for (t in tagArray) { + for (o in oldArray) { + if(typeof oldArray[o] == 'undefined') { oldArray.splice(o,1); break } + if(tagArray[t] == oldArray[o]) { matched = true; oldArray.splice(o,1); break; } + } + if(!matched) currentTags[currentTags.length] = t + matched=false + } + // more than one word changed... abort + currentTag = { text:tagArray[currentTags[0]], index:currentTags[0] } + return true +} + + +function hideSuggestions() { h.suggest.style.visibility='hidden' } +function showSuggestions() { h.suggest.style.visibility='visible' } diff --git a/templates/systemtag/list b/templates/systemtag/list new file mode 100644 index 0000000..9a7361a --- /dev/null +++ b/templates/systemtag/list @@ -0,0 +1,36 @@ +[% INCLUDE header %] +[% PROCESS macros %] + + +[% WHILE objects.size > 0 %] + + [% SET minilist = objects.splice(0,7) %] + [% FOR tag = minilist %] + + [% END %] + +[% END %] +
    + + [% SET photos = tag.photos %] + [% IF photos.last %] + + + [% ELSE %] + +

    (No photos)

    + + [% END %] +
    + [% tag %] +
    + + + +
    + [%photos.size %] + photo[%-"s" IF photos.size != 1 %] +
    +
    +[% INCLUDE pager %] +[% INCLUDE footer %] diff --git a/templates/systemtag/view b/templates/systemtag/view new file mode 100644 index 0000000..42e885d --- /dev/null +++ b/templates/systemtag/view @@ -0,0 +1,14 @@ +[% INCLUDE header %] +[% PROCESS macros %] +

    +[% IF tag.name %] +[% tag.name %] +[% ELSE %] +[% FOR tag = tags %] +[%tag%] +[% ":" UNLESS tag == tags.last %] +[% END %] +[% END %] +

    +[% view_page_of(photos) %] +[% INCLUDE footer %] diff --git a/templates/tag/list b/templates/tag/list new file mode 100644 index 0000000..9a7361a --- /dev/null +++ b/templates/tag/list @@ -0,0 +1,36 @@ +[% INCLUDE header %] +[% PROCESS macros %] + + +[% WHILE objects.size > 0 %] + + [% SET minilist = objects.splice(0,7) %] + [% FOR tag = minilist %] + + [% END %] + +[% END %] +
    + + [% SET photos = tag.photos %] + [% IF photos.last %] + + + [% ELSE %] + +

    (No photos)

    + + [% END %] +
    + [% tag %] +
    + + + +
    + [%photos.size %] + photo[%-"s" IF photos.size != 1 %] +
    +
    +[% INCLUDE pager %] +[% INCLUDE footer %] diff --git a/templates/tag/list_js b/templates/tag/list_js new file mode 100644 index 0000000..af9b159 --- /dev/null +++ b/templates/tag/list_js @@ -0,0 +1 @@ +var tagList = "[% FOR tag = tags; tag | html; " "; END %]"; diff --git a/templates/tag/view b/templates/tag/view new file mode 100644 index 0000000..00d53a0 --- /dev/null +++ b/templates/tag/view @@ -0,0 +1,28 @@ +[% INCLUDE header %] +[% PROCESS macros %] +

    +[% IF tag.name %] +[% tag.name %] +[% ELSE %] +[% FOR tag = tags %] +[%tag%] +[% ":" UNLESS tag == tags.last %] +[% END %] +[% END %] +

    + +[% view_page_of(photos) %] +[% INCLUDE footer %] diff --git a/templates/tagcloud b/templates/tagcloud new file mode 100644 index 0000000..46fbb10 --- /dev/null +++ b/templates/tagcloud @@ -0,0 +1,4 @@ +

    Popular tags

    +
    +[% request.tagcloud.html(100) %] +
    diff --git a/templates/test b/templates/test new file mode 100644 index 0000000..b5f5031 --- /dev/null +++ b/templates/test @@ -0,0 +1,2 @@ +[% INCLUDE header %] +[% INCLUDE footer %] diff --git a/templates/user/album_list b/templates/user/album_list new file mode 100644 index 0000000..59bcad0 --- /dev/null +++ b/templates/user/album_list @@ -0,0 +1,41 @@ +
    +[% IF request.user == user %] +

    Albums

    +
    + [% IF user.albums.size > 0 %] + + + + + [% FOR album = user.albums %] + + + + + + [% END %] +
    Name Private? Delete?
    [% album.name %] + +
    + +
    + [% END %] +

    Create a new album

    + Name:
    + Private?
    + +
    +[% ELSE %] + +[% IF albums.size > 0 %] +

    Public albums

    +
      + [% FOR album = albums %] +
    • [% album %] + [% END %] +
    +[% END %] + +[% END %] +
    diff --git a/templates/user/list b/templates/user/list new file mode 100644 index 0000000..66ba34e --- /dev/null +++ b/templates/user/list @@ -0,0 +1,25 @@ +[% INCLUDE header %] +[% PROCESS macros %] + + +[% WHILE objects.size > 0 %] + + [% SET minilist = objects.splice(0,3) %] + [% FOR user = minilist %] + + [% END %] + +[% END %] +
    + [% SET photos = user.photos %] + [% user %]
    + [% IF photos.last %] +

    + + [%photos.size %] + photo[%-"s" IF photos.size != 1 %] + [% ELSE %] +

    (No photos)

    + [% END %] +
    +[% INCLUDE footer %] diff --git a/templates/user/view b/templates/user/view new file mode 100644 index 0000000..b3cadec --- /dev/null +++ b/templates/user/view @@ -0,0 +1,7 @@ +[% INCLUDE header %] +[% PROCESS macros %] +

    [% user.name %]

    +[% INCLUDE album_list %] + +[% view_page_of(photos) %] +[% INCLUDE footer %]