├── .gitignore ├── test.db ├── .travis.yml ├── public ├── images │ ├── box-top.png │ ├── background.gif │ ├── box-bottom.png │ ├── box-middle.gif │ ├── profile_top.png │ └── bender_promo.png └── css │ └── main.css ├── lib ├── MojoFull │ ├── Home.pm │ ├── Blogs.pm │ └── Photos.pm ├── Schema │ ├── ResultSet │ │ ├── Photo.pm │ │ ├── Photoset.pm │ │ └── Blog.pm │ └── Result │ │ ├── BlogTag.pm │ │ ├── Blog.pm │ │ ├── Photo.pm │ │ └── Photoset.pm ├── Schema.pm ├── MojoFull.pm └── Test │ └── Database.pm ├── script ├── new_db ├── mojo_full └── generate_schema ├── t ├── home.t ├── .prove ├── schema │ ├── blog_tag.t │ ├── photoset.t │ ├── blog.t │ └── photo.t ├── fixtures │ ├── Blog.pl │ └── Photoset.pl ├── blogs.t └── photos.t ├── cpanfile ├── templates ├── blogs │ ├── show.html.ep │ └── index.html.ep ├── photos │ ├── index.html.ep │ ├── show_set.html.ep │ └── show.html.ep ├── layouts │ └── default.html.ep └── home │ └── index.html.ep └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .prove 2 | cover_db 3 | local 4 | -------------------------------------------------------------------------------- /test.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tempire/MojoExample/HEAD/test.db -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: perl 2 | perl: 3 | - "5.14" 4 | - "5.12" 5 | - "5.10" 6 | -------------------------------------------------------------------------------- /public/images/box-top.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tempire/MojoExample/HEAD/public/images/box-top.png -------------------------------------------------------------------------------- /public/images/background.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tempire/MojoExample/HEAD/public/images/background.gif -------------------------------------------------------------------------------- /public/images/box-bottom.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tempire/MojoExample/HEAD/public/images/box-bottom.png -------------------------------------------------------------------------------- /public/images/box-middle.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tempire/MojoExample/HEAD/public/images/box-middle.gif -------------------------------------------------------------------------------- /public/images/profile_top.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tempire/MojoExample/HEAD/public/images/profile_top.png -------------------------------------------------------------------------------- /public/images/bender_promo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tempire/MojoExample/HEAD/public/images/bender_promo.png -------------------------------------------------------------------------------- /lib/MojoFull/Home.pm: -------------------------------------------------------------------------------- 1 | package MojoFull::Home; 2 | use Mojo::Base 'Mojolicious::Controller'; 3 | 4 | sub index { 5 | shift->render('home/index'); 6 | } 7 | 8 | 1; 9 | -------------------------------------------------------------------------------- /script/new_db: -------------------------------------------------------------------------------- 1 | use Modern::Perl; 2 | use lib 'lib'; 3 | use Schema; 4 | use Test::Database; 5 | 6 | my $schema = 7 | Test::Database->new->create(Schema => $ENV{TEST_DB} || 'test.db'); 8 | -------------------------------------------------------------------------------- /t/home.t: -------------------------------------------------------------------------------- 1 | use Modern::Perl; 2 | use Test::More; 3 | use Test::Mojo; 4 | use Schema; 5 | use Test::Database; 6 | 7 | my $schema = Test::Database->new->create(Schema => 'test.db'); 8 | 9 | my $t = Test::Mojo->new('MojoFull'); 10 | $t->app->schema($schema); 11 | 12 | $t->get_ok('/')->status_is(200)->text_is(h1 => 'Purpose'); 13 | 14 | done_testing; 15 | -------------------------------------------------------------------------------- /t/.prove: -------------------------------------------------------------------------------- 1 | --- 2 | generation: 2 3 | last_run_time: 1328567377.29599 4 | tests: 5 | home.t: 6 | elapsed: 0.128877878189087 7 | gen: 2 8 | last_fail_time: 1328567377.29512 9 | last_result: 1 10 | last_run_time: 1328567377.29512 11 | last_todo: 0 12 | mtime: 1328567341 13 | seq: 2 14 | total_failures: 2 15 | version: 1 16 | ... 17 | -------------------------------------------------------------------------------- /t/schema/blog_tag.t: -------------------------------------------------------------------------------- 1 | use Modern::Perl; 2 | use Test::More; 3 | use Schema; 4 | use Test::Database; 5 | 6 | my $schema = Test::Database->new->create(Schema => ':memory:'); 7 | 8 | my $blog_id = '1'; 9 | my $tag_id = '1'; 10 | 11 | my $tag = $schema->resultset('BlogTag')->find($tag_id); 12 | 13 | is $tag->blog->id => $blog_id, 'related blog'; 14 | 15 | done_testing; 16 | -------------------------------------------------------------------------------- /cpanfile: -------------------------------------------------------------------------------- 1 | requires 'Mojolicious' => '4.0'; 2 | requires 'Modern::Perl' => 0; 3 | requires 'DBIx::Class' => 0; 4 | requires 'DateTime' => 0; 5 | requires 'DateTime::Format::SQLite' => 0; 6 | requires 'Time::Duration' => 0; 7 | requires 'File::Slurp' => 0; 8 | requires 'SQL::Translator' => 0; 9 | requires 'DBIx::Class::Schema::Loader' => 0; 10 | -------------------------------------------------------------------------------- /lib/Schema/ResultSet/Photo.pm: -------------------------------------------------------------------------------- 1 | package Schema::ResultSet::Photo; 2 | 3 | use Modern::Perl; 4 | use base 'DBIx::Class::ResultSet'; 5 | 6 | sub latest { 7 | my $self = shift; 8 | my $count = shift; 9 | 10 | return $self->search( undef, 11 | { order_by => { -desc => 'taken' }, page => 1, rows => $count } ); 12 | } 13 | 14 | 1; 15 | 16 | =head1 NAME 17 | 18 | Schema::ResultSet::Photoset 19 | 20 | =head1 METHODS 21 | 22 | =head2 latest 23 | 24 | Return latest photos by count 25 | 26 | =cut 27 | -------------------------------------------------------------------------------- /lib/Schema.pm: -------------------------------------------------------------------------------- 1 | use utf8; 2 | package Schema; 3 | 4 | # Created by DBIx::Class::Schema::Loader 5 | # DO NOT MODIFY THE FIRST PART OF THIS FILE 6 | 7 | use strict; 8 | use warnings; 9 | 10 | use base 'DBIx::Class::Schema'; 11 | 12 | __PACKAGE__->load_namespaces; 13 | 14 | 15 | # Created by DBIx::Class::Schema::Loader v0.07015 @ 2012-02-05 21:26:43 16 | # DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:R2szMzKfftygv45g1+A03g 17 | 18 | 19 | # You can replace this text with custom code or comments, and it will be preserved on regeneration 20 | 1; 21 | -------------------------------------------------------------------------------- /script/mojo_full: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl 2 | use Mojo::Base -strict; 3 | 4 | use File::Basename 'dirname'; 5 | use File::Spec; 6 | 7 | use lib join '/', File::Spec->splitdir(dirname(__FILE__)), 'lib'; 8 | use lib join '/', File::Spec->splitdir(dirname(__FILE__)), '..', 'lib'; 9 | 10 | # Check if Mojolicious is installed; 11 | die <start_app('MojoFull'); 19 | -------------------------------------------------------------------------------- /script/generate_schema: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl 2 | 3 | use Modern::Perl; 4 | use DBIx::Class::Schema::Loader 'make_schema_at'; 5 | 6 | my $DEBUG = @ARGV and $ARGV[0] =~ /^\-[\-]*v/; 7 | 8 | say $DBIx::Class::Schema::Loader::VERSION if $DEBUG; 9 | my @dsn = 'dbi:SQLite:dbname=test.db'; 10 | 11 | my $options = { 12 | debug => $DEBUG, 13 | dump_directory => 'lib', 14 | components => [qw/ InflateColumn::DateTime /], 15 | generate_pod => 0, 16 | }; 17 | 18 | make_schema_at(Schema => $options, \@dsn); 19 | 20 | =head1 NAME 21 | 22 | generate_dbic_schema 23 | 24 | =head1 USAGE 25 | 26 | perl generate_dbic_schema 27 | 28 | =cut 29 | -------------------------------------------------------------------------------- /lib/MojoFull/Blogs.pm: -------------------------------------------------------------------------------- 1 | package MojoFull::Blogs; 2 | use Mojo::Base 'Mojolicious::Controller'; 3 | 4 | sub index { 5 | my $self = shift; 6 | 7 | # Specified tags to search for 8 | my @tags = grep $_ ne 'tag' => split '/' => $self->param('tags') 9 | || qw/personal/; 10 | 11 | my @blogs = $self->db->resultset('Blog')->by_tags(@tags) 12 | or return $self->redirect_to('blogs'); 13 | 14 | $self->render('blogs/index', blogs => [@blogs],); 15 | } 16 | 17 | sub show { 18 | my $self = shift; 19 | my $param = $self->stash('name'); 20 | 21 | my $blog = $self->db->resultset('Blog')->by_id_or_name($param) 22 | or return $self->redirect_to('blogs'); 23 | 24 | $self->render('blogs/show', blog => $blog); 25 | } 26 | 27 | 1; 28 | -------------------------------------------------------------------------------- /lib/Schema/ResultSet/Photoset.pm: -------------------------------------------------------------------------------- 1 | package Schema::ResultSet::Photoset; 2 | 3 | use Modern::Perl; 4 | use base 'DBIx::Class::ResultSet'; 5 | 6 | sub by_id { 7 | return shift->find({id => pop}); 8 | } 9 | 10 | sub by_name { 11 | return shift->find({title => {LIKE => pop}}); 12 | } 13 | 14 | sub by_id_or_name { 15 | my ($self, $param) = @_; 16 | return $param =~ /^\d+$/ ? $self->by_id($param) : $self->by_name($param); 17 | } 18 | 19 | sub faces { 20 | return shift->by_id_or_title('Faces of Glen'); 21 | } 22 | 23 | 1; 24 | 25 | =head1 NAME 26 | 27 | Schema::ResultSet::Photoset 28 | 29 | =head1 METHODS 30 | 31 | =head2 id_or_title 32 | 33 | Find photoset by either id or title; 34 | 35 | =head2 faces 36 | 37 | Faces of Glen Photoset 38 | 39 | =cut 40 | -------------------------------------------------------------------------------- /templates/blogs/show.html.ep: -------------------------------------------------------------------------------- 1 | % layout 'default'; 2 | % title 'Phased Blog'; 3 | 4 | 35 | -------------------------------------------------------------------------------- /templates/photos/index.html.ep: -------------------------------------------------------------------------------- 1 | % layout 'default'; 2 | % title int(@$sets) . ' Phased albums'; 3 | 4 | %= $photo_count 5 | photos in 6 | %= int @$sets 7 | albums 8 | 9 |
10 | 11 | % for my $set (reverse @$sets) { 12 | 13 |
15 | 16 | %= link_to '/photos/' . $set->url_title => begin 17 |
<%= $set->photos->count %>
18 | %= image $set->primary_photo->square, alt => 'Primary photo' 19 |
<%= $set->decoded_title %>
20 | %= end 21 | 22 |
23 | %= $set->date->month_name . ', ' . $set->date->year 24 |
25 |
<%= $set->region %>
26 |
27 | 28 | % } 29 | 30 |
31 | -------------------------------------------------------------------------------- /templates/blogs/index.html.ep: -------------------------------------------------------------------------------- 1 | % layout 'default'; 2 | % title 'Phased Blog'; 3 | 4 | %= t ul => id => 'blogs' => begin 5 | 6 | % for my $blog (@$blogs) { 7 | 8 | %= t li => id => $blog->url_title, class => 'blog snippet' => begin 9 | %= t a => name => 'id'.$blog->id 10 | %= t a => name => $blog->url_title 11 | %= t h2 => begin 12 | %= link_to $blog->title => '/blogs/'.$blog->url_title, class => 'more' 13 | % end 14 | 15 | %= t div => class => 'tags' => begin 16 | % for my $tag ($blog->tags) { 17 | %= t span => class => 'tag' => $tag->name 18 | % } 19 | % end 20 | 21 | % if ($blog->subtitle) { 22 | %= t h3 => class => 'subtitle' => $blog->subtitle 23 | % } 24 | 25 | %= t h3 => class => 'time', title => $blog->time_since => $blog->created_time_string 26 | 27 | %= t div => class => 'content snippet' => begin 28 | %= $blog->snippet 29 | %= link_to 'More' => '/blogs/'.$blog->url_title, class => 'more' 30 | %= end 31 | % end 32 | % } 33 | 34 | % end 35 | -------------------------------------------------------------------------------- /lib/Schema/Result/BlogTag.pm: -------------------------------------------------------------------------------- 1 | use utf8; 2 | package Schema::Result::BlogTag; 3 | 4 | # Created by DBIx::Class::Schema::Loader 5 | # DO NOT MODIFY THE FIRST PART OF THIS FILE 6 | 7 | use strict; 8 | use warnings; 9 | 10 | use base 'DBIx::Class::Core'; 11 | __PACKAGE__->load_components("InflateColumn::DateTime"); 12 | __PACKAGE__->table("blog_tags"); 13 | __PACKAGE__->add_columns( 14 | "id", 15 | { data_type => "integer", is_auto_increment => 1, is_nullable => 0 }, 16 | "name", 17 | { data_type => "char", is_nullable => 0, size => 50 }, 18 | "blog", 19 | { data_type => "integer", is_foreign_key => 1, is_nullable => 0 }, 20 | ); 21 | __PACKAGE__->set_primary_key("id"); 22 | __PACKAGE__->belongs_to( 23 | "blog", 24 | "Schema::Result::Blog", 25 | { id => "blog" }, 26 | { is_deferrable => 1, on_delete => "CASCADE", on_update => "CASCADE" }, 27 | ); 28 | 29 | 30 | # Created by DBIx::Class::Schema::Loader v0.07015 @ 2012-02-05 21:35:07 31 | # DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:dkmcVpL+N1FV5CoZ+jiEjQ 32 | 33 | 34 | # You can replace this text with custom code or comments, and it will be preserved on regeneration 35 | 1; 36 | -------------------------------------------------------------------------------- /lib/MojoFull.pm: -------------------------------------------------------------------------------- 1 | package MojoFull; 2 | use Mojo::Base 'Mojolicious'; 3 | use Schema; 4 | 5 | # Connects once for entire application. For real apps, consider using a helper 6 | # that can reconnect on each request if necessary. 7 | has schema => sub { 8 | return Schema->connect('dbi:SQLite:' . ($ENV{TEST_DB} || 'test.db')); 9 | }; 10 | 11 | # This method will run once at server start 12 | sub startup { 13 | my $self = shift; 14 | 15 | $self->helper(db => sub { $self->app->schema }); 16 | 17 | # Routes 18 | my $r = $self->routes; 19 | 20 | # Requested id is a photoset? 21 | $r->add_condition( 22 | photoset => sub { 23 | my ($r, $c, $captures, $pattern) = @_; 24 | 25 | my $id = $captures->{id}; 26 | return 1 if $id !~ /^\d+$/ or $id =~ /^\d+$/ and length $id == 17; 27 | } 28 | ); 29 | 30 | $r->get('/')->to('home#index'); 31 | 32 | $r->get('/photos')->to('photos#index'); 33 | $r->get('/photos/:id')->over('photoset')->to('photos#show_set'); 34 | $r->get('/photos/:id')->to('photos#show'); 35 | 36 | $r->get('/blogs')->to('blogs#index'); 37 | $r->get('/blogs/(:name)')->to('blogs#show'); 38 | $r->get('/blogs/tag/(*tags)')->to('blogs#index'); 39 | } 40 | 41 | 1; 42 | -------------------------------------------------------------------------------- /lib/MojoFull/Photos.pm: -------------------------------------------------------------------------------- 1 | package MojoFull::Photos; 2 | use Mojo::Base 'Mojolicious::Controller'; 3 | 4 | sub index { 5 | my $self = shift; 6 | 7 | $self->render( 8 | 'photos/index', 9 | sets => [$self->db->resultset('Photoset')->search], 10 | photo_count => $self->db->resultset('Photo')->count 11 | ); 12 | } 13 | 14 | sub show { 15 | my $self = shift; 16 | my $id = $self->param('id'); 17 | 18 | my $photo = $self->db->resultset('Photo')->find($id) 19 | or $self->redirect_to('photos'), return; 20 | 21 | $self->render(template => 'photos/show', photo => $photo); 22 | } 23 | 24 | sub show_set { 25 | my $self = shift; 26 | my $id = $self->param('id'); 27 | 28 | my $set = $self->db->resultset('Photoset')->by_id_or_name($id) 29 | or $self->redirect_to('photos'), return; 30 | 31 | $self->render(template => 'photos/show_set', set => $set); 32 | } 33 | 34 | 1; 35 | 36 | =head1 NAME 37 | 38 | MojoFull::Photos 39 | 40 | =head1 DESCRIPTION 41 | 42 | /photos controller 43 | 44 | =head1 ACTIONS 45 | 46 | =head2 index 47 | 48 | GET /photos 49 | 50 | =head2 show 51 | 52 | GET /photos/:photo_id 53 | 54 | =head2 show_set 55 | 56 | GET /photos/:set_id 57 | 58 | =cut 59 | -------------------------------------------------------------------------------- /t/schema/photoset.t: -------------------------------------------------------------------------------- 1 | use Modern::Perl; 2 | use Test::More; 3 | use Schema; 4 | use Test::Database; 5 | 6 | my $schema = Test::Database->new->create(Schema => ':memory:'); 7 | 8 | my $photoset_id = '72157624222825789'; 9 | my $photoset_title = 'robot_arms'; 10 | my $prev_photoset_id = '72157624222820921'; 11 | my $next_photoset_id = '72157624347519408'; 12 | 13 | is $schema->resultset('Photoset')->by_id_or_name($photoset_id)->id => 14 | $photoset_id; 15 | is $schema->resultset('Photoset')->by_id_or_name($photoset_title)->id => 16 | $photoset_id; 17 | 18 | my $set = $schema->resultset('Photoset')->find($photoset_id); 19 | 20 | is ref $set->date => 'DateTime'; 21 | is $set->date->month_abbr => 'Jun'; 22 | is $set->date->year => 2010; 23 | 24 | is $set->primary->id => $set->primary_photo->id, 'primary photo alias'; 25 | is $set->region => 'California'; 26 | is $set->url_title => $photoset_title; 27 | is $set->location => 'Chico, California'; 28 | like $set->time_since => qr/\d+ \w+ and \d+ \w+ ago/; 29 | 30 | is $set->previous->id => $prev_photoset_id, 'previous photoset'; 31 | is $set->next->id => $next_photoset_id, 'next photoset'; 32 | 33 | like $set->time_since => qr/\d+ \w+ and \d+ \w+ ago/, 'time since'; 34 | 35 | done_testing; 36 | -------------------------------------------------------------------------------- /t/fixtures/Blog.pl: -------------------------------------------------------------------------------- 1 | [ Blog => { 2 | id => 1, 3 | title => 'Hello!', 4 | subtitle => 'A subtitle', 5 | created_time => '1302056856', 6 | location => 'Chico, California', 7 | content => q|Hello! content|, 8 | tags => [{name => 'personal'}, {name => 'test'}] 9 | }, 10 | Blog => { 11 | id => 2, 12 | title => 'Tech', 13 | subtitle => 'A subtitle', 14 | created_time => '1777777777', 15 | location => 'Chico, California', 16 | content => 'Tech content', 17 | 18 | tags => [{name => 'tech'}], 19 | }, 20 | Blog => { 21 | id => 3, 22 | title => 'non-tech 2', 23 | subtitle => 'A subtitle', 24 | created_time => '1302056857', 25 | location => 'Chico, California', 26 | content => 'non-tech 2 content', 27 | 28 | tags => [{name => 'personal'}], 29 | }, 30 | Blog => { 31 | id => 4, 32 | title => 'Hidden', 33 | subtitle => 'A subtitle', 34 | created_time => '1888888888', 35 | location => 'Chico, California', 36 | content => 'Hidden content', 37 | 38 | #tags => [{name => 'personal'}, {name => 'hidden'}], 39 | tags => [{name => 'hidden'}], 40 | } 41 | ] 42 | -------------------------------------------------------------------------------- /t/schema/blog.t: -------------------------------------------------------------------------------- 1 | use Modern::Perl; 2 | use Test::More; 3 | use Schema; 4 | use Test::Database; 5 | 6 | my $schema = Test::Database->new->create(Schema => ':memory:'); 7 | 8 | my $blog_id = '1'; 9 | my $blog_title = 'Hello!'; 10 | my $blog_url_title = 'hello_'; 11 | 12 | my $blog = $schema->resultset('Blog')->find($blog_id); 13 | 14 | is $blog->url_title => $blog_url_title, 'url title'; 15 | is ref $blog->created_time => 'DateTime', 'DateTime object'; 16 | is $blog->created_time_string => 'Wednesday, April 6, 2011 at 2:27AM', 17 | 'pretty time string'; 18 | 19 | #ok $blog->snippet; 20 | 21 | # Tags 22 | is $blog->tags->first->name => 'personal', 'blog tag'; 23 | ok !$schema->resultset('Blog')->by_tags(qw/bad/); 24 | is [$schema->resultset('Blog')->by_tags(qw/personal/)]->[0]->title => 25 | $blog_title; 26 | is [$schema->resultset('Blog')->by_tags(qw/test/)]->[0]->title => $blog_title; 27 | is [$schema->resultset('Blog')->by_tags(qw/personal test/)]->[0]->title => 28 | $blog_title; 29 | is $schema->resultset('Blog')->hidden->all => 1, '1 hidden entry'; 30 | is $schema->resultset('Blog')->not_hidden->all => 3, '3 non-hidden entries'; 31 | 32 | # Latest non-hidden entry 33 | is $schema->resultset('Blog')->latest->title => 'Tech', 'latest entry'; 34 | 35 | done_testing; 36 | -------------------------------------------------------------------------------- /templates/photos/show_set.html.ep: -------------------------------------------------------------------------------- 1 | % layout 'default'; 2 | % title $set->title . ' Phased albums'; 3 | 4 |
5 |
6 | All Photos 7 | % if ($set->previous) { 8 | 9 | % } 10 | % if ($set->next) { 11 | 12 | % } 13 |

