3. I Work for a Living
• Contract work / Billable Hours: How do I track them?
4. I Work for a Living
• Contract work / Billable Hours: How do I track them?
• Weekly Status Reports: What did I do?
5. I Work for a Living
• Contract work / Billable Hours: How do I track them?
• Weekly Status Reports: What did I do?
• Reliable Memory: Did I do that? How? When? Why?
6. I Work for a Living
• Contract work / Billable Hours: How do I track them?
• Weekly Status Reports: What did I do?
• Reliable Memory: Did I do that? How? When? Why?
• Task Lists: What should I do next?
14. Qublog
• I don’t want to store this information in four places
• I have a habit of writing down what I’m doing to focus
15. Qublog
• I don’t want to store this information in four places
• I have a habit of writing down what I’m doing to focus
• I needed something else to do with my lack of spare time
16. Qublog
• I don’t want to store this information in four places
• I have a habit of writing down what I’m doing to focus
• I needed something else to do with my lack of spare time
• I built Qublog (originally MyToDo and Kiln for a short while)
17. Qublog
• I don’t want to store this information in four places
• I have a habit of writing down what I’m doing to focus
• I needed something else to do with my lack of spare time
• I built Qublog (originally MyToDo and Kiln for a short while)
• It’s written using Jifty
18. Qublog
• I don’t want to store this information in four places
• I have a habit of writing down what I’m doing to focus
• I needed something else to do with my lack of spare time
• I built Qublog (originally MyToDo and Kiln for a short while)
• It’s written using Jifty
• We’re going to look at how I did it to explain Jifty
36. Qublog
• Provides a Journal for keeping thoughts
• Provides a To Do List for organizing tasks
37. Qublog
• Provides a Journal for keeping thoughts
• Provides a To Do List for organizing tasks
• Journal Comments are grouped into Timers that keep track of time spent
38. Qublog
• Provides a Journal for keeping thoughts
• Provides a To Do List for organizing tasks
• Journal Comments are grouped into Timers that keep track of time spent
• Timers are grouped into Entries to sum up work for reporting
39. Qublog
• Provides a Journal for keeping thoughts
• Provides a To Do List for organizing tasks
• Journal Comments are grouped into Timers that keep track of time spent
• Timers are grouped into Entries to sum up work for reporting
• Comments also connect with Tasks to keep track of what’s next
48. Install Jifty
• Method 1: Shipwright
svn co http://code.bestpractical.com/shipwright/jifty/ jifty-builder
cd jifty-builder
./bin/shipwright-builder # optionally --skip perl --skip-test
cp -rvp /tmp/jifty-$random/jifty /usr/local/jifty
ln -s /usr/local/jifty/bin/jifty /usr/local/bin/jifty
“notest” saves ~60 minutes according to Audrey
• Method 2: CPAN
perl -MCPAN -eshell # or just cpan
cpan> notest install Jifty
49. Install Jifty
• Method 1: Shipwright
svn co http://code.bestpractical.com/shipwright/jifty/ jifty-builder
cd jifty-builder
./bin/shipwright-builder # optionally --skip perl --skip-test
cp -rvp /tmp/jifty-$random/jifty /usr/local/jifty
ln -s /usr/local/jifty/bin/jifty /usr/local/bin/jifty
“notest” saves ~60 minutes according to Audrey
• Method 2: CPAN
perl -MCPAN -eshell # or just cpan
cpan> notest install Jifty
“notest” will get you no support according to Jesse
59. % ./bin/jifty server
WARN - Application schema has no version in the database.
WARN - Automatically creating your database.
INFO - Generating SQL for application QublogMini...
INFO - Using Jifty::Model::Session, as it appears to be new.
INFO - Using Jifty::Model::Metadata, as it appears to be new.
INFO - Set up version v0.0.1, jifty version 0.804080
INFO - You can connect to your server at http://localhost:8888/
60. % ./bin/jifty server
WARN - Application schema has no version in the database.
WARN - Automatically creating your database.
INFO - Generating SQL for application QublogMini...
INFO - Using Jifty::Model::Session, as it appears to be new.
INFO - Using Jifty::Model::Metadata, as it appears to be new.
INFO - Set up version v0.0.1, jifty version 0.804080
INFO - You can connect to your server at http://localhost:8888/
68. package QublogMini::Model::Comment;
use Jifty::DBI::Schema;
use QublogMini::Record schema {
column commented_on =>
type is 'datetime',
label is 'Commented on',
filters are qw(
Jifty::DBI::Filter::DateTime
);
column name =>
type is 'text',
label is 'Name',
render as 'textarea';
};
70. % bin/jifty server
WARN - Application schema has no version in the database.
WARN - Automatically creating your database.
INFO - Generating SQL for application QublogMini...
INFO - Using QublogMini::Model::Comment, as it appears to be
new.
INFO - Using Jifty::Model::Session, as it appears to be new.
INFO - Using Jifty::Model::Metadata, as it appears to be new.
INFO - Set up version v0.0.1, jifty version 0.804080
INFO - You can connect to your server at http://localhost:
8888/
84. package QublogMini::Model::Timer;
use Jifty::DBI::Schema;
use QublogMini::Record schema {
column started_on =>
label is 'Started on',
filters are qw( Jifty::DBI::Filter::DateTime );
column stopped_on =>
label is 'Stopped on',
filters are qw( Jifty::DBI::Filter::DateTime );
column comments =>
references QublogMini::Model::CommentCollection
by 'timer';
};
sub since { '0.0.2' }
85. package QublogMini::Model::Comment;
use Jifty::DBI::Schema;
use QublogMini::Record schema {
column commented_on =>
type is 'datetime',
label is 'Commented on',
filters are qw( Jifty::DBI::Filter::DateTime ),
;
column name =>
type is 'text',
label is 'Name',
render as 'textarea',
;
};
86. package QublogMini::Model::Comment;
use Jifty::DBI::Schema;
use QublogMini::Record schema {
column commented_on =>
type is 'datetime',
label is 'Commented on',
filters are qw( Jifty::DBI::Filter::DateTime ),
;
column name =>
type is 'text',
label is 'Name',
render as 'textarea',
;
column timer =>
label is 'Timer',
since '0.0.2',
references QublogMini::Model::Timer;
};
88. % bin/jifty server
WARN - Application schema version in database (v0.0.1) doesn't match
application schema version (0.0.2)
WARN - Automatically upgrading your database to match the current
application schema.
Jifty version 0.809130 up to date.
Jifty::Plugin::LetMe version v0.0.1 up to date.
Jifty::Plugin::SkeletonApp version v0.0.1 up to date.
Jifty::Plugin::REST version v0.0.1 up to date.
Jifty::Plugin::Halo version v0.0.1 up to date.
Jifty::Plugin::ErrorTemplates version v0.0.1 up to date.
Jifty::Plugin::OnlineDocs version v0.0.1 up to date.
Jifty::Plugin::CompressedCSSandJS version v0.0.1 up to date.
Jifty::Plugin::AdminUI version v0.0.1 up to date.
INFO - Generating SQL to upgrade QublogMini v0.0.1 database to v0.0.2
INFO - Upgrading through 0.0.2
INFO - Running upgrade script
INFO - Upgraded to version v0.0.2
INFO - You can connect to your server at http://localhost:8888/
100. package QublogMini::Model::Entry;
use Jifty::DBI::Schema;
use QublogMini::Record schema {
column name =>
type is 'text',
label is 'Name',
is mandatory;
column url =>
type is 'text',
label is 'Name';
column timers =>
references QublogMini::Model::TimerCollection
by 'entry';
};
sub since { '0.0.3' }
101. package QublogMini::Model::Timer;
use Jifty::DBI::Schema;
use QublogMini::Record schema {
# ...
column entry =>
label is 'Entry',
since '0.0.3',
references QublogMini::Model::Entry;
# ...
};
sub name {
my $self = shift;
my $name = '';
$name .= $self->entry->name . ': ' if $self->entry->id;
$name .= $self->started_on . ' to ' . $self->stopped_on
return $name;
}
106. % bin/jifty server
WARN - Application schema version in database (v0.0.2) doesn't match
application schema version (0.0.3)
WARN - Automatically upgrading your database to match the current
application schema.
Jifty version 0.809130 up to date.
Jifty::Plugin::LetMe version v0.0.1 up to date.
Jifty::Plugin::SkeletonApp version v0.0.1 up to date.
Jifty::Plugin::REST version v0.0.1 up to date.
Jifty::Plugin::Halo version v0.0.1 up to date.
Jifty::Plugin::ErrorTemplates version v0.0.1 up to date.
Jifty::Plugin::OnlineDocs version v0.0.1 up to date.
Jifty::Plugin::CompressedCSSandJS version v0.0.1 up to date.
Jifty::Plugin::AdminUI version v0.0.1 up to date.
INFO - Generating SQL to upgrade QublogMini v0.0.2 database to v0.0.3
INFO - Upgrading through 0.0.3
INFO - Running upgrade script
INFO - Upgraded to version v0.0.3
INFO - You can connect to your server at http://localhost:8888/
113. package QublogMini::Dispatcher;
use Jifty::Dispatcher -base;
on '/' => dispatch '/today';
on '/today' => run {
my $entries = QublogMini::Model::EntryCollection->new;
my $timer_alias = $entries->join(
column1 => 'id',
table2 => QublogMini::Model::Timer->table,
column2 => 'entry',
);
$entries->limit(
alias => $timer_alias,
column => 'started_on',
operator => '>=',
value => DateTime->today,
entry_aggregator => 'AND',
);
$entries->limit(
alias => $timer_alias,
column => 'started_on',
operator => '<',
value => DateTime->today->add( days => 1 ),
entry_aggregator => 'AND',
);
set entries => $entries;
show '/today';
};
114. package QublogMini::View;
use Jifty::View::Declare -base;
template '/today' => page {
{ title is quot;Today's Journalquot; }
my $entries = get 'entries';
while (my $entry = $entries->next) {
h2 { $entry->name };
p {
hyperlink
label => $entry->url,
url => $entry->url
};
}
};
115.
116. template '/today' => page {
{ title is quot;Today's Journalquot; }
my $entries = get 'entries';
while (my $entry = $entries->next) {
h2 { $entry->name };
p {
hyperlink
label => $entry->url,
url => $entry->url
};
show '/timers', $entry;
}
};
117. private template '/timers' => sub {
my ($self, $entry) = @_;
my $timers = $entry->timers;
while (my $timer = $timers->next) {
show '/show_comment', $timer->stopped_on, _('Stopped timer.')
if $timer->stopped_on;
show '/comments', $timer;
show '/show_comment', $timer->started_on, _('Started timer.');
}
};
118. private template '/comments' => sub {
my ($self, $timer) = @_;
my $comments = $timer->comments;
while (my $comment = $comments->next) {
show '/show_comment', $comment->commented_on, $comment->name;
}
};
private template '/show_comment' => sub {
my ($self, $time, $message) = @_;
p { { class is 'time' } $time };
p { { class is 'comment' } $message };
};
132. Where’d it go?
2 Problems
We need a timer
too.
We need to
redraw the journal.
133. package QublogMini::Model::Entry;
use Jifty::DBI::Schema;
use DateTime;
# ...
# Add a timer when we add an entry
sub after_create {
my ($self, $id) = @_;
return unless $$id;
my $timer = QublogMini::Model::Timer->new;
$timer->create(
entry => $$id,
started_on => DateTime->now,
);
return 1;
}
134. template '/today' => page {
{ title is quot;Today's Journalquot; }
my $entries = get 'entries';
while (my $entry = $entries->next) {
h2 { $entry->name };
p {
hyperlink
label => $entry->url,
url => $entry->url
};
show '/timers', $entry;
}
};
135. template '/today' => page {
{ title is quot;Today's Journalquot; }
my $entries = get 'entries';
while (my $entry = $entries->next) {
h2 { $entry->name };
p {
hyperlink
label => $entry->url,
url => $entry->url
};
show '/timers', $entry;
}
};
136. # Replace /today with a region
template '/today' => page {
{ title is quot;Today's Journalquot; }
render_region
name => 'entries',
path => '/entries',
;
};
137. # Replace /today with a region
template '/today' => page {
{ title is quot;Today's Journalquot; }
render_region
name => 'entries',
path => '/entries',
;
};
138. # Add a new entries fragment
template '/entries' => sub {
form {
render_region
name => 'add_entry',
path => '/add_entry_button',
;
};
my $entries = get 'entries';
while (my $entry = $entries->next) {
h2 { $entry->name };
p {
hyperlink
label => $entry->url,
url => $entry->url;
};
show '/timers', $entry;
}
};
139. on '/today' => run {
my $entries = QublogMini::Model::EntryCollection->new;
my $timer_alias = $entries->join(
column1 => 'id',
table2 => QublogMini::Model::Timer->table,
column2 => 'entry',
);
$entries->limit(
alias => $timer_alias,
column => 'started_on',
operator => '>=',
value => DateTime->today,
entry_aggregator => 'AND',
);
$entries->limit(
alias => $timer_alias,
column => 'started_on',
operator => '<',
value => DateTime->today->add( days => 1 ),
entry_aggregator => 'AND',
);
set entries => $entries;
};
140. on '/today' => run {
my $entries = QublogMini::Model::EntryCollection->new;
my $timer_alias = $entries->join(
column1 => 'id',
table2 => QublogMini::Model::Timer->table,
column2 => 'entry',
);
$entries->limit(
alias => $timer_alias,
column => 'started_on',
operator => '>=',
value => DateTime->today,
entry_aggregator => 'AND',
);
$entries->limit(
alias => $timer_alias,
column => 'started_on',
operator => '<',
value => DateTime->today->add( days => 1 ),
entry_aggregator => 'AND',
);
set entries => $entries;
};
141. on '/entries' => run {
my $entries = QublogMini::Model::EntryCollection->new;
my $timer_alias = $entries->join(
column1 => 'id',
table2 => QublogMini::Model::Timer->table,
column2 => 'entry',
);
$entries->limit(
alias => $timer_alias,
column => 'started_on',
operator => '>=',
value => DateTime->today,
entry_aggregator => 'AND',
);
$entries->limit(
alias => $timer_alias,
column => 'started_on',
operator => '<',
value => DateTime->today->add( days => 1 ),
entry_aggregator => 'AND',
);
set entries => $entries;
};
142. template '/add_entry_form' => sub {
my $entry_action = new_action class => 'CreateEntry';
render_action $entry_action, [ qw( name url ) ];
form_submit
label => _('Add Entry'),
onclick => {
submit => $entry_action,
region => Jifty->web->current_region,
replace_with => '/add_entry_button',
},
;
};
143. template '/add_entry_form' => sub {
my $entry_action = new_action class => 'CreateEntry';
render_action $entry_action, [ qw( name url ) ];
form_submit
label => _('Add Entry'),
onclick => {
submit => $entry_action,
region => Jifty->web->current_region,
replace_with => '/add_entry_button',
},
;
};
144. # Make our entry form update everything properly
template '/add_entry_form' => sub {
my $entry_action = new_action class => 'CreateEntry';
render_action $entry_action, [ qw( name url ) ];
form_submit
label => _('Add Entry'),
onclick => [
{
submit => $entry_action,
},
{
region => Jifty->web->current_region,
replace_with => '/add_entry_button',
},
{
refresh => Jifty->web->current_region->parent,
},
],
;
};
145. # Make our entry form update everything properly
template '/add_entry_form' => sub {
my $entry_action = new_action class => 'CreateEntry';
render_action $entry_action, [ qw( name url ) ];
form_submit
label => _('Add Entry'),
onclick => [
{
submit => $entry_action,
},
{
region => Jifty->web->current_region,
replace_with => '/add_entry_button',
},
{
refresh => Jifty->web->current_region->parent,
},
],
;
};
181. Canonicalization
• You can also canonicalize something
• What the heck is that?
• Rather than slap their hand, fix it for them
182. Canonicalization
• You can also canonicalize something
• What the heck is that?
• Rather than slap their hand, fix it for them
• For example, we could... (but won’t)...
183. sub canonicalize_url {
my ($self, $url) = @_;
if ($url !~ m[^https?://]) {
return 'http://'.$url;
}
return $url;
}
184. sub canonicalize_url {
my ($self, $url) = @_;
if ($url !~ m[^https?://]) {
return 'http://'.$url;
}
return $url; Too naïve, I refuse to
}
implement it!
185. But it was the best I
could come up with on
sub canonicalize_url { short notice.
my ($self, $url) = @_;
if ($url !~ m[^https?://]) {
return 'http://'.$url;
}
return $url; Too naïve, I refuse to
}
implement it!
213. Deployment
• Choice of web servers: FastCGI on Apache / Lighttpd, HTTP::Server::Simple,
mod_perl support (a little weak at the moment)
214. Deployment
• Choice of web servers: FastCGI on Apache / Lighttpd, HTTP::Server::Simple,
mod_perl support (a little weak at the moment)
• Choice of DB servers: SQLite (great for getting started/testing, but production
is possible), PostgreSQL, MySQL, and other servers supported
215. Deployment
• Choice of web servers: FastCGI on Apache / Lighttpd, HTTP::Server::Simple,
mod_perl support (a little weak at the moment)
• Choice of DB servers: SQLite (great for getting started/testing, but production
is possible), PostgreSQL, MySQL, and other servers supported
• Use a fast static server to server CSS, JS, images, etc.
216. Deployment
• Choice of web servers: FastCGI on Apache / Lighttpd, HTTP::Server::Simple,
mod_perl support (a little weak at the moment)
• Choice of DB servers: SQLite (great for getting started/testing, but production
is possible), PostgreSQL, MySQL, and other servers supported
• Use a fast static server to server CSS, JS, images, etc.
• Use CSS::Squish and JS::Squish to help compress and cache things
218. Other Goodies
• Several authentication plugins: typical email sign-up login, OpenID,
Facebook, LDAP, CAS
• Additional CRUD view helpers I haven’t demonstrated
• Built-in REST interface
• Full I18N support built-in
• Several Plugins for lots of things: Comments, Single Page Apps, Client-Side
Templating, Charting, Tab helpers
• Built-in support for Comet (server push) in addition to Ajax