]> git.decadent.org.uk Git - memories.git/commitdiff
Branched from version 1.1. 1.1
authorBen Hutchings <ben@decadent.org.uk>
Fri, 30 Dec 2005 13:14:01 +0000 (13:14 +0000)
committerBen Hutchings <ben@decadent.org.uk>
Fri, 30 Dec 2005 13:14:01 +0000 (13:14 +0000)
68 files changed:
Changes [new file with mode: 0644]
Memories.pm [new file with mode: 0644]
Memories/Album.pm [new file with mode: 0644]
Memories/Comment.pm [new file with mode: 0644]
Memories/Config.pm [new file with mode: 0644]
Memories/DBI.pm [new file with mode: 0644]
Memories/Photo.pm [new file with mode: 0644]
Memories/SystemTag.pm [new file with mode: 0644]
Memories/Tag.pm [new file with mode: 0644]
Memories/User.pm [new file with mode: 0644]
README [new file with mode: 0644]
memories.sql [new file with mode: 0644]
tag_dates [new file with mode: 0644]
templates/album/denied [new file with mode: 0644]
templates/album/list [new file with mode: 0644]
templates/album/view [new file with mode: 0644]
templates/album_adder [new file with mode: 0644]
templates/calendar [new file with mode: 0644]
templates/custom/list [new file with mode: 0644]
templates/footer [new file with mode: 0644]
templates/frontpage [new file with mode: 0644]
templates/header [new file with mode: 0644]
templates/login_box [new file with mode: 0644]
templates/macros [new file with mode: 0644]
templates/memories.css [new file with mode: 0644]
templates/memories.png [new file with mode: 0644]
templates/nav [new file with mode: 0644]
templates/pager [new file with mode: 0644]
templates/photo/comment [new file with mode: 0644]
templates/photo/exif [new file with mode: 0644]
templates/photo/recent [new file with mode: 0644]
templates/photo/tagedit [new file with mode: 0644]
templates/photo/upload [new file with mode: 0644]
templates/photo/view [new file with mode: 0644]
templates/picture/view [new file with mode: 0644]
templates/recent.rss [new file with mode: 0644]
templates/rhs [new file with mode: 0644]
templates/static/1-s.gif [new file with mode: 0644]
templates/static/1.gif [new file with mode: 0644]
templates/static/2-s.gif [new file with mode: 0644]
templates/static/2.gif [new file with mode: 0644]
templates/static/3-s.gif [new file with mode: 0644]
templates/static/3.gif [new file with mode: 0644]
templates/static/4-s.gif [new file with mode: 0644]
templates/static/4.gif [new file with mode: 0644]
templates/static/5-s.gif [new file with mode: 0644]
templates/static/5.gif [new file with mode: 0644]
templates/static/6-s.gif [new file with mode: 0644]
templates/static/6.gif [new file with mode: 0644]
templates/static/7-s.gif [new file with mode: 0644]
templates/static/7.gif [new file with mode: 0644]
templates/static/8-s.gif [new file with mode: 0644]
templates/static/8.gif [new file with mode: 0644]
templates/static/9-s.gif [new file with mode: 0644]
templates/static/9.gif [new file with mode: 0644]
templates/static/memories.css [new file with mode: 0644]
templates/static/memories.png [new file with mode: 0644]
templates/static/upload.js [new file with mode: 0644]
templates/systemtag/list [new file with mode: 0644]
templates/systemtag/view [new file with mode: 0644]
templates/tag/list [new file with mode: 0644]
templates/tag/list_js [new file with mode: 0644]
templates/tag/view [new file with mode: 0644]
templates/tagcloud [new file with mode: 0644]
templates/test [new file with mode: 0644]
templates/user/album_list [new file with mode: 0644]
templates/user/list [new file with mode: 0644]
templates/user/view [new file with mode: 0644]