<%= $set->decoded_title %>

14 |
    15 |
  • 16 | <%= $set->location %>
  • 17 |
  • 18 | <%= $set->date->month_name %>, <%= $set->date->year %>
  • 19 |
  • 20 | <%= $set->time_since %>
  • 21 |
  • 22 | <%= $set->photos->count %> Photo<%= $set->photos->count == 1 ? '' : 's' %>
  • 23 |
24 |
25 |
26 | % foreach my $photo ( $set->photos->all ) { 27 | 31 | 32 | % } 33 |
34 |
35 | -------------------------------------------------------------------------------- /t/schema/photo.t: -------------------------------------------------------------------------------- 1 | use Modern::Perl; 2 | use Test::More; 3 | use Schema; 4 | use Test::Database; 5 | 6 | my $schema = Test::Database->new->create(Schema => ':memory:'); 7 | 8 | my $photo_id = '4729801945'; 9 | my $next_photo_id = '2253839375'; 10 | my $prev_photo_id = '4730456558'; 11 | my $photoset_id = '72157624347519408'; 12 | 13 | my $photo = $schema->resultset('Photo')->find($photo_id); 14 | 15 | is $photo->location => 'Chico, California'; 16 | 17 | ok $photo->update({region => undef}); 18 | is $photo->location => 'Chico'; 19 | 20 | ok $photo->update({locality => undef, region => 'Texas'}); 21 | is $photo->location => 'Texas', 'location'; 22 | 23 | is $photo->set->id => $photoset_id, 'photoset id'; 24 | is $photo->previous->id => $prev_photo_id, 'previous photo'; 25 | is $photo->next->id => $next_photo_id, 'next photo'; 26 | 27 | ok !$photo->previous->previous, 'no previous photo'; 28 | ok !$photo->next->next, 'no next photo'; 29 | 30 | ok $photo->update({idx => undef}), 'clear idx'; 31 | ok !$photo->previous, 'returns false'; 32 | ok !$photo->next, 'returns false'; 33 | 34 | like $photo->time_since => qr/\d+ \w+ and \d+ \w+ ago/, 'time since'; 35 | ok $photo->update({taken => undef}), 'clear taken'; 36 | ok $photo = $schema->resultset('Photo')->find($photo_id), 'refresh'; 37 | ok !$photo->time_since, 'no time since'; 38 | 39 | done_testing; 40 | -------------------------------------------------------------------------------- /t/blogs.t: -------------------------------------------------------------------------------- 1 | use Modern::Perl; 2 | use Test::More; 3 | use Test::Mojo; 4 | use Schema; 5 | use Test::Database; 6 | 7 | my $schema = Test::Database->new->create(Schema => 'test.db'); 8 | 9 | my $t = Test::Mojo->new('MojoFull'); 10 | $t->app->schema($schema); 11 | 12 | # Blog not found 13 | $t->get_ok('/blogs/bad_title')->status_is(302) 14 | ->header_like(Location => qr|/blogs$|); 15 | 16 | # List 17 | $t->get_ok('/blogs')->status_is(200)->element_exists('ul#blogs') 18 | ->element_exists('#blogs > .blog.snippet') 19 | ->text_is('li#hello_ h2 > a' => 'Hello!'); 20 | 21 | # One blog entry 22 | $t->get_ok('/blogs/hello_')->status_is(200)->element_exists('ul#blogs') 23 | ->text_is('h2 > a' => 'Hello!'); 24 | 25 | # Entries by tag 26 | $t->get_ok('/blogs/tag/personal')->status_is(200)->element_exists('ul#blogs') 27 | ->text_is('li#hello_ h2 > a' => 'Hello!') 28 | ->text_is('li#non_tech_2 h2 > a' => 'non-tech 2') 29 | ->element_exists_not('li#tech'); 30 | 31 | # Entries by tag 32 | $t->get_ok('/blogs/tag/tech')->status_is(200) 33 | ->element_exists_not('li#non-tech_2') 34 | ->text_is('li#tech h2 > a' => 'Tech'); 35 | 36 | $t->get_ok('/blogs/tag/tech/tag/personal')->status_is(200) 37 | ->element_exists('ul#blogs')->text_is('li#hello_ h2 > a' => 'Hello!') 38 | ->text_is('li#non_tech_2 h2 > a' => 'non-tech 2') 39 | ->text_is('li#tech h2 > a' => 'Tech'); 40 | 41 | done_testing; 42 | -------------------------------------------------------------------------------- /t/photos.t: -------------------------------------------------------------------------------- 1 | use Modern::Perl; 2 | use Test::More; 3 | use Test::Mojo; 4 | use Schema; 5 | use Test::Database; 6 | 7 | my $schema = Test::Database->new->create(Schema => 'test.db'); 8 | 9 | my $t = Test::Mojo->new('MojoFull'); 10 | $t->app->schema($schema); 11 | 12 | # Photo not found 13 | $t->get_ok('/photos/bad_title')->status_is(302); 14 | 15 | # Photoset not found 16 | $t->get_ok('/photos/12345678912345678')->status_is(302); 17 | 18 | # All sets 19 | $t->get_ok('/photos')->status_is(200) 20 | ->element_exists('div#photosets[class*="thumbnails"]') 21 | ->content_like(qr/\d+\s+photos in\s+\d+\s+albums/i); 22 | 23 | ok my $set_id = 24 | $t->tx->res->dom('div#photosets > div.photo')->[0]->{'id'}, 25 | 'set id'; 26 | ok my $set_url = 27 | $t->tx->res->dom('div#photosets > div.photo > a')->[0]->{'href'}, 28 | 'set url'; 29 | ok my $set_title = 30 | $t->tx->res->dom('div#photosets > div.photo > a div.title')->[0]->text, 31 | 'set title'; 32 | 33 | is length $set_id => 17; 34 | 35 | # Show set 36 | $t->get_ok($set_url)->status_is(200)->text_is(h1 => $set_title); 37 | $t->get_ok("/photos/$set_id")->status_is(200)->text_is(h1 => $set_title); 38 | $t->get_ok("/photos/$set_title")->status_is(200)->text_is(h1 => $set_title); 39 | 40 | ok my $photo_url = 41 | $t->tx->res->dom('div.photoset a.slide')->[0]->{'href'}, 'photo url'; 42 | like $photo_url => qr|^/photos/\d+$|; 43 | 44 | # Photo 45 | $t->get_ok($photo_url)->status_is(200)->text_like('h1 a.title' => qr/$set_title/); 46 | 47 | done_testing; 48 | -------------------------------------------------------------------------------- /lib/Schema/ResultSet/Blog.pm: -------------------------------------------------------------------------------- 1 | package Schema::ResultSet::Blog; 2 | 3 | use Modern::Perl; 4 | use base 'DBIx::Class::ResultSet'; 5 | 6 | sub by_id { 7 | return shift->find({id => pop}); 8 | } 9 | 10 | sub by_name { 11 | return shift->find({title => {LIKE => pop}}); 12 | } 13 | 14 | sub by_id_or_name { 15 | my ($self, $param) = @_; 16 | return $param =~ /^\d+$/ ? $self->by_id($param) : $self->by_name($param); 17 | } 18 | 19 | sub by_tags { 20 | my ($self, @tags) = @_; 21 | 22 | return if !@tags; 23 | 24 | my $tags = $self->search_related(tags => {name => [@tags]}); 25 | 26 | return map $_->blog => $tags->all; 27 | } 28 | 29 | sub latest { 30 | return 31 | shift->not_hidden->search({}, 32 | {order_by => {-desc => 'created_time'}, rows => 1})->single; 33 | } 34 | 35 | sub hidden { 36 | return shift->search( 37 | {}, 38 | { join => 'tags', 39 | group_by => 'me.id', 40 | where => {'tags.name' => 'hidden'} 41 | } 42 | ); 43 | } 44 | 45 | sub not_hidden { 46 | return shift->search( 47 | {}, 48 | { join => 'tags', 49 | group_by => 'me.id', 50 | where => {'tags.name' => {'!=' => 'hidden'}} 51 | } 52 | ); 53 | } 54 | 55 | 1; 56 | 57 | =head1 NAME 58 | 59 | Schema::ResultSet::Blog 60 | 61 | =head1 METHODS 62 | 63 | =head2 by_id 64 | 65 | Find one blog entry by id 66 | 67 | =head2 by_name 68 | 69 | Find one blog entry by title 70 | 71 | =head2 by_tags 72 | 73 | Search blogs by related tags 74 | 75 | =head2 latest 76 | 77 | Most recently posted entry, chained from L 78 | 79 | =head2 hidden 80 | 81 | Hidden entries (by 'hidden' tag) 82 | 83 | =head2 not_hidden 84 | 85 | Available entries 86 | 87 | =cut 88 | -------------------------------------------------------------------------------- /templates/photos/show.html.ep: -------------------------------------------------------------------------------- 1 | % layout 'default'; 2 | % my $title = $photo->set->decoded_title . '-' . $photo->idx . ' of ' . $photo->set->photos->count; 3 | % title $title; 4 | 5 | 6 |
7 | 8 |

