--- /dev/null
+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
--- /dev/null
+package Memories;
+use strict;
+use HTML::TagCloud;
+use URI;
+use Maypole::Application qw(Upload Authentication::UserSessionCookie
+-Debug);
+use Memories::Config;
+use Memories::DBI;
+use Memories::Photo;
+use Memories::Comment;
+use Memories::Tag;
+use Memories::SystemTag;
+use Memories::User;
+use Memories::Album;
+use URI::Escape;
+use Calendar::Simple;
+
+Memories->config->auth->{ user_field } = "name";
+Memories->config->model("Maypole::Model::CDBI::Plain");
+Memories->setup([qw/ Memories::Photo Memories::User Memories::Tag
+Memories::Album Memories::SystemTag/]);
+
+sub message {
+ my ($self, $message) = @_;
+ push @{$self->{template_args}{messages}}, $message;
+}
+
+sub additional_data {
+ my $r = shift;
+ if ($r->params->{view_cal}) {
+ $r->{template_args}{view_cal} = eval {
+ Time::Piece->strptime($r->{params}{view_cal}, "%Y-%m-%d") };
+ }
+ $r->{template_args}{now} = Time::Piece->new;
+}
+
+use Maypole::Constants;
+sub authenticate {
+ my ($self, $r) = @_;
+ return DECLINED if $self->path =~/static|store/; # XXX
+ $r->get_user;
+ return OK;
+}
+
+
+use Cache::SharedMemoryCache;
+my %cache_options = ( 'namespace' => 'MemoriesStuff',
+ 'default_expires_in' => 600 );
+my $cache =
+ new Cache::SharedMemoryCache( \%cache_options ) or
+ croak( "Couldn't instantiate SharedMemoryCache" );
+
+sub zap_cache { $cache->Clear }
+use Storable qw(freeze); use MIME::Base64;
+sub do_cached {
+ 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;
+}
+
+sub _recent_uploads { Memories::Photo->search_recent() }
+
+sub recent_uploads { shift->do_cached(\&_recent_uploads) }
+sub tagcloud { shift->do_cached(\&_tagcloud) }
+
+sub _tagcloud {
+ my $cloud = HTML::TagCloud->new();
+ my $base = Memories->config->uri_base."tag/view/";
+ for my $tagging (Memories::Tagging->search_summary) {
+ my $name = $tagging->tag->name;
+ $cloud->add($name,
+ $base.uri_escape($name),
+ $tagging->{count}
+ )
+ }
+ $cloud
+}
+
+sub calendar {
+ # shift->do_cached(\&_calendar, shift ) }
+#sub _calendar {
+ 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) = Memories::SystemTag->search(name => $tag);
+ if ($x) { $d->{tag} = "/system_tag/view/$tag" }
+ }
+ push(@weekdays, $d);
+ }
+ push(@month, \@weekdays);
+ }
+ return \@month;
+}
+
+# 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
+}
+
+
+sub tag_select {
+ my ($r, $tags, $photos) = @_;
+ # XXX only affects current page
+ my %counter;
+ for (map {$_->tags} @$photos) {
+ $counter{$_->name}++;
+ }
+ delete $counter{$_->name} for @$tags;
+ my @super;
+
+ my $cloud = HTML::TagCloud->new();
+ my $base = $r->config->uri_base.$r->path."/";
+ my $tags;
+ for my $name (sort {$a cmp $b} keys %counter) {
+ if ($counter{$name} == @$photos) {
+ push @super, $name;
+ } else {
+ $cloud->add($name, $base.uri_escape($name), $counter{$name});
+ $tags++;
+ }
+ }
+ my %res;
+ if (@super) { $res{super} = \@super }
+ if ($tags) { $res{cloud} = $cloud }
+ \%res;
+}
+1;
--- /dev/null
+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;
--- /dev/null
+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;
--- /dev/null
+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",
+ };
+
+# 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;
--- /dev/null
+package Memories::DBI;
+use base qw(Class::DBI::mysql);
+Memories::DBI->connection(Memories->config->{dsn});
+Memories::DBI->autoupdate(1);
+use Carp qw(carp);
+
+1;
--- /dev/null
+package Memories::Photo;
+use strict;
+use Carp qw(cluck confess);
+use base qw(Memories::DBI Maypole::Model::CDBI::Plain);
+use Time::Piece;
+use constant PAGER_SYNTAX => "LimitXY";
+__PACKAGE__->columns(Essential => qw(id title uploader uploaded x y));
+__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});
+ 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 <a href=\"?".$r->config->uri_base."photo/view/".$photo->id."?active=tagedit\">tag your photo</a>"
+ )
+ );
+}
+
+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;
+}
+
+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 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;
--- /dev/null
+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)];
+}
+
+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;
--- /dev/null
+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;
+
+ if (@{$r->args} > 1) { # This is actually an n-level search
+ $self->multi_search($r);
+ } else {
+ 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_tag($tag->id)];
+ }
+}
+
+sub multi_search {
+ my ($self, $r) = @_;
+ my $counter = "tagaaa";
+ my @tags;
+ for (@{$r->{args}}) {
+ my $tag = $self->search(name => $_)->first;
+ if (!$tag) { return }
+ push @tags, { 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} } @tags).
+" where ". (join " AND ", map { "$_->{counter}.tag=$_->{id} and photo.id = $_->{counter}.photo" } @tags);
+
+$sql .= " order by photo.uploaded desc";
+ my $sth = $self->db_Main->prepare($sql);
+ $r->{template_args}{photos} = [ $r->{template_args}{pager}->sth_to_objects($sth) ];
+ $sth->finish;
+ $r->{template_args}{tags} = [ map { $_->{tag} } @tags ];
+}
+
+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 50
+ /);
+__PACKAGE__->set_sql(all => qq/
+SELECT id, tag, count(*) AS count
+FROM tagging
+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;
--- /dev/null
+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)];
+}
+
+# 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
+ }
+ }
+ }
+
+}
+
+1;
--- /dev/null
+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
+
+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 Maypole/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:
+
+ <Perl>
+ use lib qw( /home/simon/maypole-sites/memories/ );
+ </Perl>
+
+ PerlModule Memories
+ Alias /memories/static/ /home/simon/maypole-sites/memories/templates/static/
+ Alias /memories/store/ /opt/store/
+
+ <Location /memories>
+ PerlHandler Memories
+ SetHandler perl-script
+ </Location>
+
+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.
+
--- /dev/null
+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
+);
+
+
--- /dev/null
+use Memories;
+my $it = Memories::Photo->retrieve_all;
+
+my $thing = $it->first;
+do {
+ print $thing->title, " $tag\n";
+ my $tag = "date:".$thing->shot->ymd;
+ $thing->add_to_system_tags({tag => Memories::SystemTag->find_or_create({name
+ =>$tag}) });
+} while $thing = $it->next;
--- /dev/null
+[% INCLUDE header %]
+<div class="warning">
+This album has been marked private, and you do not have permission to
+view it.
+</div>
+
+[% INCLUDE footer; %]
--- /dev/null
+[% INCLUDE header %]
+[% PROCESS macros %]
+
+<table class="userlist">
+[% WHILE objects.size > 0 %]
+<tr>
+ [% SET minilist = objects.splice(0,3) %]
+ [% FOR album = minilist %]
+ <td>
+ <a href="[%base%]/album/view/[%album.id%]">[% album %]<br/>
+ <img src="[% album.photos.last.thumb_url |uri%]"></a><br/>
+ <span class="info">
+ [%album.photos.size %]
+ photo[%-"s" IF album.photos.size != 1 %]
+ <br>
+ <a
+ href="[%base%]/user/view/[%album.user.id%]">[%album.user%]</a>
+ </span>
+ </td>
+ [% END %]
+</tr>
+[% END %]
+</table>
+[% INCLUDE pager %]
+[% INCLUDE footer %]
--- /dev/null
+[% INCLUDE header %]
+[% PROCESS macros %]
+<h1> [% album.name %] </h1>
+[% IF album.user == request.user %]
+ [% view_page_of(photos, "delete") %]
+[% ELSE %]
+[% view_page_of(photos) %]
+[% END %]
+[% INCLUDE footer %]
--- /dev/null
+[% IF request.user.albums.size > 0 %]
+ Add to:
+ <select name="album">
+ [% FOR album = request.user.albums %]
+ <option value="[%album.id%]"> [% album %] </option>
+ [% END %]
+ </select>
+ <input type="submit" value="Add" name="Add">
+[% END %]
--- /dev/null
+<div align="center" style="border: 1px solid black; background: #eee">
+[% 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) %]
+<table class="calendar">
+ <tr>
+ <th> <a href="?view_cal=[%prev.ymd%]">« </a></th>
+ <th colspan="5">
+ [% date.strftime("%Y-%m") %]
+ </th>
+ <th> [% IF next %]
+ <a href="?view_cal=[%next.ymd%]">» </a>
+ [% END %]
+ </th>
+ </tr>
+ <tr>
+ <td>S </td><td> M </td><td>
+ T</td><td>W</td><td>T</td><td>F</td><td>S</td>
+ </tr>
+[% FOR week = calendar %]
+ <tr>
+
+[% FOR day = week %]
+ [% IF day.tag %]
+ [% IF shot AND day.day == imp_date.mday %]
+ <td class="caltoday">
+ [% ELSE %]
+ <td class="caltagged">
+ [% END %]
+ <a href="[%base%][%day.tag%]?view_cal=[%date.ymd%]">[% day.day %]</a> </td>
+ [% ELSE %]
+ <td class="calempty"> [% day.day %] </td>
+ [% END %]
+[% END %]
+ </tr>
+[% END %]
+</table>
+</div>
--- /dev/null
+[% INCLUDE header %]
+[% PROCESS macros %]
+
+<table class="userlist">
+[% WHILE objects.size > 0 %]
+<tr>
+ [% SET minilist = objects.splice(0,3) %]
+ [% FOR object = minilist %]
+ <td>
+ <a href="[%base%]/object/view/[%object.id%]">[% object %]<br/>
+ <img src="[% object.photos.last.thumb_url |uri%]"></a><br/>
+ <span class="info">
+ [%object.photos.size %]
+ photo[%-"s" IF object.photos.size != 1 %]</span>
+ </td>
+ [% END %]
+</tr>
+[% END %]
+</table>
+[% INCLUDE footer %]
--- /dev/null
+</div>
+</td>
+ <td id="rhs" valign="top">
+ [% INCLUDE rhs %]
+ </td>
+</tr>
+</table>
+</body>
+</html>
--- /dev/null
+[% INCLUDE header %]
+<h1> Welcome to Memories </h1>
+
+<p>
+ Memories is a site where you can upload and share your photos of
+ college life with your friends.
+</p>
+<p>
+ To view other people's photos, look at the <a
+ href="[%base%]/user/list">user list</a> or the <a
+ href="[%base%]/album/list">album list</a>.
+</p>
+<p>
+ 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 <a href="[%base%]/tag/list">tag list</a>.
+</p>
+<hr/>
+[% IF request.user %]
+<p>
+Now that you're logged in, you can start <a
+href="[%base%]/photo/upload">uploading photos</a>. Create some albums or
+check out what you've uploaded at <a
+href="[%base%]/user/view/[%request.user.id%]">your home page</a>.
+</p>
+[% ELSE %]
+<p>
+To get started uploading your own photos, you'll need to <a
+href="[%base%]/login_box">log in</a>. 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.
+</p>
+[% END %]
+[% INCLUDE footer %]
--- /dev/null
+<html>
+<head>
+ <title> Memories - ANCC Photo Sharing </title>
+ <link title="Maypole" href="[%base%]/static/memories.css" type="text/css" rel="stylesheet"/>
+[% IF request.params.active == "tagedit" %]
+ <script type="text/javascript" src="[%base%]/tag/list_js"></script>
+ <script type="text/javascript" src="[%base%]/static/upload.js"></script>
+</head>
+<body onload="init()">
+[% ELSE %]
+</head>
+<body>
+[% END %]
+[% INCLUDE nav %]
+<table width="100%">
+ <tr>
+ <td valign="top">
+ [% IF messages %]
+ <div class="messages">
+ <ul> [% FOR m = messages %] <li> [%m%] </li> [% END %]
+ </ul></div>
+ [% END %]
+ <div id="main">
--- /dev/null
+ [% INCLUDE header %]
+
+ <div id="login">
+ [% IF request.user %]
+ Welcome, [% request.user.name %]
+ [% ELSE %]
+
+ [% IF login_error %]
+ <div class="error"> [% login_error %] </div>
+ [% END %]
+ <form method="post" action="[% base %]/user/register">
+ <fieldset>
+ <legend>Login or register as a new user</legend>
+ <table> <tr>
+ <td class="loginfield">
+ Username:
+ </td>
+ <td>
+ <input name="name" type="text" />
+ </td>
+ </tr>
+ <tr> <td class="loginfield">
+ Password:
+ </td><td>
+ <input name="password" type="password" />
+ </td></tr></table>
+ <input type="submit" name="login" value="Login"/>
+ <input type="submit" name="login" value="Register"/>
+ </fieldset>
+ </form>
+ [% END %]
+ </div>
+
+ [% INCLUDE footer %]
--- /dev/null
+[% MACRO thumb(photo, album) BLOCK %]
+<table class="thumb">
+ <tr><td>
+ <a href="[%base%]/photo/view/[%photo.id%]">
+ <img src="[% photo.thumb_url |uri%]" alt="[%photo.title|html%]"/>
+ </a>
+ </td> </tr>
+ <tr><td>
+ <a href="[%base%]/photo/view/[%photo.id%]">
+ <b>[% photo.title |html%] </b>
+ </a>
+
+ </td></tr>
+ <tr><td> Uploaded by
+ <a href="[%base%]/user/view/[%photo.uploader.id%]">
+ [% photo.uploader.name |html%]
+ </a><br/> at [% photo.uploaded %] </td></tr>
+ [% IF request.user %]
+ [% IF album == 1 %]
+ <tr><td> Add to album: <input type="checkbox" name="add[%photo.id%]" value="[%photo.id%]"> </td></tr>
+ [% ELSIF album == 2 %]
+ <tr><td> Delete from album: <input type="checkbox" name="delete[%photo.id%]" value="[%photo.id%]"> </td></tr>
+ [% END %]
+ [% END %]
+</table>
+[% END %]
+
+[% MACRO view_page_of(photos) BLOCK; %]
+[% IF request.table == "album" AND request.template == "view" %]
+[% SET editing_album = 1%]
+[% END %]
+[% IF editing_album %]
+<form action="[%base%]/album/edit/[%album.id%]" method="post">
+[% ELSE %]
+<form action="[%base%]/album/edit" method="post">
+[% END %]
+[% IF !photos OR ! photos.size %]
+<div class="warning">
+ This [% request.table %] is empty!
+</div>
+[% ELSE %]
+<table class="userlist">
+[% WHILE photos.size > 0 %]
+[% SET triple = photos.splice(0,3) %]
+ <tr>
+ [% FOR photo = triple %]
+ <td>
+ [% IF editing_album; thumb(photo, 2); ELSE; thumb(photo, 1); END %]
+ </td>
+ [% END %]
+ </tr>
+[% END %]
+</table>
+[% END %]
+[% IF editing_album %]
+<input type="submit" name="Delete" value="Delete">
+[% ELSE %]
+[% INCLUDE album_adder %]
+[% END %]
+[% INCLUDE pager %]
+</form>
+[% END %]
--- /dev/null
+body { background: #afa; }
+
+.recentuploads {
+ float:right;
+ background: #ddd;
+ padding: 10px;
+ border: 1px solid black;
+}
--- /dev/null
+<div id="nav">
+<table width="100%"><tr><td align="left">
+<a href="[%base%]"><img src="[%base%]/static/memories.png"/></a>
+</td><td align="right">
+[% FOR photo = request.recent_uploads %]
+ <a href="[%base%]/photo/view/[%photo.id%]">
+ <img src="[% photo.thumb_url |uri%]" alt="[%photo.title|html%]"/>
+ </a>
+[% END %]
+</td></tr></table>
+</div>
--- /dev/null
+[% IF pager.last_page > 1 %]
+ [% FOREACH num = [pager.first_page .. pager.last_page] %]
+ [% IF num == pager.current_page %]
+ [% IF num < 10 %]
+ <img src="[%base%]/static/[%num%]-s.gif">
+ [% ELSE %]
+ [[% num %]]
+ [% END %]
+ [% ELSE %]
+ <a href="?page=[% num %]">
+ [% IF num < 10 %]
+ <img src="[%base%]/static/[%num%].gif">
+ [% ELSE %]
+ [[% num %]]
+ [% END %]
+ </a>
+ [% END %]
+[% END %]
+[% END %]
--- /dev/null
+<div class="comments">
+[% FOR comment = photo.comments %]
+ <div class="comment">
+ [% comment.name | html %] writes...<hr>
+ [% comment.content %]
+ </div>
+[% END %]
+ <div class="comment">
+ <form action="[%base%]/photo/add_comment/[%photo.id%]" method="post">
+ Name: <input name="name" /> <hr>
+ <textarea name="content" cols="60" rows="5"/>
+ </textarea>
+ <p>
+ <input type="submit" name="Comment on this picture" value="Comment on this picture">
+ </p>
+ </form>
+ </div>
+</div>
--- /dev/null
+[% SET exif = photo.exif_info %]
+[% FOR group = exif.keys %]
+ [% SET counter = 0 %]
+ <h3> [% group %] </h3>
+ <table id="exif">
+ [% FOR key = exif.$group.keys %]
+
+ [% IF counter % 2 == 0 %] <tr> [% END %]
+ <td class="exiftag"> [% key %] </td>
+ <td class="exifvalue"> [% exif.$group.$key %] </td>
+ [% IF counter % 2 == 1 %] </tr>
+ [% ELSE %] <td> </td> [% END %]
+ [% SET counter = counter + 1 %]
+ [% END %]
+ </table>
+[% END %]
--- /dev/null
+[% INCLUDE header %]
+[% PROCESS macros %]
+<h1> Recent photos </h1>
+<table class="userlist">
+[% WHILE photos.size > 0 %]
+[% SET triple = photos.splice(0,3) %]
+ <tr>
+ [% FOR photo = triple %]
+ <td>
+ [% thumb(photo, 1) %]
+ </td>
+ [% END %]
+ </tr>
+[% END %]
+</table>
+
+[% INCLUDE pager %]
+[% INCLUDE footer %]
--- /dev/null
+<div class="messages"><small>
+<b>Tagging advice</b>: Tags should be words, (<i>portrait</i>, <i>henry</i>) or
+phrases surrounded by double quotes. (<i>"tall buildings"</i>) You can
+put any number of tags in the "add tags" box, like this:
+<p>
+<i> landscape cambridge "tall buildings" </i>
+</p>
+</small>
+<p> Suggested tags </p>
+<div id="suggestionlist"/>
+</div>
+
+Delete tags: <form action="[%base%]/photo/edit_tags/[%photo.id%]" method="post">
+<ul>
+[% FOR tagging = photo.taggings %]
+ <li> [% tagging.tag %] <input type="checkbox" name="delete_[%tagging.id%]">
+[% END %]
+</ul>
+<p> Add tags: <input type="textbox" name="newtags" id="tags"> <input
+type="submit" value="Tag it!"> </p>
+</form>
--- /dev/null
+<html>
+<head>
+ <title> Memories - ANCC Photo Sharing </title>
+ <link title="Maypole" href="[%base%]/static/memories.css" type="text/css" rel="stylesheet"/>
+ <script type="text/javascript" src="[%base%]/tag/list_js"></script>
+ <script type="text/javascript" src="[%base%]/static/upload.js"></script>
+</head>
+<body onload="init()">
+[% INCLUDE nav %]
+<table width="100%">
+ <tr>
+ <td valign="top">
+ [% IF messages %]
+ <div class="messages">
+ <ul> [% FOR m = messages %] <li> [%m%] </li> [% END %]
+ </ul></div>
+ [% END %]
+ <div id="main">
+<h1> Upload a photo </h1>
+<p>
+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.
+</p>
+<p>
+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.
+</p>
+
+<form method="post" action="[%base%]/photo/do_upload" enctype="multipart/form-data">
+
+<table>
+ <tr><td> Title: </td>
+ <td><input name="title"></td>
+ </tr>
+
+ <tr><td> Tags: </td>
+ <td><input name="tags" id="tags"></td>
+ <tr><td> </td> <td>
+<div class="messages"><small>
+<b>Tagging advice</b>: Tags should be words, (<i>portrait</i>, <i>henry</i>) or
+phrases surrounded by double quotes. (<i>"tall buildings"</i>) You can
+put any number of tags in the "add tags" box, like this:
+<p>
+<i> landscape cambridge "tall buildings" </i>
+</p>
+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 <b><a
+href="[%base%]tag/list">tag list</a></b> to find existing tags.
+</small>
+<p>
+Suggested tags
+</p>
+<div id="suggestionlist"/>
+</td> </tr>
+ <tr><td> File </td>
+ <td><input type="file" name="photo"></td>
+ <tr><td colspan="2"><input type="submit" name="Upload" value="Upload"></td></tr>
+</table>
+</form>
+[% INCLUDE footer %]
--- /dev/null
+[% INCLUDE header %]
+[% SET tab = request.params.active || "comment" %]
+[% SET url = base _ "/photo/view/" _ photo.id; %]
+<table width="100%">
+ <tr valign="top">
+ <td width="70%">
+<h1>[% photo.title %]</h1>
+</td>
+<td align="right" class="tagbrowse">
+[% FOR tag = photo.tags;
+ "<a href=\""; base;"/tag/view/";tag.name | html | uri;"\">";
+ tag;
+ "</a> (";
+ tag.taggings.size; ")";
+ ", " UNLESS tag == photo.tags.last;
+END %]
+</td>
+</tr>
+<tr>
+<td>
+[% IF photo.albums %]
+In albums:
+[% FOR album = photo.albums; %]
+<a href="[%base%]/album/view/[%album.id%]">[%album.name%]</a> ([%album.photos.size%])
+[% ";" UNLESS album == photo.albums.last %]
+[% END %]
+[% END %]
+
+</td>
+<td align="right" class="tagbrowse"><small>
+<i>Photo shot on [% photo.shot.ymd %] </i> <br>
+Uploaded by <a href="[%base%]/user/view/[%photo.uploader.id%]"> [%
+photo.uploader %] </a>
+</small></td>
+</tr>
+</table>
+<div class="photoview">
+ [% 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; %]
+ <img src="[% photo.sized_url(size) %]">
+ <p> (Original size [%photo.dimensions %]) </p>
+ <p>
+ Size:
+ [% SET i = 0; WHILE i < sizes.size %]
+ [% IF photo.is_bigger(sizes.$i); %]
+ [% IF i == size %]
+ [% sizes.$i %]
+ [% ELSE %]
+ <a href="[%url%]?scale=[% i %]&active=[%tab%]">[% sizes.$i %]</a>
+ [% END %]
+ [% END; %]
+ [% SET i = i + 1 %]
+ [% END %]
+ </p>
+ [%# Now put it back in the session %]
+ [% SET request.session.scale = size %]
+</div>
+
+[% MACRO do_tab(tabname, label) BLOCK; %]
+<li>
+ [% IF tab == tabname %]<a class="active">
+ [% ELSE %]<a href="[%url%]?scale=[%size%]&active=[%tabname%]">
+ [% END %]
+ [%label%]</a>
+</li>
+[% END %]
+
+<ul id="tabmenu">
+ [%do_tab("comment", "Comments") %]
+ [%do_tab("exif", "Photo info") %]
+ [%do_tab("tagedit", "Edit tags") %]
+</ul>
+<div id="content">
+[%
+IF request.params.active == "tagedit"; INCLUDE tagedit;
+ELSIF request.params.active == "exif"; INCLUDE exif;
+ELSE; INCLUDE comment; END;
+%]
+</div>
+[%
+INCLUDE footer;
+%]
--- /dev/null
+[% INCLUDE header %]
+
+[%# Scale to 800 across by default, else scale to appropriate size %]
+
+<div class="sizing">
+<a href="[%base%]/store/[%picture.id%].jpg"> Full size image </a>
+</div>
--- /dev/null
+<?xml version="1.0" encoding="ISO-8859-1"?>
+<rss version="2.0" xmlns:media="http://search.yahoo.com/mrss">
+ <channel>
+ <title>Memories photostream</title>
+ <link>[%base%]</link>
+ <description>Recent photos from [%base %]</description>
+[% FOR photo = request.recent_uploads %]
+ <item>
+ <title>[%photo.title|html%]</title>
+ <pubDate>[%photo.shot.strftime("%a, %d %b %Y %H:%M:%S %z") %]</pubDate>
+
+ <description>
+[% FILTER html %]
+ <a href="[%base%]/photo/view/[%photo.id%]">
+ <img src="[% photo.thumb_url |uri%]" alt="[%photo.title|html%]"/>
+ </a>
+[% END %]
+ </description>
+ </item>
+[% END %]
+</channel>
+</rss>
--- /dev/null
+<table class="commands">
+[% IF ! request.user %]
+<tr>
+<td><a href="[%base%]/login_box">Login</a> </td>
+</tr>
+[% ELSE %]
+<tr>
+<td style="border:1px solid black; font-weight:normal; background: #eee">[% request.user.name %]</td></tr>
+[% END %]
+<tr>
+<td><a href="[%base%]/user/list">List users</a> </td>
+</tr>
+<tr>
+<td><a href="[%base%]/tag/list">View all tags</a> </td>
+</tr>
+<tr>
+<td><a href="[%base%]/photo/recent">Recently uploaded</a></td>
+</tr>
+<tr>
+<td><a href="[%base%]/album/list">List albums</a></td>
+</tr>
+
+[% IF request.user %]
+<tr>
+<td><a href="[%base%]/user/view/[%request.user.id%]">Home</a></td>
+</tr>
+<tr>
+<td> <a href="[%base%]/photo/upload">Upload</a> </td>
+</tr>
+[% END %]
+</table>
+[% INCLUDE calendar %]
+ [% INCLUDE tagcloud %]
--- /dev/null
+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;}
--- /dev/null
+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<search.length; i++) {
+ var tl = search[i].trim()
+ if(tagHash[tl]) continue // do not suggest already typed tag
+ suggestions[suggestions.length] = makeTag(h.suggest, tl, 'complete')
+ suggestions.length++
+ }}
+ if (suggestions.length > 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' }
--- /dev/null
+[% INCLUDE header %]
+[% PROCESS macros %]
+
+<table class="taglist">
+[% WHILE objects.size > 0 %]
+<tr>
+ [% SET minilist = objects.splice(0,7) %]
+ [% FOR tag = minilist %]
+ <td>
+ <table><tr> <td width="100%" class="taglistheader">
+ <a href="[%base%]/tag/view/[%tag.name |html|uri%]">[% tag %]</a>
+ </td></tr>
+ [% SET photos = tag.photos %]
+ [% IF photos.last %]
+ <tr><td>
+ <a href="[%base%]/tag/view/[%tag.name |html|uri%]">
+ <img src="[% photos.last.thumb_url |uri%]">
+ </a>
+ </td></tr>
+ <tr><td class="info">
+ [%photos.size %]
+ photo[%-"s" IF photos.size != 1 %]</span>
+ </td></tr>
+ [% ELSE %]
+ <td><tr>
+ <p><i>(No photos)</i></p>
+ </td></tr>
+ [% END %]
+ </table>
+ </td>
+ [% END %]
+</tr>
+[% END %]
+</table>
+[% INCLUDE pager %]
+[% INCLUDE footer %]
--- /dev/null
+[% INCLUDE header %]
+[% PROCESS macros %]
+<h1>
+[% IF tag.name %]
+[% tag.name %]
+[% ELSE %]
+[% FOR tag = tags %]
+<a href="[%base%]/tag/view/[%tag.name%]">[%tag%]</a>
+[% ":" UNLESS tag == tags.last %]
+[% END %]
+[% END %]
+</h1>
+[% view_page_of(photos) %]
+[% INCLUDE footer %]
--- /dev/null
+[% INCLUDE header %]
+[% PROCESS macros %]
+
+<table class="taglist">
+[% WHILE objects.size > 0 %]
+<tr>
+ [% SET minilist = objects.splice(0,7) %]
+ [% FOR tag = minilist %]
+ <td>
+ <table><tr> <td width="100%" class="taglistheader">
+ <a href="[%base%]/tag/view/[%tag.name |html|uri%]">[% tag %]</a>
+ </td></tr>
+ [% SET photos = tag.photos %]
+ [% IF photos.last %]
+ <tr><td>
+ <a href="[%base%]/tag/view/[%tag.name |html|uri%]">
+ <img src="[% photos.last.thumb_url |uri%]">
+ </a>
+ </td></tr>
+ <tr><td class="info">
+ [%photos.size %]
+ photo[%-"s" IF photos.size != 1 %]</span>
+ </td></tr>
+ [% ELSE %]
+ <td><tr>
+ <p><i>(No photos)</i></p>
+ </td></tr>
+ [% END %]
+ </table>
+ </td>
+ [% END %]
+</tr>
+[% END %]
+</table>
+[% INCLUDE pager %]
+[% INCLUDE footer %]
--- /dev/null
+var tagList = "[% FOR tag = tags; tag | html; " "; END %]";
--- /dev/null
+[% INCLUDE header %]
+[% PROCESS macros %]
+<h1>
+[% IF tag.name %]
+[% tag.name %]
+[% ELSE %]
+[% FOR tag = tags %]
+<a href="[%base%]/tag/view/[%tag.name%]">[%tag%]</a>
+[% ":" UNLESS tag == tags.last %]
+[% END %]
+[% END %]
+</h1>
+<div class="related">
+[% SET stuff = request.tag_select(tags,photos) %]
+[% IF stuff.super %]
+Enclosing tags:
+[% FOR tag = stuff.super %]
+<a href="[%base%]/tag/view/[%tag%]">[%tag%]</a>
+[% END %]
+<br>
+[% END %]
+[% IF stuff.cloud %]
+Inside this tag:
+[% stuff.cloud.html() %]
+[% END %]
+</div>
+[% view_page_of(photos) %]
+[% INCLUDE footer %]
--- /dev/null
+<p align="center" style="border: 1px solid black; background: #eee"> Popular tags </p>
+<div class="htmltagcloud">
+[% request.tagcloud.html(75) %]
+</div>
--- /dev/null
+[% INCLUDE header %]
+[% INCLUDE footer %]
--- /dev/null
+<div class="albums">
+[% IF request.user == user %]
+ <h2> Albums </h2>
+ <form method="post" action="[%base%]/user/edit_albums/[%user.id%]">
+ [% IF user.albums.size > 0 %]
+ <table style="width:100%">
+ <tr>
+ <th style="width: 70%"> Name </th> <th> Private? </th> <th> Delete? </th>
+ </tr>
+ [% FOR album = user.albums %]
+ <tr>
+ <td> <a href="[%base%]/album/view/[%album.id%]/">[% album.name %]</a> </td>
+ <td> <input type="checkbox" name="privacy_[% album.id%]"
+ value="1" [% IF album.privacy == 1 %] checked="1" [%END%] />
+ </td>
+ <td> <input type="submit" value="Delete" name="delete_[%album.id%]"/>
+ </td>
+ </tr>
+ [% END %]
+ </table>
+ <input type="submit" value="Submit changes" name="changes">
+ <hr>
+ [% END %]
+ <h3> Create a new album </h3>
+ Name: <input name="new_name"/> <br>
+ Private? <input type="checkbox" name="new_privacy"/> <br>
+ <input type="submit" value="Create" name="create">
+ </form>
+[% ELSE %]
+
+[% IF albums.size > 0 %]
+ <h2> Public albums </h2>
+ <ul>
+ [% FOR album = albums %]
+ <li> <a href="[%base%]/album/view/[%album.id%]">[% album %]</a>
+ [% END %]
+ </ul>
+[% END %]
+
+[% END %]
+</div>
--- /dev/null
+[% INCLUDE header %]
+[% PROCESS macros %]
+
+<table class="userlist">
+[% WHILE objects.size > 0 %]
+<tr>
+ [% SET minilist = objects.splice(0,3) %]
+ [% FOR user = minilist %]
+ <td>
+ [% SET photos = user.photos %]
+ <a href="[%base%]/user/view/[%user.id%]">[% user %]<br/>
+ [% IF photos.last %]
+ <img src="[% photos.last.thumb_url |uri%]"></a><br/>
+ <span class="info">
+ [%photos.size %]
+ photo[%-"s" IF photos.size != 1 %]</span>
+ [% ELSE %]
+ <p><i>(No photos)</i></p>
+ [% END %]
+ </td>
+ [% END %]
+</tr>
+[% END %]
+</table>
+[% INCLUDE footer %]
--- /dev/null
+[% INCLUDE header %]
+[% PROCESS macros %]
+<h1> [% user.name %] </h1>
+[% INCLUDE album_list %]
+
+[% view_page_of(photos) %]
+[% INCLUDE footer %]