diff --git a/Changes b/Changes
new file mode 100644 (file)
index 0000000..de9c121
--- /dev/null
+++ b/Changes
@@ -0,0 +1,9 @@
+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/Memories.pm b/Memories.pm
new file mode 100644 (file)
index 0000000..33fbf40
--- /dev/null
@@ -0,0 +1,148 @@
+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;
diff --git a/Memories/Album.pm b/Memories/Album.pm
new file mode 100644 (file)
index 0000000..b4f869d
--- /dev/null
@@ -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 (file)
index 0000000..2b7f0ca
--- /dev/null
@@ -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 (file)
index 0000000..f874eff
--- /dev/null
@@ -0,0 +1,55 @@
+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;
diff --git a/Memories/DBI.pm b/Memories/DBI.pm
new file mode 100644 (file)
index 0000000..4d3595d
--- /dev/null
@@ -0,0 +1,7 @@
+package Memories::DBI;
+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 (file)
index 0000000..288ac4a
--- /dev/null
@@ -0,0 +1,239 @@
+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;
diff --git a/Memories/SystemTag.pm b/Memories/SystemTag.pm
new file mode 100644 (file)
index 0000000..d4f8439
--- /dev/null
@@ -0,0 +1,62 @@
+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;
diff --git a/Memories/Tag.pm b/Memories/Tag.pm
new file mode 100644 (file)
index 0000000..bcaa6c0
--- /dev/null
@@ -0,0 +1,100 @@
+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;
diff --git a/Memories/User.pm b/Memories/User.pm
new file mode 100644 (file)
index 0000000..2fa82ca
--- /dev/null
@@ -0,0 +1,67 @@
+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;
diff --git a/README b/README
new file mode 100644 (file)
index 0000000..abc3838
--- /dev/null
+++ b/README
@@ -0,0 +1,59 @@
+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.
+
diff --git a/memories.sql b/memories.sql
new file mode 100644 (file)
index 0000000..9ebe256
--- /dev/null
@@ -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/tag_dates b/tag_dates
new file mode 100644 (file)
index 0000000..e44af81
--- /dev/null
+++ b/tag_dates
@@ -0,0 +1,10 @@
+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;
diff --git a/templates/album/denied b/templates/album/denied
new file mode 100644 (file)
index 0000000..7b641a9
--- /dev/null
@@ -0,0 +1,7 @@
+[% INCLUDE header %]
+<div class="warning">
+This album has been marked private, and you do not have permission to
+view it.
+</div>
+
+[% INCLUDE footer; %]
diff --git a/templates/album/list b/templates/album/list
new file mode 100644 (file)
index 0000000..0229289
--- /dev/null
@@ -0,0 +1,25 @@
+[% 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 %]
diff --git a/templates/album/view b/templates/album/view
new file mode 100644 (file)
index 0000000..a8ad02d
--- /dev/null
@@ -0,0 +1,9 @@
+[% 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 %]
diff --git a/templates/album_adder b/templates/album_adder
new file mode 100644 (file)
index 0000000..2c3db73
--- /dev/null
@@ -0,0 +1,9 @@
+[% 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 %]
diff --git a/templates/calendar b/templates/calendar
new file mode 100644 (file)
index 0000000..700072f
--- /dev/null
@@ -0,0 +1,43 @@
+<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%]">&laquo; </a></th>
+        <th colspan="5"> 
+            [% date.strftime("%Y-%m") %] 
+        </th>
+        <th> [% IF next %]
+            <a href="?view_cal=[%next.ymd%]">&raquo; </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>
diff --git a/templates/custom/list b/templates/custom/list
new file mode 100644 (file)
index 0000000..3dd50b3
--- /dev/null
@@ -0,0 +1,20 @@
+[% 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 %]
diff --git a/templates/footer b/templates/footer
new file mode 100644 (file)
index 0000000..64186ac
--- /dev/null
@@ -0,0 +1,9 @@
+</div>
+</td>
+    <td id="rhs" valign="top">
+        [% INCLUDE rhs %]
+    </td>
+</tr>
+</table>
+</body>
+</html>
diff --git a/templates/frontpage b/templates/frontpage
new file mode 100644 (file)
index 0000000..2b36f1e
--- /dev/null
@@ -0,0 +1,35 @@
+[% 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 %]
diff --git a/templates/header b/templates/header
new file mode 100644 (file)
index 0000000..8dc15a6
--- /dev/null
@@ -0,0 +1,23 @@
+<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">
diff --git a/templates/login_box b/templates/login_box
new file mode 100644 (file)
index 0000000..b875129
--- /dev/null
@@ -0,0 +1,34 @@
+    [% 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 %]
diff --git a/templates/macros b/templates/macros
new file mode 100644 (file)
index 0000000..f97cdd3
--- /dev/null
@@ -0,0 +1,62 @@
+[% 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 %]
diff --git a/templates/memories.css b/templates/memories.css
new file mode 100644 (file)
index 0000000..ac5608e
--- /dev/null
@@ -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 (file)
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 (file)
index 0000000..f42bb06
--- /dev/null
@@ -0,0 +1,11 @@
+<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>
diff --git a/templates/pager b/templates/pager
new file mode 100644 (file)
index 0000000..8ff1ed0
--- /dev/null
@@ -0,0 +1,19 @@
+[% 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 %]
diff --git a/templates/photo/comment b/templates/photo/comment
new file mode 100644 (file)
index 0000000..498e5b6
--- /dev/null
@@ -0,0 +1,18 @@
+<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>
diff --git a/templates/photo/exif b/templates/photo/exif
new file mode 100644 (file)
index 0000000..b60ddbc
--- /dev/null
@@ -0,0 +1,16 @@
+[% 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> &nbsp; </td> [% END %]
+    [% SET counter = counter + 1 %]
+    [% END %]
+    </table>
+[% END %]
diff --git a/templates/photo/recent b/templates/photo/recent
new file mode 100644 (file)
index 0000000..37d1ae3
--- /dev/null
@@ -0,0 +1,18 @@
+[% 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 %]
diff --git a/templates/photo/tagedit b/templates/photo/tagedit
new file mode 100644 (file)
index 0000000..a34d8bd
--- /dev/null
@@ -0,0 +1,21 @@
+<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>
diff --git a/templates/photo/upload b/templates/photo/upload
new file mode 100644 (file)
index 0000000..d17d3cb
--- /dev/null
@@ -0,0 +1,63 @@
+<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> &nbsp; </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 %]
diff --git a/templates/photo/view b/templates/photo/view
new file mode 100644 (file)
index 0000000..33b562b
--- /dev/null
@@ -0,0 +1,86 @@
+[% 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>&nbsp;(";
+    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;
+%]
diff --git a/templates/picture/view b/templates/picture/view
new file mode 100644 (file)
index 0000000..976b786
--- /dev/null
@@ -0,0 +1,7 @@
+[% 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>
diff --git a/templates/recent.rss b/templates/recent.rss
new file mode 100644 (file)
index 0000000..51be582
--- /dev/null
@@ -0,0 +1,22 @@
+<?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>
diff --git a/templates/rhs b/templates/rhs
new file mode 100644 (file)
index 0000000..e23114a
--- /dev/null
@@ -0,0 +1,33 @@
+<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 %]
diff --git a/templates/static/1-s.gif b/templates/static/1-s.gif
new file mode 100644 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
index 0000000..45752c9
--- /dev/null
@@ -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 (file)
index 0000000..97847d1
Binary files /dev/null and b/templates/static/memories.png differ
diff --git a/templates/static/upload.js b/templates/static/upload.js
new file mode 100644 (file)
index 0000000..0b033c6
--- /dev/null
@@ -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={'&lt;':'<','&gt;':'>','&amp;':'&','&quot;':'"'};
+
+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' }
diff --git a/templates/systemtag/list b/templates/systemtag/list
new file mode 100644 (file)
index 0000000..9a7361a
--- /dev/null
@@ -0,0 +1,36 @@
+[% 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 %]
diff --git a/templates/systemtag/view b/templates/systemtag/view
new file mode 100644 (file)
index 0000000..42e885d
--- /dev/null
@@ -0,0 +1,14 @@
+[% 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 %]
diff --git a/templates/tag/list b/templates/tag/list
new file mode 100644 (file)
index 0000000..9a7361a
--- /dev/null
@@ -0,0 +1,36 @@
+[% 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 %]
diff --git a/templates/tag/list_js b/templates/tag/list_js
new file mode 100644 (file)
index 0000000..af9b159
--- /dev/null
@@ -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 (file)
index 0000000..8ce4707
--- /dev/null
@@ -0,0 +1,28 @@
+[% 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 %]
diff --git a/templates/tagcloud b/templates/tagcloud
new file mode 100644 (file)
index 0000000..bd46663
--- /dev/null
@@ -0,0 +1,4 @@
+<p align="center" style="border: 1px solid black; background: #eee"> Popular tags </p>
+<div class="htmltagcloud">
+[% request.tagcloud.html(75) %]
+</div>
diff --git a/templates/test b/templates/test
new file mode 100644 (file)
index 0000000..b5f5031
--- /dev/null
@@ -0,0 +1,2 @@
+[% INCLUDE header %]
+[% INCLUDE footer %]
diff --git a/templates/user/album_list b/templates/user/album_list
new file mode 100644 (file)
index 0000000..59bcad0
--- /dev/null
@@ -0,0 +1,41 @@
+<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>
diff --git a/templates/user/list b/templates/user/list
new file mode 100644 (file)
index 0000000..66ba34e
--- /dev/null
@@ -0,0 +1,25 @@
+[% 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 %]
diff --git a/templates/user/view b/templates/user/view
new file mode 100644 (file)
index 0000000..b3cadec
--- /dev/null
@@ -0,0 +1,7 @@
+[% INCLUDE header %]
+[% PROCESS macros %]
+<h1> [% user.name %] </h1>
+[% INCLUDE album_list %]
+
+[% view_page_of(photos) %]
+[% INCLUDE footer %]