12 | %= $photo->set->decoded_title 13 | 14 |

15 | 16 | 26 | 27 |
    28 |
  • 29 | <%= $photo->location %>
  • 30 |
  • 31 | <%= $photo->taken->month_name %>, <%= $photo->taken->year %>
  • 32 |
  • 33 | <%= $photo->time_since %>
  • 34 |
  • 35 | <%= $photo->idx %> of <%= $photo->set->photos->count %>
  • 36 |
37 | 38 |
39 | 40 |
41 | % if ($photo->next) { 42 | 44 | % } 45 | % else { 46 | 48 | % } 49 |
50 | 51 |
52 | -------------------------------------------------------------------------------- /templates/layouts/default.html.ep: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 18 | 19 | 20 | 21 | 38 | 39 |
40 |
41 | 42 |
43 | <%= content %> 44 |
45 | 46 |
47 |
48 | 49 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /lib/Schema/Result/Blog.pm: -------------------------------------------------------------------------------- 1 | use utf8; 2 | package Schema::Result::Blog; 3 | 4 | # Created by DBIx::Class::Schema::Loader 5 | # DO NOT MODIFY THE FIRST PART OF THIS FILE 6 | 7 | use strict; 8 | use warnings; 9 | 10 | use base 'DBIx::Class::Core'; 11 | __PACKAGE__->load_components("InflateColumn::DateTime"); 12 | __PACKAGE__->table("blogs"); 13 | __PACKAGE__->add_columns( 14 | "id", 15 | { data_type => "integer", is_auto_increment => 1, is_nullable => 0 }, 16 | "title", 17 | { data_type => "char", is_nullable => 0, size => 100 }, 18 | "subtitle", 19 | { data_type => "tinytext", is_nullable => 1 }, 20 | "content", 21 | { data_type => "text", is_nullable => 0 }, 22 | "created_time", 23 | { data_type => "char", is_nullable => 0, size => 20 }, 24 | "timestamp", 25 | { 26 | data_type => "timestamp", 27 | default_value => \"current_timestamp", 28 | is_nullable => 0, 29 | }, 30 | "location", 31 | { data_type => "char", is_nullable => 1, size => 100 }, 32 | ); 33 | __PACKAGE__->set_primary_key("id"); 34 | __PACKAGE__->has_many( 35 | "blog_tags", 36 | "Schema::Result::BlogTag", 37 | { "foreign.blog" => "self.id" }, 38 | { cascade_copy => 0, cascade_delete => 0 }, 39 | ); 40 | 41 | 42 | # Created by DBIx::Class::Schema::Loader v0.07015 @ 2012-02-05 21:35:07 43 | # DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:Pu8So1Y80nFfvuHrqLpqag 44 | 45 | use Time::Duration; 46 | use DateTime; 47 | 48 | # Convert date strings to datetime objects, and vice versa 49 | __PACKAGE__->inflate_column( 50 | "created_time", 51 | { inflate => sub { DateTime->from_epoch(epoch => shift); }, 52 | deflate => sub { shift->epoch; }, 53 | } 54 | ); 55 | 56 | __PACKAGE__->has_many( 57 | "tags", "Schema::Result::BlogTag", 58 | {"foreign.blog" => "self.id"}, 59 | {cascade_copy => 0, cascade_delete => 0}, 60 | ); 61 | 62 | sub url_title { 63 | my $self = shift; 64 | my $title = $self->title; 65 | 66 | $title =~ s/\W/_/g; 67 | 68 | return lc $title; 69 | } 70 | 71 | sub time_since { 72 | return Time::Duration::ago(time - shift->created_time->epoch); 73 | } 74 | 75 | sub created_time_string { 76 | return shift->created_time->strftime("%A, %B %e, %Y at %l:%M%p"); 77 | } 78 | 79 | sub snippet { 80 | return shift->content; 81 | } 82 | 83 | 1; 84 | 85 | =head1 RELATIONSHIPS 86 | 87 | =head2 tags 88 | 89 | Type: has_many 90 | 91 | Related object: L 92 | 93 | Alias for: L 94 | 95 | =head1 METHODS 96 | 97 | =head2 url_title 98 | 99 | Title for readable URLs 100 | 101 | =head2 time_since 102 | 103 | Time since blog was entered 104 | 105 | =head2 created_time_string 106 | 107 | pretty created time string 108 | 109 | =cut 110 | -------------------------------------------------------------------------------- /lib/Test/Database.pm: -------------------------------------------------------------------------------- 1 | package Test::Database; 2 | 3 | use Modern::Perl; 4 | use lib 'lib'; 5 | use Carp; 6 | use Cwd qw/ abs_path getcwd /; 7 | use File::Slurp 'slurp'; 8 | use constant DEBUG => $ENV{DEBUG}; 9 | 10 | our $schema; 11 | 12 | use Mojo::Base -base; 13 | has fixture_path => 't/fixtures'; 14 | 15 | sub import { 16 | my ($self, $schema_name, $file) = @_; 17 | 18 | return if @_ < 3; 19 | 20 | # Export connected schema to $schema 21 | no strict 'refs'; 22 | my $caller = caller; 23 | *{"$caller\::schema"} = $self->create($schema_name => $file); 24 | } 25 | 26 | sub create { 27 | my $self = shift; 28 | my $schema_name = shift; 29 | my $file = shift || $ENV{TEST_DB} || ':memory:'; 30 | 31 | # Remove previous 32 | unlink $file if -e $file; 33 | 34 | # New db 35 | my $dsn = "dbi:SQLite:dbname=$file"; 36 | my $schema = 37 | $schema_name->connect($dsn, '', '', {quote_char => '`', name_sep => '.'}); 38 | $schema->deploy; 39 | 40 | # Fixtures 41 | $self->insert_fixtures($file, $schema); 42 | 43 | return $schema; 44 | } 45 | 46 | sub insert_fixtures { 47 | my $self = shift; 48 | my $file = shift; 49 | my $schema = shift; 50 | 51 | # Store working dir 52 | my $cwd = getcwd; 53 | 54 | chdir $self->fixture_path; 55 | 56 | foreach my $fixture (<*>) { 57 | 58 | warn "$fixture" if DEBUG; 59 | 60 | my $info = eval slurp $fixture; 61 | chdir $cwd, croak "Could not insert fixture $fixture: $@" if $@; 62 | 63 | # Arrayrefs of rows, (dbic syntax) table defined by fixture filename 64 | if (ref $info->[0] eq 'HASH') { 65 | my $rs_name = (split /\./, $fixture)[0]; 66 | $rs_name =~ s/s$//; 67 | 68 | # list context, so that populate uses dbic ->insert overrides 69 | my @noop = $schema->resultset(ucfirst $rs_name)->populate($info); 70 | 71 | next; 72 | } 73 | 74 | # Arrayref of hashrefs, multiple tables per file 75 | for (my $i = 0; $i < @$info; $i++) { 76 | $schema->resultset($info->[$i])->create($info->[++$i]); 77 | } 78 | } 79 | 80 | # Restore working dir 81 | chdir $cwd; 82 | } 83 | 84 | sub disconnect { 85 | return shift->storage->dbh->disconnect; 86 | } 87 | 88 | 1; 89 | 90 | =head1 NAME 91 | 92 | Test::Database 93 | 94 | =head1 DESCRIPTION 95 | 96 | Deploy schema & load fixtures 97 | 98 | =head1 USAGE 99 | 100 | # Creates an sqlite3 test.db database from DBIC Schema 101 | my $schema = Test::Database->new->create(Schema => 'test.db'); 102 | 103 | # Creates an in-memory sqlite3 database from DBIC Schema 104 | my $schema = Test::Database->new->create(Schema => ':memory:'); 105 | 106 | =head1 METHODS 107 | 108 | =head2 import 109 | 110 | Allows for compile time generation of database. 111 | Exports the $schema into the current namespace: 112 | 113 | use Test::Database Schema => 'test.db'; 114 | print $schema->sources; 115 | 116 | =head2 create ($schema_name, $file_name) 117 | 118 | Create new sqlite3 database from DBIC schema 119 | 120 | =head2 insert_fixtures 121 | 122 | Insert fixtures into sqlite3 database 123 | 124 | =head2 disconnect ($schema) 125 | 126 | Disconnect from database handle 127 | 128 | =cut 129 | -------------------------------------------------------------------------------- /lib/Schema/Result/Photo.pm: -------------------------------------------------------------------------------- 1 | use utf8; 2 | package Schema::Result::Photo; 3 | 4 | # Created by DBIx::Class::Schema::Loader 5 | # DO NOT MODIFY THE FIRST PART OF THIS FILE 6 | 7 | use strict; 8 | use warnings; 9 | 10 | use base 'DBIx::Class::Core'; 11 | __PACKAGE__->load_components("InflateColumn::DateTime"); 12 | __PACKAGE__->table("photos"); 13 | __PACKAGE__->add_columns( 14 | "id", 15 | { data_type => "char", default_value => "", is_nullable => 0, size => 20 }, 16 | "description", 17 | { data_type => "char", is_nullable => 1, size => 255 }, 18 | "lat", 19 | { data_type => "char", is_nullable => 1, size => 10 }, 20 | "lon", 21 | { data_type => "char", is_nullable => 1, size => 10 }, 22 | "region", 23 | { data_type => "char", is_nullable => 1, size => 20 }, 24 | "locality", 25 | { data_type => "char", is_nullable => 1, size => 20 }, 26 | "country", 27 | { data_type => "char", is_nullable => 1, size => 20 }, 28 | "square", 29 | { data_type => "tinytext", is_nullable => 1 }, 30 | "original_url", 31 | { data_type => "tinytext", is_nullable => 1 }, 32 | "taken", 33 | { data_type => "datetime", is_nullable => 1 }, 34 | "isprimary", 35 | { data_type => "char", is_nullable => 1, size => 1 }, 36 | "small", 37 | { data_type => "tinytext", is_nullable => 1 }, 38 | "medium", 39 | { data_type => "tinytext", is_nullable => 1 }, 40 | "original", 41 | { data_type => "tinytext", is_nullable => 1 }, 42 | "thumbnail", 43 | { data_type => "tinytext", is_nullable => 1 }, 44 | "large", 45 | { data_type => "tinytext", is_nullable => 1 }, 46 | "is_glen", 47 | { data_type => "char", is_nullable => 1, size => 1 }, 48 | "idx", 49 | { data_type => "integer", is_nullable => 1 }, 50 | "photoset", 51 | { data_type => "char", is_foreign_key => 1, is_nullable => 1, size => 20 }, 52 | ); 53 | __PACKAGE__->set_primary_key("id"); 54 | __PACKAGE__->belongs_to( 55 | "photoset", 56 | "Schema::Result::Photoset", 57 | { id => "photoset" }, 58 | { 59 | is_deferrable => 1, 60 | join_type => "LEFT", 61 | on_delete => "CASCADE", 62 | on_update => "CASCADE", 63 | }, 64 | ); 65 | __PACKAGE__->has_many( 66 | "photosets", 67 | "Schema::Result::Photoset", 68 | { "foreign.primary_photo" => "self.id" }, 69 | { cascade_copy => 0, cascade_delete => 0 }, 70 | ); 71 | 72 | 73 | # Created by DBIx::Class::Schema::Loader v0.07015 @ 2012-02-05 21:35:07 74 | # DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:hBwyD8s8bHZ6aae1bl2a3Q 75 | 76 | use Time::Duration; 77 | 78 | __PACKAGE__->belongs_to( 79 | "set", 80 | "Schema::Result::Photoset", 81 | {id => "photoset"}, 82 | { is_deferrable => 1, 83 | join_type => "LEFT", 84 | on_delete => "CASCADE", 85 | on_update => "CASCADE", 86 | }, 87 | ); 88 | 89 | sub location { 90 | return join ', ' => grep {defined} map { $_->locality, $_->region } shift; 91 | } 92 | 93 | sub next { 94 | return $_->result_source->resultset->find( 95 | { photoset => $_->set->id, 96 | idx => $_->idx + 1 97 | } 98 | ) for grep $_->idx => shift; 99 | } 100 | 101 | sub previous { 102 | my $self = shift; 103 | 104 | return if !$self->idx; 105 | 106 | return $self->result_source->resultset->find( 107 | { photoset => $self->set->id, 108 | idx => $self->idx - 1 109 | } 110 | ); 111 | } 112 | 113 | sub time_since { 114 | return Time::Duration::ago(time - $_->taken->epoch) 115 | for grep $_->taken => shift; 116 | } 117 | 118 | =head1 RELATIONSHIPS 119 | 120 | =head2 set 121 | 122 | Type: belongs_to 123 | 124 | Related object: L 125 | 126 | Alias for L 127 | 128 | =head1 METHODS 129 | 130 | =head2 location 131 | 132 | City, State 133 | 134 | =head2 previous 135 | 136 | Previous photo in set according to idx 137 | 138 | =head2 next 139 | 140 | Next photo in set according to idx 141 | 142 | =head2 time_since 143 | 144 | Time since photo was taken 145 | 146 | =cut 147 | 148 | 1; 149 | -------------------------------------------------------------------------------- /lib/Schema/Result/Photoset.pm: -------------------------------------------------------------------------------- 1 | use utf8; 2 | package Schema::Result::Photoset; 3 | 4 | # Created by DBIx::Class::Schema::Loader 5 | # DO NOT MODIFY THE FIRST PART OF THIS FILE 6 | 7 | use strict; 8 | use warnings; 9 | 10 | use base 'DBIx::Class::Core'; 11 | __PACKAGE__->load_components("InflateColumn::DateTime"); 12 | __PACKAGE__->table("photosets"); 13 | __PACKAGE__->add_columns( 14 | "id", 15 | { data_type => "char", is_nullable => 0, size => 20 }, 16 | "title", 17 | { data_type => "char", is_nullable => 0, size => 255 }, 18 | "server", 19 | { data_type => "char", is_nullable => 0, size => 10 }, 20 | "farm", 21 | { data_type => "integer", is_nullable => 0 }, 22 | "photos", 23 | { data_type => "integer", is_nullable => 1 }, 24 | "videos", 25 | { data_type => "integer", is_nullable => 1 }, 26 | "secret", 27 | { data_type => "char", is_nullable => 0, size => 20 }, 28 | "primary_photo", 29 | { data_type => "char", is_foreign_key => 1, is_nullable => 1, size => 20 }, 30 | "idx", 31 | { data_type => "integer", is_nullable => 0 }, 32 | "description", 33 | { data_type => "char", is_nullable => 0, size => 255 }, 34 | "timestamp", 35 | { 36 | data_type => "timestamp", 37 | default_value => \"current_timestamp", 38 | is_nullable => 0, 39 | }, 40 | "date_create", 41 | { data_type => "integer", is_nullable => 1 }, 42 | "date_update", 43 | { data_type => "integer", is_nullable => 1 }, 44 | "can_comment", 45 | { data_type => "integer", is_nullable => 1 }, 46 | "count_comments", 47 | { data_type => "integer", is_nullable => 1 }, 48 | "count_views", 49 | { data_type => "integer", is_nullable => 1 }, 50 | "needs_interstitial", 51 | { data_type => "integer", is_nullable => 1 }, 52 | "visibility_can_see_set", 53 | { data_type => "integer", is_nullable => 1 }, 54 | ); 55 | __PACKAGE__->set_primary_key("id"); 56 | __PACKAGE__->has_many( 57 | "photos", 58 | "Schema::Result::Photo", 59 | { "foreign.photoset" => "self.id" }, 60 | { cascade_copy => 0, cascade_delete => 0 }, 61 | ); 62 | __PACKAGE__->belongs_to( 63 | "primary_photo", 64 | "Schema::Result::Photo", 65 | { id => "primary_photo" }, 66 | { 67 | is_deferrable => 1, 68 | join_type => "LEFT", 69 | on_delete => "CASCADE", 70 | on_update => "CASCADE", 71 | }, 72 | ); 73 | 74 | 75 | # Created by DBIx::Class::Schema::Loader v0.07015 @ 2012-02-05 21:35:07 76 | # DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:A75CmBVd9ombVH4YCAs9eg 77 | 78 | use Time::Duration; 79 | use Encode; 80 | 81 | __PACKAGE__->belongs_to( 82 | "primary", 83 | "Schema::Result::Photo", 84 | { id => "primary_photo" }, 85 | { 86 | is_deferrable => 1, 87 | join_type => "LEFT", 88 | on_delete => "CASCADE", 89 | on_update => "CASCADE", 90 | }, 91 | ); 92 | 93 | sub date { shift->primary_photo->taken } 94 | sub region { shift->primary_photo->region } 95 | 96 | sub decoded_title { return decode_utf8 shift->title } 97 | 98 | sub url_title { 99 | my $self = shift; 100 | 101 | my $title = $self->decoded_title; 102 | $title =~ s/[^a-zA-Z0-9]+/_/g; 103 | 104 | return $self->id if $title eq '_'; 105 | 106 | return lc $title; 107 | } 108 | 109 | sub previous { 110 | my $self = shift; 111 | return $self->result_source->resultset->search({idx => $self->idx - 1}) 112 | ->first; 113 | } 114 | 115 | sub next { 116 | my $self = shift; 117 | 118 | return $self->result_source->resultset->search({idx => $self->idx + 1}) 119 | ->first; 120 | } 121 | 122 | sub location { 123 | return shift->primary_photo->location; 124 | } 125 | 126 | sub time_since { 127 | return shift->primary_photo->time_since; 128 | } 129 | 130 | 1; 131 | 132 | =head1 RELATIONSHIPS 133 | 134 | =head2 primary 135 | 136 | Type: belongs_to 137 | 138 | Related object: L 139 | 140 | Alias for L 141 | 142 | =head1 METHODS 143 | 144 | =head2 date 145 | 146 | Datetime object from set's primary photo 147 | 148 | =head2 decoded_title 149 | 150 | Title decoded from utf8 151 | 152 | =head2 url_title 153 | 154 | Title for use in readable URLs - uses id for incompatible titles 155 | 156 | =head2 region 157 | 158 | Region from set's primary photo 159 | 160 | =head2 previous 161 | 162 | Previous set, ordered by idx field 163 | 164 | =head2 next 165 | 166 | Next set, ordered by idx field 167 | 168 | =head2 location 169 | 170 | Location from set's primary photo 171 | 172 | =head2 time_since 173 | 174 | Time since photo was taken 175 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Purpose 4 | 5 | - [Introduction](http://tempi.re/mojolicious-full-and-lite-apps-understanding-the-difference) 6 | - Compare a [full Mojolicious app](https://github.com/tempire/MojoExample/) to a [lite app](https://github.com/tempire/MojoExample/blob/master/mojolite) with the same functionality. 7 | - See an example of DBIx::Class usage with Mojolicious 8 | - See an example of tests for a Mojolicious app 9 | 10 | ## Notes 11 | 12 | - Both apps make use of the DBIx::Class schema. 13 | - The schema is in lib/Schema.pm, lib/Schema/* 14 | - The DBIx::Class schema connects to a provided sqlite3 database, test.db 15 | - The controller tests create a new test.db, populated using fixtures from t/fixtures/* 16 | - The schema tests use an in-memory sqlite3 database, populated using fixtures 17 | from t/fixtures/* 18 | - Test::Database is a utility for populating the sqlite3 databases with 19 | fixtures from t/fixtures/* 20 | 21 | 22 | # Usage 23 | 24 | ## Live 25 | 26 | Running on Heroku 27 | 28 | Heroku is running Hypnotoad, the *full featured UNIX optimized preforking 29 | non-blocking I/O HTTP 1.1 and WebSocket server built around the very well 30 | tested and reliable Mojo::Server::Daemon with IPv6, TLS, Bonjour, libev 31 | and hot deployment support that just works*. 32 | 33 | To easily deploy your own Mojolicious app to Heroku, check out Deploy Perl Mojolicious web apps to Heroku. 34 | 35 | ## Locally 36 | 37 | A minimum of Perl 5.10 is required. If your Perl is too old, Perlbrew is über easy to install! 38 | 39 | If you *must* run on Perl 5.8, you can try a back-ported version of Mojolicious, but you're on your own :) 40 | 41 | ### Install 42 | 43 | git clone git@github.com:tempire/MojoExample.git 44 | cd MojoExample 45 | 46 | 47 | Install the Carton package manager. Carton will install all dependencies 48 | to the local/ sub-directory. 49 | 50 | curl -L cpanmin.us | perl - Carton 51 | carton install 52 | 53 | ### Run 54 | 55 | #### Full app 56 | 57 | carton exec morbo script/mojo_full 58 | 59 | #### Light app 60 | 61 | carton exec morbo mojolite 62 | 63 | # Index 64 | 65 | * [lib/](https://github.com/tempire/MojoExample/blob/master/lib) 66 | * [MojoFull.pm](https://github.com/tempire/MojoExample/blob/master/lib/MojoFull.pm) - Mojolicious Application 67 | * [MojoFull/](https://github.com/tempire/MojoExample/blob/master/lib/MojoFull) - Mojolicious Controllers 68 | * [Blogs.pm](https://github.com/tempire/MojoExample/blob/master/lib/MojoFull/Blogs.pm) 69 | * [Home.pm](https://github.com/tempire/MojoExample/blob/master/lib/MojoFull/Home.pm) 70 | * [Photos.pm](https://github.com/tempire/MojoExample/blob/master/lib/MojoFull/Photos.pm) 71 | * [Schema.pm](https://github.com/tempire/MojoExample/blob/master/lib/Schema.pm) - DBIx::Class model 72 | * [Schema/](https://github.com/tempire/MojoExample/blob/master/lib/Schema) 73 | * [Result/](https://github.com/tempire/MojoExample/blob/master/lib/Schema/Result) - DBIx::Class Result classes 74 | * [Blog.pm](https://github.com/tempire/MojoExample/blob/master/lib/Schema/Result/Blog.pm) 75 | * [BlogTag.pm](https://github.com/tempire/MojoExample/blob/master/lib/Schema/Result/BlogTag.pm) 76 | * [Photo.pm](https://github.com/tempire/MojoExample/blob/master/lib/Schema/Result/Photo.pm) 77 | * [Photoset.pm](https://github.com/tempire/MojoExample/blob/master/lib/Schema/Result/Photoset.pm) 78 | * [ResultSet/](https://github.com/tempire/MojoExample/blob/master/lib/Schema/ResultSet/) - DBIx::Class ResultSet classes 79 | * [Blog.pm](https://github.com/tempire/MojoExample/blob/master/lib/Schema/ResultSet/Blog.pm) 80 | * [Photo.pm](https://github.com/tempire/MojoExample/blob/master/lib/Schema/ResultSet/Photo.pm) 81 | * [Photoset.pm](https://github.com/tempire/MojoExample/blob/master/lib/Schema/ResultSet/Photoset.pm) 82 | * [Test/](https://github.com/tempire/MojoExample/blob/master/lib/Test/) 83 | * [Database.pm](https://github.com/tempire/MojoExample/blob/master/lib/Test/Database.pm) - Utility class for populating test fixtures 84 | * [public/](https://github.com/tempire/MojoExample/blob/master/public/) - Static files 85 | * [css/](https://github.com/tempire/MojoExample/blob/master/public/css/) 86 | * [main.css](https://github.com/tempire/MojoExample/blob/master/public/css/main.css) 87 | * [images/](https://github.com/tempire/MojoExample/blob/master/public/images/) 88 | * [background.gif](https://github.com/tempire/MojoExample/blob/master/public/images/background.gif) 89 | * [bender_promo.png](https://github.com/tempire/MojoExample/blob/master/public/images/bender_promo.png) 90 | * [box-bottom.png](https://github.com/tempire/MojoExample/blob/master/public/images/box-bottom.png) 91 | * [box-middle.gif](https://github.com/tempire/MojoExample/blob/master/public/images/box-middle.gif) 92 | * [box-top.png](https://github.com/tempire/MojoExample/blob/master/public/images/box-top.png) 93 | * [profile_top.png](https://github.com/tempire/MojoExample/blob/master/public/images/profile_top.png) 94 | * [script/](https://github.com/tempire/MojoExample/blob/master/script/) - Utilities 95 | * [generate_schema](https://github.com/tempire/MojoExample/blob/master/script/generate_schema) - Generates DBIx::Class schema from database file 96 | * [mojo_full*](https://github.com/tempire/MojoExample/blob/master/script/mojo_full) 97 | * [new_db](https://github.com/tempire/MojoExample/blob/master/script/new_db) - Generates database file from DBIx::Class and fixtures 98 | * [t/](https://github.com/tempire/MojoExample/blob/master/t/) - Tests 99 | * [fixtures/](https://github.com/tempire/MojoExample/blob/master/t/fixtures/) - Fixtures (placeholder data) for tests 100 | * [Blog.pl](https://github.com/tempire/MojoExample/blob/master/t/fixtures/Blog.pl) 101 | * [Photoset.pl](https://github.com/tempire/MojoExample/blob/master/t/fixtures/Photoset.pl) 102 | * [schema/](https://github.com/tempire/MojoExample/blob/master/t/schema/) - DBIx::Class model tests 103 | * [blog.t](https://github.com/tempire/MojoExample/blob/master/t/schema/blog.t) 104 | * [blog_tag.t](https://github.com/tempire/MojoExample/blob/master/t/schema/blog_tag.t) 105 | * [photo.t](https://github.com/tempire/MojoExample/blob/master/t/schema/photo.t) 106 | * [photoset.t](https://github.com/tempire/MojoExample/blob/master/t/schema/photoset.t) 107 | * [home.t](https://github.com/tempire/MojoExample/blob/master/t/home.t) - Home.pm controller tests 108 | * [blogs.t](https://github.com/tempire/MojoExample/blob/master/t/blogs.t) - Blogs.pm controller tests 109 | * [photos.t](https://github.com/tempire/MojoExample/blob/master/t/photos.t) - Photos.pm controller tests 110 | * [templates/](https://github.com/tempire/MojoExample/blob/master/templates/) 111 | * [blogs/](https://github.com/tempire/MojoExample/blob/master/templates/blogs/) - Blogs.pm templates 112 | * [index.html.ep](https://github.com/tempire/MojoExample/blob/master/templates/blogs/index.html.ep) - Tag Helpers 113 | * [show.html.ep](https://github.com/tempire/MojoExample/blob/master/templates/blogs/show.html.ep) 114 | * [home/](https://github.com/tempire/MojoExample/blob/master/templates/home/) - Home.pm templates 115 | * [index.html.ep](https://github.com/tempire/MojoExample/blob/master/templates/home/index.html.ep) 116 | * [photos/](https://github.com/tempire/MojoExample/blob/master/templates/photos/) - Photos.pm templates 117 | * [index.html.ep](https://github.com/tempire/MojoExample/blob/master/templates/photos/index.html.ep) 118 | * [show.html.ep](https://github.com/tempire/MojoExample/blob/master/templates/photos/show.html.ep) 119 | * [show_set.html.ep](https://github.com/tempire/MojoExample/blob/master/templates/photos/show_set.html.ep) 120 | * [layouts/](https://github.com/tempire/MojoExample/blob/master/templates/layouts/) 121 | * [default.html.ep](https://github.com/tempire/MojoExample/blob/master/templates/layouts/default.html.ep) 122 | * *[mojolite](https://github.com/tempire/MojoExample/blob/master/mojolite)* - Mojolicious::Lite app, with all the application files listed above embedded 123 | * [README.md](https://github.com/tempire/MojoExample/blob/master/README.md) - This file 124 | 125 | # Copyright License 126 | 127 | Copyright (C) 2008-2014, Glen Hinkle. 128 | 129 | MojoExample is free software, you can redistribute it and/or modify it under the same terms as Perl5 (http://dev.perl.org/licenses/). 130 | -------------------------------------------------------------------------------- /t/fixtures/Photoset.pl: -------------------------------------------------------------------------------- 1 | [ Photoset => { 2 | can_comment => 0, 3 | count_comments => 0, 4 | count_views => 6307, 5 | date_create => 1277390082, 6 | date_update => 1318601957, 7 | description => "", 8 | farm => 2, 9 | id => 72157624222820921, 10 | idx => 1, 11 | needs_interstitial => 0, 12 | photos => 7, 13 | primary_photo => 4730349774, 14 | secret => "6386417eb2", 15 | server => 1369, 16 | timestamp => "2012-02-06 23:42:42", 17 | title => "Head Museum", 18 | videos => 0, 19 | visibility_can_see_set => 1, 20 | 21 | photos => [ 22 | { country => undef, 23 | description => "Again - receding hairpieces please!", 24 | id => 4730349774, 25 | idx => 1, 26 | is_glen => undef, 27 | isprimary => undef, 28 | large => undef, 29 | lat => undef, 30 | locality => 'Fort Lauderdale', 31 | lon => undef, 32 | medium => 33 | "http://farm2.staticflickr.com/1369/4730349774_6386417eb2.jpg", 34 | original => 35 | "http://farm2.staticflickr.com/1369/4730349774_3ccba3ded5_o.jpg", 36 | original_url => undef, 37 | photoset => 72157624222820921, 38 | region => 'Florida', 39 | small => 40 | "http://farm2.staticflickr.com/1369/4730349774_6386417eb2_m.jpg", 41 | square => 42 | "http://farm2.staticflickr.com/1369/4730349774_6386417eb2_s.jpg", 43 | taken => "2010-06-23 23:27:30", 44 | thumbnail => 45 | "http://farm2.staticflickr.com/1369/4730349774_6386417eb2_t.jpg", 46 | } 47 | ] 48 | }, 49 | Photoset => { 50 | can_comment => 0, 51 | count_comments => 0, 52 | count_views => 3333, 53 | date_create => 1277390129, 54 | date_update => 1318601957, 55 | description => "", 56 | farm => 2, 57 | id => 72157624222825789, 58 | idx => 2, 59 | needs_interstitial => 0, 60 | photos => 8, 61 | primary_photo => 4730337840, 62 | secret => "469d0a3a05", 63 | server => 1101, 64 | timestamp => "2012-02-06 23:41:51", 65 | title => "Robot Arms", 66 | videos => 1, 67 | visibility_can_see_set => 1, 68 | photos => [ 69 | { country => undef, 70 | description => "", 71 | id => 4730337840, 72 | idx => 1, 73 | is_glen => undef, 74 | isprimary => undef, 75 | large => undef, 76 | lat => undef, 77 | locality => 'Chico', 78 | lon => undef, 79 | medium => 80 | "http://farm2.staticflickr.com/1101/4730337840_469d0a3a05.jpg", 81 | original => 82 | "http://farm2.staticflickr.com/1101/4730337840_c907bcd766_o.jpg", 83 | original_url => undef, 84 | photoset => 72157624222825789, 85 | region => 'California', 86 | small => 87 | "http://farm2.staticflickr.com/1101/4730337840_469d0a3a05_m.jpg", 88 | square => 89 | "http://farm2.staticflickr.com/1101/4730337840_469d0a3a05_s.jpg", 90 | taken => "2010-06-23 22:37:34", 91 | thumbnail => 92 | "http://farm2.staticflickr.com/1101/4730337840_469d0a3a05_t.jpg", 93 | }, 94 | { country => undef, 95 | description => 96 | "Finished! (mostly)\nI decided to finish this up in the middle of working on the cube. The cube is pretty difficult - I needed something easy to do for a while, and I had finally gotten the parts I needed.\n\nFYI - if you're ever in a rush for parts, Troy's Surplus on bricklink is NOT the place to go. Took them a week just to invoice me!", 97 | id => 4656987762, 98 | idx => 6, 99 | is_glen => undef, 100 | isprimary => undef, 101 | large => undef, 102 | lat => undef, 103 | locality => 'Chico', 104 | lon => undef, 105 | medium => 106 | "http://farm5.staticflickr.com/4024/4656987762_f3da33213e.jpg", 107 | original => 108 | "http://farm5.staticflickr.com/4024/4656987762_771ba8730f_o.jpg", 109 | original_url => undef, 110 | photoset => 72157624222825789, 111 | region => 'California', 112 | small => 113 | "http://farm5.staticflickr.com/4024/4656987762_f3da33213e_m.jpg", 114 | square => 115 | "http://farm5.staticflickr.com/4024/4656987762_f3da33213e_s.jpg", 116 | taken => "2010-05-31 11:46:43", 117 | thumbnail => 118 | "http://farm5.staticflickr.com/4024/4656987762_f3da33213e_t.jpg", 119 | } 120 | ] 121 | }, 122 | Photoset => { 123 | can_comment => 0, 124 | count_comments => 0, 125 | count_views => 12853, 126 | date_create => 1277389796, 127 | date_update => 1323797050, 128 | description => 129 | "This was the very first Futurama creation I made. I first completed the building in Feb. 2008, along with 11 characters. In Dec. 2009 I extended the building to include the sub pen and built a small surrounding layout (street, river). This was expanded in June 2010 to the full layout with numerous buildings, sewers, and characters.", 130 | farm => 2, 131 | id => 72157624347519408, 132 | idx => 3, 133 | needs_interstitial => 0, 134 | photos => 26, 135 | primary_photo => 4730456558, 136 | secret => "97de792b60", 137 | server => 1357, 138 | timestamp => "2012-02-06 23:42:22", 139 | title => "Planet Express", 140 | videos => 0, 141 | visibility_can_see_set => 1, 142 | 143 | photos => [ 144 | { country => undef, 145 | description => 146 | "This layout is the culmination of a work in progress for over two years. I started with the Planet Express and major characters back in February 2008. In December 2009 I expanded the building to include the sub pen, and built up a portion of the surrounding area. In late April through June 2010, I worked overtime to build an 80"x60" layout, ready in time for Brickworld 2010 on June 17th. This layout is a part of the much larger Northern Illinois Lego Train Club, and may be seen at our various shows in the Chicago area.\n\nTo check out all of the creations, building and characters within this layout, view the World of Tomorrow photo collection.\n\nUPDATE, 11/1/10: I finally have a video of the entire layout. Check it out here.", 147 | id => 4730456558, 148 | idx => 1, 149 | is_glen => undef, 150 | isprimary => undef, 151 | large => undef, 152 | lat => undef, 153 | locality => 'Chico', 154 | lon => undef, 155 | medium => 156 | "http://farm2.staticflickr.com/1012/4730456558_92d8dc4cd2.jpg", 157 | original => 158 | "http://farm2.staticflickr.com/1012/4730456558_787e41a4aa_o.jpg", 159 | original_url => undef, 160 | photoset => 72157624347519408, 161 | region => 'California', 162 | small => 163 | "http://farm2.staticflickr.com/1012/4730456558_92d8dc4cd2_m.jpg", 164 | square => 165 | "http://farm2.staticflickr.com/1012/4730456558_92d8dc4cd2_s.jpg", 166 | taken => "2010-06-23 22:23:21", 167 | thumbnail => 168 | "http://farm2.staticflickr.com/1012/4730456558_92d8dc4cd2_t.jpg", 169 | 170 | }, 171 | { country => undef, 172 | description => 173 | "The building has gone through a few subtle refinements over the years, but it largely looks exactly the same as when I first made it. Most changes involve interior structure improvements.", 174 | id => 4729801945, 175 | idx => 2, 176 | is_glen => undef, 177 | isprimary => undef, 178 | large => undef, 179 | lat => undef, 180 | locality => 'Chico', 181 | lon => undef, 182 | medium => 183 | "http://farm2.staticflickr.com/1065/4729801945_c35294c20f.jpg", 184 | original => 185 | "http://farm2.staticflickr.com/1065/4729801945_739b3d5f44_o.jpg", 186 | original_url => undef, 187 | photoset => 72157624347519408, 188 | region => 'California', 189 | small => 190 | "http://farm2.staticflickr.com/1065/4729801945_c35294c20f_m.jpg", 191 | square => 192 | "http://farm2.staticflickr.com/1065/4729801945_c35294c20f_s.jpg", 193 | taken => "2010-06-23 22:21:51", 194 | thumbnail => 195 | "http://farm2.staticflickr.com/1065/4729801945_c35294c20f_t.jpg", 196 | }, 197 | { country => undef, 198 | description => 199 | "Nearly completed, just need some more 1x4 yellow tiles with stripes.\nTwo more additions to the cast, too.", 200 | id => 2253839375, 201 | idx => 3, 202 | is_glen => undef, 203 | isprimary => undef, 204 | large => undef, 205 | lat => undef, 206 | locality => 'Chico', 207 | lon => undef, 208 | medium => 209 | "http://farm3.staticflickr.com/2276/2253839375_5fdf5b2888.jpg", 210 | original => 211 | "http://farm3.staticflickr.com/2276/2253839375_3565fea796_o.jpg", 212 | original_url => undef, 213 | photoset => 72157624347519408, 214 | region => 'California', 215 | small => 216 | "http://farm3.staticflickr.com/2276/2253839375_5fdf5b2888_m.jpg", 217 | square => 218 | "http://farm3.staticflickr.com/2276/2253839375_5fdf5b2888_s.jpg", 219 | taken => "2008-02-10 02:22:56", 220 | thumbnail => 221 | "http://farm3.staticflickr.com/2276/2253839375_5fdf5b2888_t.jpg", 222 | }, 223 | ] 224 | } 225 | ] 226 | -------------------------------------------------------------------------------- /templates/home/index.html.ep: -------------------------------------------------------------------------------- 1 | % layout 'default'; 2 |

Purpose

3 | 4 |
    5 |
  • Introduction
  • 6 |
  • Compare a full Mojolicious app to a lite app with the same functionality.
  • 7 |
  • See an example of DBIx::Class usage with Mojolicious
  • 8 |
  • See an example of tests for a Mojolicious app
  • 9 |
10 | 11 | 12 |

Notes

13 | 14 |
    15 |
  • Both apps make use of the DBIx::Class schema.
  • 16 |
  • The schema is in lib/Schema.pm, lib/Schema/*
  • 17 |
  • The DBIx::Class schema connects to a provided sqlite3 database, test.db
  • 18 |
  • The controller tests create a new test.db, populated using fixtures from t/fixtures/*
  • 19 |
  • The schema tests use an in-memory sqlite3 database, populated using fixtures 20 | from t/fixtures/*
  • 21 |
  • Test::Database is a utility for populating the sqlite3 databases with 22 | fixtures from t/fixtures/*
  • 23 |
24 | 25 | 26 |

Usage

27 | 28 |

Live

29 | 30 |

Running on Heroku

31 | 32 |

Heroku is running Hypnotoad, the full featured UNIX optimized preforking 33 | non-blocking I/O HTTP 1.1 and WebSocket server built around the very well 34 | tested and reliable Mojo::Server::Daemon with IPv6, TLS, Bonjour, libev 35 | and hot deployment support that just works.

36 | 37 |

To easily deploy your own Mojolicious app to Heroku, check out Deploy Perl Mojolicious web apps to Heroku.

38 | 39 |

Locally

40 | 41 |

A minimum of Perl 5.10 is required. If your Perl is too old, Perlbrew is über easy to install!

42 | 43 |

If you must run on Perl 5.8, you can try a back-ported version of Mojolicious, but you're on your own :)

44 | 45 |

Install

46 | 47 |
git clone git@github.com:tempire/MojoExample.git
 48 | cd MojoExample
 49 | 
50 | 51 |

Install the Carton package manager. Carton will install all dependencies 52 | to the local/ sub-directory.

53 | 54 |
curl -L cpanmin.us | perl - Carton
 55 | carton install
 56 | 
57 | 58 |

Run

59 | 60 |

Full app

61 | 62 |
carton exec morbo script/mojo_full
 63 | 
64 | 65 |

Light app

66 | 67 |
carton exec morbo mojolite
 68 | 
69 | 70 |

Index

71 | 72 | 205 | 206 | 207 |

Copyright License

208 | 209 |

Copyright (C) 2008-2014, Glen Hinkle.

210 | 211 |

MojoExample is free software, you can redistribute it and/or modify it under the same terms as Perl5 (http://dev.perl.org/licenses/).

212 | -------------------------------------------------------------------------------- /public/css/main.css: -------------------------------------------------------------------------------- 1 | body { 2 | width: 100%; 3 | margin: 0; 4 | font-family: "helvetica neue", helvetica, Arial; 5 | font-size: 100%; 6 | background-color: #0E0E0E; 7 | background-image: url( /images/background.gif ); 8 | background-repeat: repeat-x; 9 | } 10 | .hidden { 11 | display: none; 12 | } 13 | .highlighted { 14 | /*background-color: #FFFFB1;*/ 15 | } 16 | .blog.home { 17 | margin: auto auto 15px auto; 18 | width: 70%; 19 | } 20 | .blog.home h3 { 21 | padding: 0; 22 | margin: 0; 23 | } 24 | .blog.home .content { 25 | font-size: .7em; 26 | } 27 | .blog.home .snippet { 28 | margin: 5px 0 0 0; 29 | color: gray; 30 | } 31 | .blog div.tags { 32 | float: right; 33 | margin: 4px 0 20px; 34 | font-size: 0.9em; 35 | } 36 | .blog span.tag { 37 | background-color: #6CD6F4; 38 | color: white; 39 | -webkit-border-radius: 4px; 40 | -moz-border-radius: 4px; 41 | padding: 3px; 42 | } 43 | 44 | .dp-highlighter { 45 | font-family: Consolas,Monaco,Lucida Console,Liberation Mono,DejaVu Sans Mono,Bitstream Vera Sans Mono,Courier New; 46 | font-size: 1em; 47 | -moz-border-radius: 6px; 48 | -webkit-border-radius: 6px; 49 | padding: 3px; 50 | } 51 | .dp-highlighter ol li, .dp-highlighter .columns div { 52 | line-height: inherit; 53 | } 54 | a { 55 | color: #0D5B9E; 56 | } 57 | h1 { 58 | padding: 0 0 10px; 59 | margin: 0; 60 | } 61 | ul.horizontal-list { 62 | color: gray; 63 | overflow: auto; 64 | margin: 0; 65 | padding: 0; 66 | position: relative; 67 | left: -20px; 68 | } 69 | ul.horizontal-list li { 70 | float: left; 71 | margin-left: 24px; 72 | } 73 | ul.spaced { 74 | list-style-type: disc; 75 | } 76 | li { 77 | margin: 10px 0 0 0; 78 | } 79 | .spaced li { 80 | padding: 10px 0; 81 | } 82 | table.structure { 83 | border: dotted black 1px; 84 | border-collapse: collapse; 85 | width: 100%; 86 | } 87 | table.structure td { 88 | border: dotted black 1px; 89 | border-collapse: collapse; 90 | padding: 10px; 91 | } 92 | #header { 93 | margin: auto; 94 | height: 140px; 95 | width: 1000px; 96 | position: relative; 97 | } 98 | #header a.home { 99 | height: 135px; 100 | width: 140px; 101 | display: block; 102 | position: absolute; 103 | left: 70px; 104 | top: 15px; 105 | z-index: 0; /* for IE7 - keep boxtop in front of profile picture */ 106 | } 107 | #header img.profile { 108 | border: 0; 109 | position: absolute; 110 | top: 40px; 111 | left: -15px; 112 | z-index: -1; /* keep profile picture behind white content box */ 113 | } 114 | .fb_status { 115 | /* width: 100%; */ 116 | display: block; 117 | padding: 10px; 118 | position: relative; 119 | z-index: 1; 120 | top: 10px; 121 | margin: auto; 122 | } 123 | .fb_status .time_since { 124 | width: inherit; 125 | } 126 | .fb_status a { 127 | color: inherit; 128 | text-decoration: none; 129 | } 130 | #header .fb_status { 131 | left: 100px; 132 | width: 70%; 133 | } 134 | #header .fb_status .statii { 135 | font-size: 1.4em; 136 | color: white; 137 | } 138 | .fb_status .statii * { 139 | text-decoration: none; 140 | color: inherit; 141 | } 142 | #header .fb_status .time_since { 143 | display: block; 144 | font-size: 0.7em; 145 | /* color: #302E26; */ 146 | position: relative; 147 | left: 20px; 148 | } 149 | #content .fb_status { 150 | text-align: center; 151 | } 152 | #content .fb_status .statii { 153 | font-size: 3em; 154 | letter-spacing: -2px; 155 | color: #0D6780; 156 | } 157 | #content .fb_status .statii.over2lines { 158 | clear: both; 159 | font-size: 2.5em; 160 | letter-spacing: -2px; 161 | color: #0D6780; 162 | } 163 | #content .fb_status .time_since { 164 | display: block; 165 | position: relative; 166 | color: gray; 167 | top: 10px; 168 | } 169 | #header .toc { 170 | font-size: 1.2em; 171 | margin: 0; 172 | padding: 0; 173 | position: absolute; 174 | z-index: 10; 175 | right: 30px; 176 | bottom: 0px; 177 | } 178 | #header .toc li { 179 | display: inline; 180 | margin-right: 5px; 181 | } 182 | #header .toc li a { 183 | text-decoration: none; 184 | color: white; 185 | } 186 | #main { 187 | margin: auto; 188 | width: 1000px; 189 | overflow: auto; 190 | } 191 | #box-top { 192 | background-image: url( ../images/box-top.png ); 193 | background-repeat: no-repeat; 194 | height: 39px; 195 | position: relative; 196 | z-index: 1; /* for IE7 - keep boxtop in front of profile picture */ 197 | } 198 | #content { 199 | background-image: url( ../images/box-middle.gif ); 200 | background-repeat:repeat-y; 201 | padding: 0 40px 0 40px; 202 | /* padding: 1px 45px 10px 35px; */ 203 | /* padding-left: 35px; */ 204 | /* width: 98%; */ 205 | overflow: hidden; 206 | min-height: 400px; 207 | position: relative; 208 | z-index: 1; /* for IE7 - keep boxtop in front of profile picture */ 209 | } 210 | #box-bottom { 211 | background-image: url( ../images/box-bottom.png ); 212 | background-repeat: no-repeat; 213 | height: 39px; 214 | } 215 | #google-location, #google-location iframe { 216 | float: left; 217 | width: 250px; 218 | } 219 | .photo { 220 | overflow: hidden; 221 | } 222 | .wide { 223 | width: 100%; 224 | } 225 | #photosets.mini-thumbnails { 226 | margin: 0 20px; 227 | padding: 0; 228 | text-align: left; 229 | float: left; 230 | overflow: auto; 231 | } 232 | .photos.mini-thumbnails.home { 233 | margin: auto; 234 | position: relative; 235 | width: 70%; 236 | } 237 | #latitude { 238 | display: none; 239 | height: 225px; 240 | overflow: hidden; 241 | } 242 | #latitude iframe { 243 | position: relative; 244 | top: -8px; 245 | } 246 | form.blogs input { 247 | width: 300px; 248 | } 249 | #blogs { 250 | margin: 0; 251 | padding: 0; 252 | margin: auto; 253 | /* margin-top: 10px; */ 254 | font-size: 0.8em; 255 | } 256 | #blogs .blog { 257 | margin: auto; 258 | /* margin-top: 40px; */ 259 | width: 65%; 260 | padding: 0 20px; 261 | list-style-image: none; 262 | list-style-position: outside; 263 | list-style-type: none; 264 | border-bottom: dashed silver 1px; 265 | /* border-bottom: dashed silver 1px; */ 266 | padding-top: 10px; 267 | padding-bottom: 10px; 268 | -moz-border-radius: 1em; 269 | -webkit-border-radius: 1em; 270 | } 271 | #blogs.index .blog { 272 | width: 95%; 273 | border-bottom: 0px; 274 | } 275 | #blogs .blog.single { 276 | margin-top: -2px; 277 | padding: 20px 0 30px; 278 | border-bottom: solid black dashed; 279 | /* border: solid #CCCC66 3px; */ 280 | margin: auto; 281 | } 282 | #blogs .blog h2 { 283 | /* background-color: #90C6D5; 284 | padding: 3px 3px 3px 10px; 285 | */ 286 | font-size: 1.2em; 287 | margin: 0; 288 | } 289 | #blogs .blog.single h2 { 290 | font-size: 2em; 291 | } 292 | #blogs .blog h3 { 293 | font-weight: normal; 294 | display: block; 295 | margin: 0 0 3px 0; 296 | padding: 3px 0 0 0; 297 | /* border-bottom: solid silver 1px; */ 298 | } 299 | #blogs .blog h3.subtitle { 300 | color: #3E3C33; 301 | font-style: italic; 302 | } 303 | #blogs .blog.snippet .subtitle { 304 | } 305 | #blogs .blog .time { 306 | color: gray; 307 | font-size: 0.8em; 308 | } 309 | #blogs .blog.single h3 { 310 | font-size: 1.1em; 311 | } 312 | #blogs .blog .content { 313 | padding-top: 10px; 314 | } 315 | #blogs .blog.single .content h1 { 316 | padding: 20px 0 0 0; 317 | } 318 | 319 | #blogs .blog a.less { 320 | float: right; 321 | position: relative; 322 | top: -50px; 323 | } 324 | #blogs .blog a.less.bottom { 325 | float: right; 326 | position: relative; 327 | top: 0px; 328 | } 329 | #blogs .blog a.edit { 330 | float: right; 331 | position: relative; 332 | top: -50px; 333 | right: 10px; 334 | } 335 | .blog.snippet { 336 | font-size: .9em; 337 | color: gray; 338 | } 339 | #photosets { 340 | margin: 20px; 341 | text-align: right; 342 | overflow: auto; 343 | } 344 | .photo-count { 345 | padding: 8px; 346 | padding-left: 50px; 347 | } 348 | .mini-thumbnails { 349 | overflow: hidden; 350 | } 351 | .mini-thumbnails .photo { 352 | padding: 0 2px; 353 | position: relative; 354 | float: left; 355 | } 356 | .photo .label { 357 | position: absolute; 358 | -moz-border-radius: .4em; 359 | -webkit-border-radius: .4em; 360 | background-color: white; 361 | } 362 | .mini-thumbnails .photo .label { 363 | bottom: 0; 364 | left: -3px; 365 | padding: 3px 5px; 366 | } 367 | .mini-thumbnails .photo img.small { 368 | width: 240px; 369 | } 370 | .mini-thumbnails .photo img.mini { 371 | width: 40px; 372 | } 373 | .thumbnails .photo { 374 | width: 110px; 375 | height: 160px; 376 | float: left; 377 | font-size: .9em; 378 | position: relative; 379 | } 380 | #photosets .label { 381 | right: 0px; 382 | top: -3px; 383 | padding: 2px 5px; 384 | width: 15px; 385 | color: black; 386 | font-size: .9em; 387 | } 388 | #photosets .photo { 389 | float: left; 390 | } 391 | #photosets .photo a { 392 | text-decoration: none; 393 | color: black; 394 | } 395 | .photos img { 396 | -moz-border-radius: .4em; 397 | -webkit-border-radius: .4em; 398 | border: solid gray 1px; 399 | } 400 | .photo img.cover { 401 | position: absolute; 402 | border: 0; 403 | } 404 | #photosets .photo .date { 405 | color: gray; 406 | font-size: 0.8em; 407 | } 408 | #photosets .photo .region { 409 | color: gray; 410 | font-size: 0.8em; 411 | } 412 | #photosets .photo .title { 413 | font-weight: bold; 414 | font-size: .8em; 415 | } 416 | .photoset { 417 | margin-top: 0px; 418 | overflow: hidden; 419 | } 420 | .photoset h1 { 421 | margin: 10px 0; 422 | padding: 0; 423 | } 424 | .photoset h2 { 425 | margin: 0; 426 | padding: 0; 427 | float: left; 428 | color: gray; 429 | font-weight: normal; 430 | } 431 | .photoset h2.count { 432 | float: right; 433 | margin-right: 15px; 434 | } 435 | .photoset .photos { 436 | margin: auto; 437 | margin-top: 10px; 438 | width: 100%; 439 | overflow: hidden; 440 | /*text-align: right;*/ 441 | } 442 | .photoset .photo { 443 | height: 80px; 444 | float: left; 445 | width: 80px; 446 | } 447 | .photo .nav { 448 | padding: 10px 5px; 449 | } 450 | /* 451 | .photoset .photo.primary { 452 | height: 100%; 453 | width: 375px; 454 | } 455 | */ 456 | .selected { 457 | position: absolute; 458 | margin: auto; 459 | } 460 | 461 | #google-calendar { 462 | width: 100%; 463 | } 464 | #google-calendar iframe { 465 | width: 100%; 466 | height: 500px; 467 | } 468 | 469 | #footer { 470 | height: 90px; 471 | margin: auto; 472 | width: 950px; 473 | font-size: 1em; 474 | text-align: center; 475 | } 476 | #footer ul { 477 | padding: 0; 478 | margin: 10px; 479 | position: relative; 480 | } 481 | #footer ul.sub { 482 | display: none; 483 | font-size: 0.8em; 484 | opacity: 0.9; 485 | } 486 | #footer ul li.key { 487 | display: none; 488 | position: absolute; 489 | top: -5px; 490 | left: 0px; 491 | color: #272929; 492 | font-size: 1.5em; 493 | } 494 | #footer ul li { 495 | display: inline; 496 | } 497 | #footer ul li a { 498 | color: #3D4040; 499 | padding: 0 5px; 500 | } 501 | 502 | #iphone { 503 | position: relative; 504 | top: 13px; 505 | width: 900px; 506 | overflow: hidden; 507 | } 508 | #iphone .display { 509 | float: left; 510 | } 511 | #iphone p { 512 | font-size: 0.9em; 513 | } 514 | #iphone .description { 515 | float: left; 516 | position: relative; 517 | top: 130px; 518 | left: 30px; 519 | width: 600px; 520 | } 521 | #iphone .display img { 522 | display: block; 523 | } 524 | #iphone .display img.badge { 525 | position: relative; 526 | left: 22px; 527 | } 528 | .photos.home > a:first-child img { 529 | width: 240px; 530 | } 531 | a.photo > img { 532 | /*width: 40px;*/ 533 | } 534 | --------------------------------------------------------------------------------