Merge Web application.
2
.gitignore
vendored
@ -1,3 +1,5 @@
|
||||
.gradle/
|
||||
*.sw?
|
||||
build/
|
||||
.sass-cache
|
||||
*.build.tar.gz
|
||||
|
26
.ide/change_workspace.pl
Executable file
@ -0,0 +1,26 @@
|
||||
#!/usr/bin/perl -w
|
||||
|
||||
my @files=`ls vim-views/ | grep view`;
|
||||
my $cwd=`pwd`;
|
||||
chomp($cwd);
|
||||
|
||||
$cwd=~s!(.*?)/VBS!$1!;
|
||||
|
||||
print $cwd;
|
||||
|
||||
chdir("vim-views");
|
||||
|
||||
foreach my $file (@files) {
|
||||
chomp($file);
|
||||
system("mv", "$file", "$file.bak");
|
||||
open(IN,"<$file.bak");
|
||||
open(OUT, ">$file");
|
||||
|
||||
while(<IN>) {
|
||||
s!(edit\s).*?(/VBS.*)!$1$cwd$2!;
|
||||
print OUT;
|
||||
}
|
||||
|
||||
close(IN);
|
||||
close(OUT);
|
||||
}
|
9
.ide/clean_trailing_whitespace.sh
Executable file
@ -0,0 +1,9 @@
|
||||
#!/bin/bash
|
||||
|
||||
perl -pi -e 's/[\t\r\f ]+$//g' $@
|
||||
|
||||
for file in $@
|
||||
do
|
||||
rm "$file.bak"
|
||||
echo "$file done!"
|
||||
done
|
20
.ide/line-count.pl
Executable file
@ -0,0 +1,20 @@
|
||||
#!/usr/bin/perl -w
|
||||
|
||||
my $lc=0;
|
||||
my $flc;
|
||||
my $filename;
|
||||
while(<>) {
|
||||
$filename=$_;
|
||||
chomp($filename);
|
||||
open(IN,"<$filename");
|
||||
|
||||
$flc=0;
|
||||
while(<IN>) {
|
||||
$flc++;
|
||||
$lc++;
|
||||
}
|
||||
|
||||
print "$filename: $flc\n";
|
||||
}
|
||||
|
||||
print "Total: $lc\n";
|
8
.ide/vim-ide.vim
Executable file
@ -0,0 +1,8 @@
|
||||
set viewoptions=cursor,folds,options,slash,unix
|
||||
|
||||
let ideHome = $PWD
|
||||
augroup ide
|
||||
au BufWinEnter *.java,*.xml,*.scss,*.yaws,*.html,*.js execute "source ".ideHome."/.ide/vim-views/".strpart(bufname("%"), strridx(bufname("%"), "/") + 1).".view"
|
||||
au BufWinLeave *.java,*.xml,*.scss,*.yaws,*.html,*.js execute "mkview! ".ideHome."/.ide/vim-views/".strpart(bufname("%"), strridx(bufname("%"), "/") + 1).".view"
|
||||
au BufWinLeave *.java,*.xml,*.scss,*.yaws,*.html,*.js execute "silent !echo 'syntax on' >> ".ideHome."/.ide/vim-views/".strpart(bufname("%"), strridx(bufname("%"), "/") + 1).".view"
|
||||
augroup END
|
108
.ide/vim-views/index.yaws.view
Normal file
@ -0,0 +1,108 @@
|
||||
let s:so_save = &so | let s:siso_save = &siso | set so=0 siso=0
|
||||
argglobal
|
||||
edit /mnt/secure/projects/jdb-labs/timestamper/web-app/www/index.yaws
|
||||
setlocal keymap=
|
||||
setlocal noarabic
|
||||
setlocal autoindent
|
||||
setlocal balloonexpr=
|
||||
setlocal nobinary
|
||||
setlocal bufhidden=
|
||||
setlocal buflisted
|
||||
setlocal buftype=
|
||||
setlocal nocindent
|
||||
setlocal cinkeys=0{,0},0),:,0#,!^F,o,O,e
|
||||
setlocal cinoptions=
|
||||
setlocal cinwords=if,else,while,do,for,switch
|
||||
setlocal comments=s1:/*,mb:*,ex:*/,://,b:#,:%,:XCOMM,n:>,fb:-
|
||||
setlocal commentstring=/*%s*/
|
||||
setlocal complete=.,w,b,u,t,i
|
||||
setlocal completefunc=
|
||||
setlocal nocopyindent
|
||||
setlocal nocursorcolumn
|
||||
setlocal nocursorline
|
||||
setlocal define=
|
||||
setlocal dictionary=
|
||||
setlocal nodiff
|
||||
setlocal equalprg=
|
||||
setlocal errorformat=
|
||||
setlocal expandtab
|
||||
if &filetype != 'erlang'
|
||||
setlocal filetype=erlang
|
||||
endif
|
||||
setlocal foldcolumn=0
|
||||
setlocal foldenable
|
||||
setlocal foldexpr=0
|
||||
setlocal foldignore=#
|
||||
setlocal foldlevel=0
|
||||
setlocal foldmarker={{{,}}}
|
||||
setlocal foldmethod=manual
|
||||
setlocal foldminlines=1
|
||||
setlocal foldnestmax=20
|
||||
setlocal foldtext=foldtext()
|
||||
setlocal formatexpr=
|
||||
setlocal formatoptions=tcq
|
||||
setlocal formatlistpat=^\\s*\\d\\+[\\]:.)}\\t\ ]\\s*
|
||||
setlocal grepprg=
|
||||
setlocal iminsert=2
|
||||
setlocal imsearch=2
|
||||
setlocal include=
|
||||
setlocal includeexpr=
|
||||
setlocal indentexpr=
|
||||
setlocal indentkeys=0{,0},:,0#,!^F,o,O,e
|
||||
setlocal noinfercase
|
||||
setlocal iskeyword=@,48-57,_,192-255
|
||||
setlocal keywordprg=
|
||||
setlocal nolinebreak
|
||||
setlocal nolisp
|
||||
setlocal nolist
|
||||
setlocal makeprg=
|
||||
setlocal matchpairs=(:),{:},[:]
|
||||
setlocal nomodeline
|
||||
setlocal modifiable
|
||||
setlocal nrformats=octal,hex
|
||||
setlocal number
|
||||
setlocal numberwidth=4
|
||||
setlocal omnifunc=
|
||||
setlocal path=
|
||||
setlocal nopreserveindent
|
||||
setlocal nopreviewwindow
|
||||
setlocal quoteescape=\\
|
||||
setlocal noreadonly
|
||||
setlocal norightleft
|
||||
setlocal rightleftcmd=search
|
||||
setlocal noscrollbind
|
||||
setlocal shiftwidth=4
|
||||
setlocal noshortname
|
||||
setlocal nosmartindent
|
||||
setlocal softtabstop=0
|
||||
setlocal nospell
|
||||
setlocal spellcapcheck=[.?!]\\_[\\])'\"\ \ ]\\+
|
||||
setlocal spellfile=
|
||||
setlocal spelllang=en
|
||||
setlocal statusline=
|
||||
setlocal suffixesadd=
|
||||
setlocal swapfile
|
||||
setlocal synmaxcol=3000
|
||||
if &syntax != 'erlang'
|
||||
setlocal syntax=erlang
|
||||
endif
|
||||
setlocal tabstop=4
|
||||
setlocal tags=
|
||||
setlocal textwidth=80
|
||||
setlocal thesaurus=
|
||||
setlocal nowinfixheight
|
||||
setlocal nowinfixwidth
|
||||
setlocal wrap
|
||||
setlocal wrapmargin=0
|
||||
silent! normal! zE
|
||||
let s:l = 104 - ((59 * winheight(0) + 35) / 71)
|
||||
if s:l < 1 | let s:l = 1 | endif
|
||||
exe s:l
|
||||
normal! zt
|
||||
104
|
||||
normal! 038l
|
||||
lcd /mnt/secure/projects/jdb-labs/timestamper/web-app/www
|
||||
let &so = s:so_save | let &siso = s:siso_save
|
||||
doautoall SessionLoadPost
|
||||
" vim: set ft=vim :
|
||||
syntax on
|
107
.ide/vim-views/ts-screen.scss.view
Normal file
@ -0,0 +1,107 @@
|
||||
let s:so_save = &so | let s:siso_save = &siso | set so=0 siso=0
|
||||
argglobal
|
||||
edit /mnt/secure/projects/jdb-labs/timestamper/web-app/www/css/ts-screen.scss
|
||||
setlocal keymap=
|
||||
setlocal noarabic
|
||||
setlocal autoindent
|
||||
setlocal balloonexpr=
|
||||
setlocal nobinary
|
||||
setlocal bufhidden=
|
||||
setlocal buflisted
|
||||
setlocal buftype=
|
||||
setlocal nocindent
|
||||
setlocal cinkeys=0{,0},0),:,0#,!^F,o,O,e
|
||||
setlocal cinoptions=
|
||||
setlocal cinwords=if,else,while,do,for,switch
|
||||
setlocal comments=s1:/*,mb:*,ex:*/,://,b:#,:%,:XCOMM,n:>,fb:-
|
||||
setlocal commentstring=/*%s*/
|
||||
setlocal complete=.,w,b,u,t,i
|
||||
setlocal completefunc=
|
||||
setlocal nocopyindent
|
||||
setlocal nocursorcolumn
|
||||
setlocal nocursorline
|
||||
setlocal define=
|
||||
setlocal dictionary=
|
||||
setlocal nodiff
|
||||
setlocal equalprg=
|
||||
setlocal errorformat=
|
||||
setlocal expandtab
|
||||
if &filetype != ''
|
||||
setlocal filetype=
|
||||
endif
|
||||
setlocal foldcolumn=0
|
||||
setlocal foldenable
|
||||
setlocal foldexpr=0
|
||||
setlocal foldignore=#
|
||||
setlocal foldlevel=0
|
||||
setlocal foldmarker={{{,}}}
|
||||
setlocal foldmethod=manual
|
||||
setlocal foldminlines=1
|
||||
setlocal foldnestmax=20
|
||||
setlocal foldtext=foldtext()
|
||||
setlocal formatexpr=
|
||||
setlocal formatoptions=tcq
|
||||
setlocal formatlistpat=^\\s*\\d\\+[\\]:.)}\\t\ ]\\s*
|
||||
setlocal grepprg=
|
||||
setlocal iminsert=2
|
||||
setlocal imsearch=2
|
||||
setlocal include=
|
||||
setlocal includeexpr=
|
||||
setlocal indentexpr=
|
||||
setlocal indentkeys=0{,0},:,0#,!^F,o,O,e
|
||||
setlocal noinfercase
|
||||
setlocal iskeyword=@,48-57,_,192-255
|
||||
setlocal keywordprg=
|
||||
setlocal nolinebreak
|
||||
setlocal nolisp
|
||||
setlocal nolist
|
||||
setlocal makeprg=
|
||||
setlocal matchpairs=(:),{:},[:]
|
||||
setlocal nomodeline
|
||||
setlocal modifiable
|
||||
setlocal nrformats=octal,hex
|
||||
setlocal number
|
||||
setlocal numberwidth=4
|
||||
setlocal omnifunc=
|
||||
setlocal path=
|
||||
setlocal nopreserveindent
|
||||
setlocal nopreviewwindow
|
||||
setlocal quoteescape=\\
|
||||
setlocal noreadonly
|
||||
setlocal norightleft
|
||||
setlocal rightleftcmd=search
|
||||
setlocal noscrollbind
|
||||
setlocal shiftwidth=4
|
||||
setlocal noshortname
|
||||
setlocal nosmartindent
|
||||
setlocal softtabstop=0
|
||||
setlocal nospell
|
||||
setlocal spellcapcheck=[.?!]\\_[\\])'\"\ \ ]\\+
|
||||
setlocal spellfile=
|
||||
setlocal spelllang=en
|
||||
setlocal statusline=
|
||||
setlocal suffixesadd=
|
||||
setlocal swapfile
|
||||
setlocal synmaxcol=3000
|
||||
if &syntax != 'sass'
|
||||
setlocal syntax=sass
|
||||
endif
|
||||
setlocal tabstop=4
|
||||
setlocal tags=
|
||||
setlocal textwidth=80
|
||||
setlocal thesaurus=
|
||||
setlocal nowinfixheight
|
||||
setlocal nowinfixwidth
|
||||
setlocal wrap
|
||||
setlocal wrapmargin=0
|
||||
silent! normal! zE
|
||||
let s:l = 392 - ((46 * winheight(0) + 35) / 71)
|
||||
if s:l < 1 | let s:l = 1 | endif
|
||||
exe s:l
|
||||
normal! zt
|
||||
392
|
||||
normal! 022l
|
||||
let &so = s:so_save | let &siso = s:siso_save
|
||||
doautoall SessionLoadPost
|
||||
" vim: set ft=vim :
|
||||
syntax on
|
143
.ide/vim-views/ts.js.view
Normal file
@ -0,0 +1,143 @@
|
||||
let s:so_save = &so | let s:siso_save = &siso | set so=0 siso=0
|
||||
argglobal
|
||||
edit /mnt/secure/projects/jdb-labs/timestamper/web-app/www/js/ts.js
|
||||
setlocal keymap=
|
||||
setlocal noarabic
|
||||
setlocal autoindent
|
||||
setlocal balloonexpr=
|
||||
setlocal nobinary
|
||||
setlocal bufhidden=
|
||||
setlocal buflisted
|
||||
setlocal buftype=
|
||||
setlocal nocindent
|
||||
setlocal cinkeys=0{,0},0),:,0#,!^F,o,O,e
|
||||
setlocal cinoptions=
|
||||
setlocal cinwords=if,else,while,do,for,switch
|
||||
setlocal comments=s1:/*,mb:*,ex:*/,://,b:#,:%,:XCOMM,n:>,fb:-
|
||||
setlocal commentstring=/*%s*/
|
||||
setlocal complete=.,w,b,u,t,i
|
||||
setlocal completefunc=
|
||||
setlocal nocopyindent
|
||||
setlocal nocursorcolumn
|
||||
setlocal nocursorline
|
||||
setlocal define=
|
||||
setlocal dictionary=
|
||||
setlocal nodiff
|
||||
setlocal equalprg=
|
||||
setlocal errorformat=
|
||||
setlocal expandtab
|
||||
if &filetype != 'javascript'
|
||||
setlocal filetype=javascript
|
||||
endif
|
||||
setlocal foldcolumn=0
|
||||
setlocal foldenable
|
||||
setlocal foldexpr=0
|
||||
setlocal foldignore=#
|
||||
setlocal foldlevel=0
|
||||
setlocal foldmarker={{{,}}}
|
||||
setlocal foldmethod=manual
|
||||
setlocal foldminlines=1
|
||||
setlocal foldnestmax=20
|
||||
setlocal foldtext=foldtext()
|
||||
setlocal formatexpr=
|
||||
setlocal formatoptions=tcq
|
||||
setlocal formatlistpat=^\\s*\\d\\+[\\]:.)}\\t\ ]\\s*
|
||||
setlocal grepprg=
|
||||
setlocal iminsert=2
|
||||
setlocal imsearch=2
|
||||
setlocal include=
|
||||
setlocal includeexpr=
|
||||
setlocal indentexpr=
|
||||
setlocal indentkeys=0{,0},:,0#,!^F,o,O,e
|
||||
setlocal noinfercase
|
||||
setlocal iskeyword=@,48-57,_,192-255
|
||||
setlocal keywordprg=
|
||||
setlocal nolinebreak
|
||||
setlocal nolisp
|
||||
setlocal nolist
|
||||
setlocal makeprg=
|
||||
setlocal matchpairs=(:),{:},[:]
|
||||
setlocal nomodeline
|
||||
setlocal modifiable
|
||||
setlocal nrformats=octal,hex
|
||||
setlocal number
|
||||
setlocal numberwidth=4
|
||||
setlocal omnifunc=
|
||||
setlocal path=
|
||||
setlocal nopreserveindent
|
||||
setlocal nopreviewwindow
|
||||
setlocal quoteescape=\\
|
||||
setlocal noreadonly
|
||||
setlocal norightleft
|
||||
setlocal rightleftcmd=search
|
||||
setlocal noscrollbind
|
||||
setlocal shiftwidth=4
|
||||
setlocal noshortname
|
||||
setlocal nosmartindent
|
||||
setlocal softtabstop=0
|
||||
setlocal nospell
|
||||
setlocal spellcapcheck=[.?!]\\_[\\])'\"\ \ ]\\+
|
||||
setlocal spellfile=
|
||||
setlocal spelllang=en
|
||||
setlocal statusline=
|
||||
setlocal suffixesadd=
|
||||
setlocal swapfile
|
||||
setlocal synmaxcol=3000
|
||||
if &syntax != 'javascript'
|
||||
setlocal syntax=javascript
|
||||
endif
|
||||
setlocal tabstop=4
|
||||
setlocal tags=
|
||||
setlocal textwidth=80
|
||||
setlocal thesaurus=
|
||||
setlocal nowinfixheight
|
||||
setlocal nowinfixwidth
|
||||
setlocal wrap
|
||||
setlocal wrapmargin=0
|
||||
silent! normal! zE
|
||||
9,18fold
|
||||
20,28fold
|
||||
30,43fold
|
||||
45,62fold
|
||||
64,82fold
|
||||
87,289fold
|
||||
291,378fold
|
||||
380,448fold
|
||||
450,487fold
|
||||
489,603fold
|
||||
605,664fold
|
||||
666,702fold
|
||||
9
|
||||
normal zc
|
||||
20
|
||||
normal zc
|
||||
30
|
||||
normal zc
|
||||
45
|
||||
normal zo
|
||||
64
|
||||
normal zc
|
||||
87
|
||||
normal zc
|
||||
291
|
||||
normal zo
|
||||
380
|
||||
normal zc
|
||||
450
|
||||
normal zc
|
||||
489
|
||||
normal zo
|
||||
605
|
||||
normal zc
|
||||
666
|
||||
normal zc
|
||||
let s:l = 334 - ((273 * winheight(0) + 35) / 71)
|
||||
if s:l < 1 | let s:l = 1 | endif
|
||||
exe s:l
|
||||
normal! zt
|
||||
334
|
||||
normal! 042l
|
||||
let &so = s:so_save | let &siso = s:siso_save
|
||||
doautoall SessionLoadPost
|
||||
" vim: set ft=vim :
|
||||
syntax on
|
185
.ide/vim-views/ts_api.erl.view
Normal file
@ -0,0 +1,185 @@
|
||||
let s:so_save = &so | let s:siso_save = &siso | set so=0 siso=0
|
||||
argglobal
|
||||
edit /mnt/secure/projects/jdb-labs/timestamper/web-app/src/ts_api.erl
|
||||
setlocal keymap=
|
||||
setlocal noarabic
|
||||
setlocal autoindent
|
||||
setlocal balloonexpr=
|
||||
setlocal nobinary
|
||||
setlocal bufhidden=
|
||||
setlocal buflisted
|
||||
setlocal buftype=
|
||||
setlocal nocindent
|
||||
setlocal cinkeys=0{,0},0),:,0#,!^F,o,O,e
|
||||
setlocal cinoptions=
|
||||
setlocal cinwords=if,else,while,do,for,switch
|
||||
setlocal comments=s1:/*,mb:*,ex:*/,://,b:#,:%,:XCOMM,n:>,fb:-
|
||||
setlocal commentstring=/*%s*/
|
||||
setlocal complete=.,w,b,u,t,i
|
||||
setlocal completefunc=
|
||||
setlocal nocopyindent
|
||||
setlocal nocursorcolumn
|
||||
setlocal nocursorline
|
||||
setlocal define=
|
||||
setlocal dictionary=
|
||||
setlocal nodiff
|
||||
setlocal equalprg=
|
||||
setlocal errorformat=
|
||||
setlocal expandtab
|
||||
if &filetype != 'erlang'
|
||||
setlocal filetype=erlang
|
||||
endif
|
||||
setlocal foldcolumn=0
|
||||
setlocal foldenable
|
||||
setlocal foldexpr=0
|
||||
setlocal foldignore=#
|
||||
setlocal foldlevel=0
|
||||
setlocal foldmarker={{{,}}}
|
||||
setlocal foldmethod=manual
|
||||
setlocal foldminlines=1
|
||||
setlocal foldnestmax=20
|
||||
setlocal foldtext=foldtext()
|
||||
setlocal formatexpr=
|
||||
setlocal formatoptions=tcq
|
||||
setlocal formatlistpat=^\\s*\\d\\+[\\]:.)}\\t\ ]\\s*
|
||||
setlocal grepprg=
|
||||
setlocal iminsert=2
|
||||
setlocal imsearch=2
|
||||
setlocal include=
|
||||
setlocal includeexpr=
|
||||
setlocal indentexpr=
|
||||
setlocal indentkeys=0{,0},:,0#,!^F,o,O,e
|
||||
setlocal noinfercase
|
||||
setlocal iskeyword=@,48-57,_,192-255
|
||||
setlocal keywordprg=
|
||||
setlocal nolinebreak
|
||||
setlocal nolisp
|
||||
setlocal nolist
|
||||
setlocal makeprg=
|
||||
setlocal matchpairs=(:),{:},[:]
|
||||
setlocal nomodeline
|
||||
setlocal modifiable
|
||||
setlocal nrformats=octal,hex
|
||||
setlocal number
|
||||
setlocal numberwidth=4
|
||||
setlocal omnifunc=
|
||||
setlocal path=
|
||||
setlocal nopreserveindent
|
||||
setlocal nopreviewwindow
|
||||
setlocal quoteescape=\\
|
||||
setlocal noreadonly
|
||||
setlocal norightleft
|
||||
setlocal rightleftcmd=search
|
||||
setlocal noscrollbind
|
||||
setlocal shiftwidth=4
|
||||
setlocal noshortname
|
||||
setlocal nosmartindent
|
||||
setlocal softtabstop=0
|
||||
setlocal nospell
|
||||
setlocal spellcapcheck=[.?!]\\_[\\])'\"\ \ ]\\+
|
||||
setlocal spellfile=
|
||||
setlocal spelllang=en
|
||||
setlocal statusline=
|
||||
setlocal suffixesadd=
|
||||
setlocal swapfile
|
||||
setlocal synmaxcol=3000
|
||||
if &syntax != 'erlang'
|
||||
setlocal syntax=erlang
|
||||
endif
|
||||
setlocal tabstop=4
|
||||
setlocal tags=
|
||||
setlocal textwidth=80
|
||||
setlocal thesaurus=
|
||||
setlocal nowinfixheight
|
||||
setlocal nowinfixwidth
|
||||
setlocal wrap
|
||||
setlocal wrapmargin=0
|
||||
silent! normal! zE
|
||||
7,28fold
|
||||
35,51fold
|
||||
55,73fold
|
||||
77,92fold
|
||||
96,126fold
|
||||
130,161fold
|
||||
167,196fold
|
||||
198,202fold
|
||||
204,239fold
|
||||
241,248fold
|
||||
250,267fold
|
||||
269,301fold
|
||||
303,310fold
|
||||
312,331fold
|
||||
335,421fold
|
||||
423,429fold
|
||||
431,453fold
|
||||
455,469fold
|
||||
471,486fold
|
||||
492,499fold
|
||||
502,506fold
|
||||
508,515fold
|
||||
517,525fold
|
||||
528,539fold
|
||||
541,552fold
|
||||
554,563fold
|
||||
7
|
||||
normal zc
|
||||
35
|
||||
normal zc
|
||||
55
|
||||
normal zc
|
||||
77
|
||||
normal zc
|
||||
96
|
||||
normal zc
|
||||
130
|
||||
normal zc
|
||||
167
|
||||
normal zc
|
||||
198
|
||||
normal zc
|
||||
204
|
||||
normal zo
|
||||
241
|
||||
normal zc
|
||||
250
|
||||
normal zc
|
||||
269
|
||||
normal zc
|
||||
303
|
||||
normal zc
|
||||
312
|
||||
normal zo
|
||||
335
|
||||
normal zc
|
||||
423
|
||||
normal zc
|
||||
431
|
||||
normal zc
|
||||
455
|
||||
normal zc
|
||||
471
|
||||
normal zc
|
||||
492
|
||||
normal zc
|
||||
502
|
||||
normal zc
|
||||
508
|
||||
normal zc
|
||||
517
|
||||
normal zc
|
||||
528
|
||||
normal zc
|
||||
541
|
||||
normal zc
|
||||
554
|
||||
normal zc
|
||||
let s:l = 250 - ((25 * winheight(0) + 35) / 71)
|
||||
if s:l < 1 | let s:l = 1 | endif
|
||||
exe s:l
|
||||
normal! zt
|
||||
250
|
||||
normal! 0
|
||||
let &so = s:so_save | let &siso = s:siso_save
|
||||
doautoall SessionLoadPost
|
||||
" vim: set ft=vim :
|
||||
syntax on
|
107
.ide/vim-views/ts_common.erl.view
Normal file
@ -0,0 +1,107 @@
|
||||
let s:so_save = &so | let s:siso_save = &siso | set so=0 siso=0
|
||||
argglobal
|
||||
edit ~/projects/jdb-labs/timestamper/web-app/src/ts_common.erl
|
||||
setlocal keymap=
|
||||
setlocal noarabic
|
||||
setlocal autoindent
|
||||
setlocal balloonexpr=
|
||||
setlocal nobinary
|
||||
setlocal bufhidden=
|
||||
setlocal buflisted
|
||||
setlocal buftype=
|
||||
setlocal nocindent
|
||||
setlocal cinkeys=0{,0},0),:,0#,!^F,o,O,e
|
||||
setlocal cinoptions=
|
||||
setlocal cinwords=if,else,while,do,for,switch
|
||||
setlocal comments=s1:/*,mb:*,ex:*/,://,b:#,:%,:XCOMM,n:>,fb:-
|
||||
setlocal commentstring=/*%s*/
|
||||
setlocal complete=.,w,b,u,t,i
|
||||
setlocal completefunc=
|
||||
setlocal nocopyindent
|
||||
setlocal nocursorcolumn
|
||||
setlocal nocursorline
|
||||
setlocal define=
|
||||
setlocal dictionary=
|
||||
setlocal nodiff
|
||||
setlocal equalprg=
|
||||
setlocal errorformat=
|
||||
setlocal expandtab
|
||||
if &filetype != 'erlang'
|
||||
setlocal filetype=erlang
|
||||
endif
|
||||
setlocal foldcolumn=0
|
||||
setlocal foldenable
|
||||
setlocal foldexpr=0
|
||||
setlocal foldignore=#
|
||||
setlocal foldlevel=0
|
||||
setlocal foldmarker={{{,}}}
|
||||
setlocal foldmethod=manual
|
||||
setlocal foldminlines=1
|
||||
setlocal foldnestmax=20
|
||||
setlocal foldtext=foldtext()
|
||||
setlocal formatexpr=
|
||||
setlocal formatoptions=tcq
|
||||
setlocal formatlistpat=^\\s*\\d\\+[\\]:.)}\\t\ ]\\s*
|
||||
setlocal grepprg=
|
||||
setlocal iminsert=2
|
||||
setlocal imsearch=2
|
||||
setlocal include=
|
||||
setlocal includeexpr=
|
||||
setlocal indentexpr=
|
||||
setlocal indentkeys=0{,0},:,0#,!^F,o,O,e
|
||||
setlocal noinfercase
|
||||
setlocal iskeyword=@,48-57,_,192-255
|
||||
setlocal keywordprg=
|
||||
setlocal nolinebreak
|
||||
setlocal nolisp
|
||||
setlocal nolist
|
||||
setlocal makeprg=
|
||||
setlocal matchpairs=(:),{:},[:]
|
||||
setlocal nomodeline
|
||||
setlocal modifiable
|
||||
setlocal nrformats=octal,hex
|
||||
setlocal number
|
||||
setlocal numberwidth=4
|
||||
setlocal omnifunc=
|
||||
setlocal path=
|
||||
setlocal nopreserveindent
|
||||
setlocal nopreviewwindow
|
||||
setlocal quoteescape=\\
|
||||
setlocal noreadonly
|
||||
setlocal norightleft
|
||||
setlocal rightleftcmd=search
|
||||
setlocal noscrollbind
|
||||
setlocal shiftwidth=4
|
||||
setlocal noshortname
|
||||
setlocal nosmartindent
|
||||
setlocal softtabstop=0
|
||||
setlocal nospell
|
||||
setlocal spellcapcheck=[.?!]\\_[\\])'\"\ \ ]\\+
|
||||
setlocal spellfile=
|
||||
setlocal spelllang=en
|
||||
setlocal statusline=
|
||||
setlocal suffixesadd=
|
||||
setlocal swapfile
|
||||
setlocal synmaxcol=3000
|
||||
if &syntax != 'erlang'
|
||||
setlocal syntax=erlang
|
||||
endif
|
||||
setlocal tabstop=4
|
||||
setlocal tags=
|
||||
setlocal textwidth=80
|
||||
setlocal thesaurus=
|
||||
setlocal nowinfixheight
|
||||
setlocal nowinfixwidth
|
||||
setlocal wrap
|
||||
setlocal wrapmargin=0
|
||||
silent! normal! zE
|
||||
let s:l = 16 - ((14 * winheight(0) + 23) / 46)
|
||||
if s:l < 1 | let s:l = 1 | endif
|
||||
exe s:l
|
||||
normal! zt
|
||||
16
|
||||
normal! 034l
|
||||
let &so = s:so_save | let &siso = s:siso_save
|
||||
doautoall SessionLoadPost
|
||||
" vim: set ft=vim :
|
||||
syntax on
|
86
Makefile
Normal file
@ -0,0 +1,86 @@
|
||||
MODS = $(wildcard src/*.erl)
|
||||
BEAMS = $(MODS:src/%.erl=build/ebin/%.beam)
|
||||
SCSS = $(wildcard www/css/*.scss)
|
||||
CSS_FILES = $(SCSS:www/css/%.scss=build/www/css/%.css)
|
||||
TEST_MODS = $(wildcard test/*.erl)
|
||||
TEST_BEAMS = $(TEST_MODS:test/%.erl=build/test/%.beam)
|
||||
TS_ROOT=/usr/local/var/yaws/timestamper.jdb-labs.com
|
||||
TS_ROOT_DEV=/home/jdbernard/temp/timestamper.jdb-labs.com
|
||||
BUILD_SERVER=dev.jdb-labs.com
|
||||
BUILD_SOURCE=/~jdbernard/projects/timestamper/web-app
|
||||
CWD = `pwd`
|
||||
|
||||
default: build
|
||||
|
||||
all : compile test
|
||||
|
||||
compile : init $(BEAMS) $(CSS_FILES)
|
||||
|
||||
compile-test : init $(TEST_BEAMS)
|
||||
|
||||
test : start-test-server run-test stop-test-server
|
||||
|
||||
test-shell : compile compile-test config-yaws-dev
|
||||
@echo Starting an interactive YAWS shell with test paths loaded.
|
||||
@yaws -i --pa build/ebin --pa build/test --id test_inst
|
||||
|
||||
run-test : compile compile-test config-yaws-dev
|
||||
@erl -pa ./build/ebin -pa ./build/test -run timestamper_api_tests test -run init stop -noshell
|
||||
|
||||
start-test-server :
|
||||
@yaws -D --id test_inst
|
||||
|
||||
stop-test-server :
|
||||
@yaws --stop --id test_inst
|
||||
|
||||
clean:
|
||||
rm -rf build*
|
||||
|
||||
init:
|
||||
-mkdir -p build/ebin
|
||||
-mkdir -p build/www/css
|
||||
-mkdir -p build/www/js
|
||||
-mkdir -p build/www/img
|
||||
|
||||
build/ebin/%.beam : src/%.erl
|
||||
erlc -W -o build/ebin $<
|
||||
|
||||
build/test/%.beam : test/%.erl
|
||||
@echo Compiling sources...
|
||||
erlc -W -o build/test $<
|
||||
|
||||
build/www/css/%.css : www/css/%.scss
|
||||
scss $< $@
|
||||
|
||||
build: compile
|
||||
-mkdir -p build/include
|
||||
cp -r www/js build/www/
|
||||
cp -r www/img build/www/
|
||||
cp -r www/*.* build/www/
|
||||
cp lib/* build/ebin
|
||||
cp src/ts_db_records.hrl build/include
|
||||
cp yaws.prod.conf build/yaws.conf
|
||||
tar czf timestamper-web.build.tar.gz build
|
||||
|
||||
deploy: build
|
||||
@service yaws stop
|
||||
@echo Removing existing artifacts.
|
||||
- @rm -r "$(TS_ROOT)"
|
||||
@echo Copying current artifacts.
|
||||
@cp -r build "$(TS_ROOT)"
|
||||
@service yaws start
|
||||
@echo Done.
|
||||
|
||||
deploy-dev: build
|
||||
@echo Removing existing artifacts.
|
||||
- rm -r $(TS_ROOT_DEV)
|
||||
@echo Copying current artifacts.
|
||||
cp -r build $(TS_ROOT_DEV)
|
||||
@echo Altering configuration for DEV.
|
||||
sed -i 's@$(TS_ROOT)@$(TS_ROOT_DEV)@' $(TS_ROOT_DEV)/yaws.conf
|
||||
# mv "$(TS_ROOT_DEV)/www" "$(TS_ROOT_DEV)/timestamper"
|
||||
# mkdir "$(TS_ROOT_DEV)/www"
|
||||
# mv "$(TS_ROOT_DEV)/timestamper" "$(TS_ROOT_DEV)/www/timestamper"
|
||||
@echo Done.
|
||||
|
||||
|
BIN
db/test/DECISION_TAB.LOG
Normal file
BIN
db/test/LATEST.LOG
Normal file
BIN
db/test/id_counter.DCD
Normal file
BIN
db/test/id_counter.DCL
Normal file
BIN
db/test/schema.DAT
Normal file
1
db/test/ts_entry.DCD
Normal file
@ -0,0 +1 @@
|
||||
cXM
|
BIN
db/test/ts_entry.DCL
Normal file
BIN
db/test/ts_ext_data.DCD
Normal file
BIN
db/test/ts_timeline.DCD
Normal file
BIN
db/test/ts_timeline.DCL
Normal file
BIN
db/test/ts_user.DCD
Normal file
97
doc/api.rst
Normal file
@ -0,0 +1,97 @@
|
||||
TimeStamper Web Service API
|
||||
===========================
|
||||
|
||||
This document describes the REST API the TimeStamper web service exposes.
|
||||
|
||||
General Assumptions and Definitions
|
||||
-----------------------------------
|
||||
|
||||
Values that vary are notated using typeset in *emphasized text* when they
|
||||
appear in the text of the documentation and are ``<enclosed by angle
|
||||
brackets>`` when they appear in code examples or other monospaced text.
|
||||
|
||||
Paths in this document are relative to the server root. So
|
||||
``/<user-id>/current`` refers to ``http://sub.domain.tld/<user-id>/current``
|
||||
|
||||
Any paths inteded to be interpreted differently will use the full, absolute
|
||||
form (ie. ``http://www.twitter.com/bob``)
|
||||
|
||||
User Management: ``/<user-id>``
|
||||
-------------------------------
|
||||
|
||||
All requests directed at the user id URL are related to user management. The
|
||||
request is further interpreted by the request type. This resource responds to
|
||||
the ``GET``, ``POST``, ``PUT``, and ``DELETE`` HTTP verbs.
|
||||
|
||||
*user-id*:
|
||||
The user identifier, or username. This is a string value; valid characters
|
||||
are ``[a-zA-Z0-9_]``.
|
||||
|
||||
PUT
|
||||
~~~
|
||||
|
||||
Create a new user. A new user is created only if there is not an existing user
|
||||
with the same *user-id*. If
|
||||
|
||||
Returns:
|
||||
* ``201 Created`` if the user was successfully created.
|
||||
* ``409 Conflict`` if a user already exists for this *user-id*.
|
||||
|
||||
.. TODO: input format, preconditions, other returns
|
||||
|
||||
GET
|
||||
~~~
|
||||
|
||||
Returns the information about the user for *user-id*.
|
||||
|
||||
Return:
|
||||
* ``200 OK``
|
||||
* ``404 Not Found`` if there is no user for *user-id*.
|
||||
|
||||
POST
|
||||
~~~~
|
||||
|
||||
Updates information about the user for *user-id*.
|
||||
|
||||
DELETE
|
||||
~~~~~~
|
||||
|
||||
Deletes the user for *user-id*.
|
||||
|
||||
Timelines: ``/<user-id>/<timeline-id>``
|
||||
---------------------------------------
|
||||
|
||||
*user-id*:
|
||||
See `User Management`_.
|
||||
|
||||
*timeline-id*:
|
||||
A timeline identifier. A string; valid characters are ``[a-zA-Z0-9_]``.
|
||||
|
||||
GET
|
||||
~~~
|
||||
|
||||
Returns the timeline meta-data.
|
||||
|
||||
POST
|
||||
~~~~
|
||||
|
||||
Update timeline meta-data.
|
||||
|
||||
PUT
|
||||
~~~
|
||||
|
||||
Create a new timeline.
|
||||
|
||||
DELETE
|
||||
~~~~~~
|
||||
|
||||
Delete a timeline.
|
||||
|
||||
List Events
|
||||
-----------
|
||||
|
||||
``/<user-id>/<timeline-id>/list``
|
||||
---------------------------------
|
||||
|
||||
GET
|
||||
~~~
|
24
doc/db_layer.rst
Normal file
@ -0,0 +1,24 @@
|
||||
TimeStamper DB Layer
|
||||
====================
|
||||
|
||||
The following modules make up the database layer:
|
||||
|
||||
* ``ts_user``: Interface to user data.
|
||||
* ``ts_timeline``: Interface to timeline data.
|
||||
* ``ts_entry``: Interface to timeline entry data.
|
||||
* ``ts_ext_data``: Interface to extended data that can be set on different
|
||||
records.
|
||||
* ``ts_db_records``: Definition of data records.
|
||||
|
||||
The following modules and files are implementation details of the DB layer:
|
||||
|
||||
* ``id_counter``: Adds support for unique, sequential ID generation.
|
||||
* ``ts_common``: Provides the implementation for any operations that are common
|
||||
between the different interfaces.
|
||||
|
||||
Philosophy
|
||||
----------
|
||||
|
||||
The database layer should abstract all database-specific code away from the
|
||||
caller. Users of the DB layer should not have to think about transactions,
|
||||
locking, etc.
|
4
doc/features.todo.txt
Normal file
@ -0,0 +1,4 @@
|
||||
- Switch to local storage if unable to reach the server, sync when server is
|
||||
available.
|
||||
- Provide full-text search on timestamp marks and notes. Use Lucene in a
|
||||
seperate process? Build our own Erlang indexing code?
|
4
doc/issues/0000tn4.rst
Normal file
@ -0,0 +1,4 @@
|
||||
Refactor models and views.
|
||||
==========================
|
||||
|
||||
Try to find the behavior that is common to mobile and desktop versions.
|
2
doc/issues/desktop/0000fs5.rst
Normal file
@ -0,0 +1,2 @@
|
||||
Add UI for note taking.
|
||||
=======================
|
9
doc/issues/desktop/0001fs5.rst
Normal file
@ -0,0 +1,9 @@
|
||||
Add Markdown converter for notes.
|
||||
=================================
|
||||
|
||||
Brief description.
|
||||
|
||||
========= ==========
|
||||
Created: 2011-05-15
|
||||
Resolved: 2011-05-15
|
||||
========= ==========
|
11
doc/issues/desktop/0002bs5.rst
Normal file
@ -0,0 +1,11 @@
|
||||
Duration mis-set on new entries.
|
||||
================================
|
||||
|
||||
Fix the duration bug when adding new events. Need to set the nextModel for
|
||||
the previously 'current' timestamp and set the nextModel of the new timestamp
|
||||
to 'null'
|
||||
|
||||
========= ==========
|
||||
Created: 2011-05-15
|
||||
Resolved: 2011-05-15
|
||||
========= ==========
|
15
doc/issues/desktop/0003ts3.rst
Normal file
@ -0,0 +1,15 @@
|
||||
Generate day seperators.
|
||||
========================
|
||||
|
||||
When generating EventViews in the EventListView, we need to automatically
|
||||
create and insert day seperators (see prototype).
|
||||
|
||||
Resolution
|
||||
----------
|
||||
|
||||
Day separators are added to the timeline by EntryListView.render.
|
||||
|
||||
========= ==========
|
||||
Created: 2011-05-15
|
||||
Resolved: 2011-05-17
|
||||
========= ==========
|
10
doc/issues/desktop/0004bn5.rst
Normal file
@ -0,0 +1,10 @@
|
||||
Fix UI for tasks with a duration a day or longer.
|
||||
=================================================
|
||||
|
||||
Tasks that are extremely long-running can overflow the space set aside for
|
||||
the *Duration* column.
|
||||
|
||||
========= ==========
|
||||
Created: 2011-05-15
|
||||
Resolved: YYYY-MM-DD
|
||||
========= ==========
|
14
doc/issues/desktop/0005bs2.rst
Normal file
@ -0,0 +1,14 @@
|
||||
Fix user menu UI.
|
||||
=================
|
||||
|
||||
UI for user menu does not work.
|
||||
|
||||
Resolution
|
||||
----------
|
||||
|
||||
UI menu changed to be displayed to the right of the username, in the empty black space vailable.
|
||||
|
||||
========= ==========
|
||||
Created: 2011-05-15
|
||||
Resolved: 2011-06-01
|
||||
========= ==========
|
9
doc/issues/desktop/0006bs2.rst
Normal file
@ -0,0 +1,9 @@
|
||||
Fix timeline menu UI.
|
||||
=====================
|
||||
|
||||
UI for timeline menu does not work.
|
||||
|
||||
========= ==========
|
||||
Created: 2011-05-15
|
||||
Resolved: 2011-06-07
|
||||
========= ==========
|
9
doc/issues/desktop/0007ts2.rst
Normal file
@ -0,0 +1,9 @@
|
||||
Implement timeline selection.
|
||||
=============================
|
||||
|
||||
Allow the user to change timelines.
|
||||
|
||||
========= ==========
|
||||
Created: 2011-05-15
|
||||
Resolved: 2011-06-08
|
||||
========= ==========
|
15
doc/issues/desktop/0008ts2.rst
Normal file
@ -0,0 +1,15 @@
|
||||
Create UI for timeline creation.
|
||||
================================
|
||||
|
||||
There should be a way through the interface for a user to create a new
|
||||
timeline.
|
||||
|
||||
Resolution
|
||||
----------
|
||||
|
||||
Abstracted the login dialog CSS to support other dialogs of the same look and feel. Used this to create a dialog for creating new timelines.
|
||||
|
||||
========= ==========
|
||||
Created: 2011-05-15
|
||||
Resolved: 2011-06-01
|
||||
========= ==========
|
12
doc/issues/desktop/0009tn3.rst
Normal file
@ -0,0 +1,12 @@
|
||||
Implement correct start time editor.
|
||||
====================================
|
||||
|
||||
The start time input field needs to look the same as the start time view.
|
||||
Alternatively, use a date picker.
|
||||
|
||||
----
|
||||
|
||||
========= ==========
|
||||
Created: 2011-05-15
|
||||
Resolved: YYYY-MM-DD
|
||||
========= ==========
|
18
doc/issues/desktop/0010tn4.rst
Normal file
@ -0,0 +1,18 @@
|
||||
Implement UI for entry re-order when chronological order changes.
|
||||
=================================================================
|
||||
|
||||
The UI should re-order the EntryList display when the chronological of the entries
|
||||
changes based on user input.
|
||||
|
||||
Two ways to do this spring to mind:
|
||||
|
||||
1. ``slideUp`` the EntryView at it's original position, find the new position,
|
||||
move it in the DOM and ``slideDown`` the element into view.
|
||||
2. Detach the element from the list, position it absolutely, animate it to it's new
|
||||
absolute position (based on the position of its new neighbors) and re-insert it
|
||||
into the list.
|
||||
|
||||
========= ==========
|
||||
Created: 2011-05-15
|
||||
Resolved: YYYY-MM-DD
|
||||
========= ==========
|
10
doc/issues/desktop/0011fn5.rst
Normal file
@ -0,0 +1,10 @@
|
||||
Create a subtle alternating background for EntryViews
|
||||
=====================================================
|
||||
|
||||
The goal is to visually tie together elements in the same row, and subtley
|
||||
distinquish each row from its neighbors.
|
||||
|
||||
========= ==========
|
||||
Created: 2011-05-15
|
||||
Resolved: YYYY-MM-DD
|
||||
========= ==========
|
9
doc/issues/desktop/0012fn6.rst
Normal file
@ -0,0 +1,9 @@
|
||||
Create tooltip/some help system.
|
||||
================================
|
||||
|
||||
Need some way to make actions discoverable and easy to understand.
|
||||
|
||||
========= ==========
|
||||
Created: 2011-05-15
|
||||
Resolved: YYYY-MM-DD
|
||||
========= ==========
|
10
doc/issues/desktop/0013fn5.rst
Normal file
@ -0,0 +1,10 @@
|
||||
Create a real-time tick-tock for the current entry duration.
|
||||
============================================================
|
||||
|
||||
Have some visible indication that the display is being updated as time passes.
|
||||
Blink the field, or just the units.
|
||||
|
||||
========= ==========
|
||||
Created: 2011-05-15
|
||||
Resolved: YYYY-MM-DD
|
||||
========= ==========
|
12
doc/issues/desktop/0014fn6.rst
Normal file
@ -0,0 +1,12 @@
|
||||
Automatic code highlighting.
|
||||
============================
|
||||
|
||||
Look at content in ``<pre>`` and ``<code>`` blocks and see if we can highlight it.
|
||||
Planning to use Highlight.js to do this.
|
||||
|
||||
----
|
||||
|
||||
========= ==========
|
||||
Created: 2011-05-15
|
||||
Resolved: YYYY-MM-DD
|
||||
========= ==========
|
9
doc/issues/desktop/0015ts2.rst
Normal file
@ -0,0 +1,9 @@
|
||||
Create new timeline button in timeline menu.
|
||||
============================================
|
||||
|
||||
Need a button to trigger the new timeline dialog.
|
||||
|
||||
========= ==========
|
||||
Created: 2011-06-01
|
||||
Resolved: 2011-06-07
|
||||
========= ==========
|
7
doc/issues/desktop/0016bn5.rst
Normal file
@ -0,0 +1,7 @@
|
||||
Duration of next previous entries are not updated when a timestamp is updated.
|
||||
==============================================================================
|
||||
|
||||
========= ==========
|
||||
Created: 2011-06-01
|
||||
Resolved: YYYY-MM-DD
|
||||
========= ==========
|
7
doc/issues/desktop/0017ts2.rst
Normal file
@ -0,0 +1,7 @@
|
||||
Implement timeline creation.
|
||||
============================
|
||||
|
||||
========= ==========
|
||||
Created: 2011-06-01
|
||||
Resolved: 2011-06-07
|
||||
========= ==========
|
15
doc/issues/desktop/0018bs2.rst
Normal file
@ -0,0 +1,15 @@
|
||||
Error in UI when creating or selecting a timeline.
|
||||
==================================================
|
||||
|
||||
A type error is occurring whenever a timeline link is clicked.
|
||||
|
||||
Resolution
|
||||
----------
|
||||
|
||||
The problem was an event registered on the AppView class, linked
|
||||
to 'selectTimeline' function, which was not defined.
|
||||
|
||||
========= ==========
|
||||
Created: 2011-06-07
|
||||
Resolved: 2011-06-08
|
||||
========= ==========
|
16
doc/issues/desktop/0019bn3.rst
Normal file
@ -0,0 +1,16 @@
|
||||
API calls fail silently after session timeout.
|
||||
==============================================
|
||||
|
||||
API starts failing with a 500 error after session time-out.
|
||||
|
||||
Steps to Reproduce
|
||||
------------------
|
||||
|
||||
1. Login
|
||||
2. Wait until session times out (5 min?)
|
||||
3. Try to create a new entry.
|
||||
|
||||
========= ==========
|
||||
Created: 2011-06-08
|
||||
Resolved: YYYY-MM-DD
|
||||
========= ==========
|
22
doc/issues/desktop/0020fs5.rst
Normal file
@ -0,0 +1,22 @@
|
||||
Add exclusion filter for entries.
|
||||
=================================
|
||||
|
||||
Add a way for users to add regexes for entries to be ignored in the
|
||||
display. Exclusions may be per-timeline or per-user.
|
||||
|
||||
Resolution
|
||||
----------
|
||||
|
||||
``ts_user`` and ``ts_timeline`` both support an exnteded data property called
|
||||
``entry_exclusions`` which accepts a list of regular expression strings. Any entries
|
||||
whose marks match any of the regular expressions should be excluded from view.
|
||||
|
||||
EntryListView.render filters out any matching entries from its display. It still
|
||||
takes them into account when calculating the duration of other entries.
|
||||
|
||||
----
|
||||
|
||||
========= ==========
|
||||
Created: 2011-06-10
|
||||
Resolved: 2011-06-15
|
||||
========= ==========
|
7
doc/issues/desktop/0021ts5.rst
Normal file
@ -0,0 +1,7 @@
|
||||
Constrain width of notes fields to width of mark.
|
||||
=================================================
|
||||
|
||||
========= ==========
|
||||
Created: 2011-06-10
|
||||
Resolved: 2011-06-17
|
||||
====================
|
12
doc/issues/desktop/0022bn5.rst
Normal file
@ -0,0 +1,12 @@
|
||||
Deleting an entry should cascade delete extended data.
|
||||
======================================================
|
||||
|
||||
Currently the data remains in the database. It should not cause any
|
||||
problems, but it is wasting space.
|
||||
|
||||
----
|
||||
|
||||
========= ==========
|
||||
Created: 2011-06-15
|
||||
Resolved: YYYY-MM-DD
|
||||
========= ==========
|
12
doc/issues/desktop/0023bn7.rst
Normal file
@ -0,0 +1,12 @@
|
||||
Check for exclusion after mark update.
|
||||
======================================
|
||||
|
||||
If a user rewrites the mark of an entry, we should check to see if it now
|
||||
matches one of the defined entry exclusions.
|
||||
|
||||
----
|
||||
|
||||
========= ==========
|
||||
Created: YYYY-MM-DD
|
||||
Resolved: YYYY-MM-DD
|
||||
========= ==========
|
11
doc/issues/desktop/0024tn5.rst
Normal file
@ -0,0 +1,11 @@
|
||||
Hide day separators when all entries that day are excluded.
|
||||
===========================================================
|
||||
|
||||
We should hide headings when there is no visible data.
|
||||
|
||||
----
|
||||
|
||||
========= ==========
|
||||
Created: 2011-06-17
|
||||
Resolved: YYYY-MM-DD
|
||||
========= ==========
|
21
doc/issues/desktop/0025tn5.rst
Normal file
@ -0,0 +1,21 @@
|
||||
Never animate excluded entries.
|
||||
===============================
|
||||
|
||||
We need to have some visible indication when a user enters an
|
||||
exlucded item that it was added, but it looks ugly and dirty when
|
||||
we first render the list.
|
||||
|
||||
Part of this is motivated by the fact that entering an excluded
|
||||
entry is the excpected way for a user to stop the timer on an item
|
||||
without having an explicit next item.
|
||||
|
||||
If we choose to implement *D0026* this would free up the UI a bit.
|
||||
In that case, we could gently flash the 'Show/Hide Excluded Items'
|
||||
button (*D0027*).
|
||||
|
||||
----
|
||||
|
||||
========= ==========
|
||||
Created: 2011-06-17
|
||||
Resolved: YYYY-MM-DD
|
||||
========= ==========
|
16
doc/issues/desktop/0026fn4.rst
Normal file
@ -0,0 +1,16 @@
|
||||
Add a default 'stop' behaviour.
|
||||
===============================
|
||||
|
||||
It might be more natural to allow the user to hit a button on
|
||||
the current entry to stop the timer on it than to explicitly
|
||||
create a new excluded entry.
|
||||
|
||||
Maybe have a default exclusion for all users, "ENTRY STOP" for example,
|
||||
and have the button automatically add this entry.
|
||||
|
||||
----
|
||||
|
||||
========= ==========
|
||||
Created: 2011-06-17
|
||||
Resolved: YYYY-MM-DD
|
||||
========= ==========
|
12
doc/issues/desktop/0027fn4.rst
Normal file
@ -0,0 +1,12 @@
|
||||
Create a way to unhide excluded entries.
|
||||
========================================
|
||||
|
||||
A button somewhere would be a good way to allow users to
|
||||
show/hide excluded entries.
|
||||
|
||||
----
|
||||
|
||||
========= ==========
|
||||
Created: 2011-06-17
|
||||
Resolved: YYYY-MM-DD
|
||||
========= ==========
|
11
doc/issues/desktop/0028tn5.rst
Normal file
@ -0,0 +1,11 @@
|
||||
Put more visual emphasis on the new entry text field.
|
||||
=====================================================
|
||||
|
||||
Add a shadow, or light highlight, something.
|
||||
|
||||
----
|
||||
|
||||
========= ==========
|
||||
Created: 2011-05-15
|
||||
Resolved: YYYY-MM-DD
|
||||
========= ==========
|
1
doc/issues/mobile/0000tn5.rst
Normal file
@ -0,0 +1 @@
|
||||
Prototype out mobile workflow.
|
34
doc/model.txt
Normal file
@ -0,0 +1,34 @@
|
||||
Data
|
||||
----
|
||||
UserModel
|
||||
TimelineModel
|
||||
TimelineListModel
|
||||
EntryListModel
|
||||
EntryModel
|
||||
|
||||
|
||||
Views
|
||||
-----
|
||||
EntryView
|
||||
NewEntryInput
|
||||
TimelineListView
|
||||
TimelineView
|
||||
UserView
|
||||
|
||||
|
||||
Data Dependencies
|
||||
-----------------
|
||||
UserModel: none
|
||||
TimelineModel: none
|
||||
TimelineListModel: UserModel
|
||||
EntryModel: none
|
||||
EntryListModel: TimelineModel
|
||||
|
||||
|
||||
View Dependencies
|
||||
-----------------
|
||||
UserView: UserModel
|
||||
TimelineView: TimelineModel, UserView
|
||||
TimelineListView: TimelineListModel, UserView
|
||||
EntryView: EntryModel, EntryListView
|
||||
EntryListView: EntryListModel, TimelineView
|
BIN
doc/model.xcf
Normal file
5
doc/sampl-urls.txt
Normal file
@ -0,0 +1,5 @@
|
||||
/ts_api/users/jdbernard
|
||||
/ts_api/timelines/jdbernard/work
|
||||
/ts_api/entries/jdbernard/work/1
|
||||
|
||||
/ts_api/app/user_summary/jdbernard
|
29
doc/todo.rst
Normal file
@ -0,0 +1,29 @@
|
||||
Current
|
||||
=======
|
||||
|
||||
Upcoming
|
||||
========
|
||||
- Generate day seperators
|
||||
- Fix UI for tasks with a duration a day or longer
|
||||
- Fix hover UI for user menu
|
||||
- Fix hover UI for timeline menu
|
||||
- Test (implement?) timeline selection
|
||||
- Add UI for timeline creation
|
||||
- Change the UI for editing the start-time. Use a date-picker (custom jQuery
|
||||
UI theme?)
|
||||
- Fix UI for timestamp edits which change the order of events in the timeline.
|
||||
- Create a light, alternating background for entries
|
||||
- Add hover-enabled icons for editing entries/showing notes
|
||||
- Create tooltips.
|
||||
- Create a realtime tick-tock for the duration of the current item.
|
||||
- Mobile version of the app.
|
||||
- Refactor code, seperate out reusable bits for mobile version.
|
||||
- Automatic code-highlighting (Highlight.js)
|
||||
|
||||
Done
|
||||
====
|
||||
- Add UI for note-taking
|
||||
- Add Markdown converter for notes.
|
||||
- Fix the duration bug when adding new events. Need to set the nextModel for
|
||||
the previously 'current' timestamp and set the nextModel of the new timestamp
|
||||
to 'null'
|
7
lib/uuid.app
Normal file
@ -0,0 +1,7 @@
|
||||
{application, uuid,
|
||||
[{description, "Erlang UUID"},
|
||||
{vsn, "0.4.4"},
|
||||
{modules, [uuid]},
|
||||
{registered, []},
|
||||
{applications, [stdlib, crypto]},
|
||||
{env, []}]}.
|
BIN
lib/uuid.beam
Normal file
5
login.post
Normal file
@ -0,0 +1,5 @@
|
||||
POST /ts_api/login HTTP/1.0
|
||||
Content-Type: application/json
|
||||
Content-Length: 45
|
||||
|
||||
{"username":"jdbernard", "password":"Y0uthc"}
|
23
src/id_counter.erl
Normal file
@ -0,0 +1,23 @@
|
||||
-module(id_counter).
|
||||
-export([create_table/1, next_counter/1, dirty_next_counter/1]).
|
||||
|
||||
-record(id_counter, {name, next_value = 0}).
|
||||
|
||||
%% Create the table structure for Mnesia
|
||||
create_table(Opts) ->
|
||||
mnesia:create_table(id_counter, Opts ++
|
||||
[{attributes, record_info(fields, id_counter)}]).
|
||||
|
||||
%% Get the next id for a given name
|
||||
next_counter(Name) ->
|
||||
Rec = case mnesia:read({id_counter, Name}) of
|
||||
[] -> #id_counter{name=Name, next_value=0};
|
||||
[Val] -> Val
|
||||
end,
|
||||
NextRec = Rec#id_counter{next_value = Rec#id_counter.next_value+ 1},
|
||||
ok = mnesia:write(NextRec),
|
||||
Rec#id_counter.next_value.
|
||||
|
||||
%% Get the next id for a given name
|
||||
dirty_next_counter(Name) ->
|
||||
mnesia:dirty_update_counter(id_counter, Name, 1) - 1.
|
17
src/timestamper.erl
Normal file
@ -0,0 +1,17 @@
|
||||
-module(timestamper).
|
||||
-export([start/0, create_tables/1]).
|
||||
|
||||
start() ->
|
||||
ok = application:load(mnesia),
|
||||
ok = application:set_env(mnesia, dir, "/usr/lib/yaws/mnesia"),
|
||||
ok = mnesia:start(),
|
||||
ok.
|
||||
|
||||
create_tables(Nodes) ->
|
||||
TableOpts = [{disc_copies, Nodes}],
|
||||
{atomic, ok} = id_counter:create_table(TableOpts),
|
||||
{atomic, ok} = ts_user:create_table(TableOpts),
|
||||
{atomic, ok} = ts_timeline:create_table(TableOpts),
|
||||
{atomic, ok} = ts_entry:create_table(TableOpts),
|
||||
{atomic, ok} = ts_ext_data:create_table(TableOpts),
|
||||
ok.
|
18
src/timestamper_dev.erl
Normal file
@ -0,0 +1,18 @@
|
||||
-module(timestamper_dev).
|
||||
-export([start/0, create_tables/1]).
|
||||
|
||||
start() ->
|
||||
ok = application:load(mnesia),
|
||||
ok = application:set_env(mnesia, dir, "/home/jdbernard/projects/jdb-labs/timestamper/web-app/db/test"),
|
||||
ok = mnesia:start(),
|
||||
error_logger:info_report("TimeStamper app started."),
|
||||
ok.
|
||||
|
||||
create_tables(Nodes) ->
|
||||
TableOpts = [{disc_copies, Nodes}],
|
||||
{atomic, ok} = id_counter:create_table(TableOpts),
|
||||
{atomic, ok} = ts_user:create_table(TableOpts),
|
||||
{atomic, ok} = ts_timeline:create_table(TableOpts),
|
||||
{atomic, ok} = ts_entry:create_table(TableOpts),
|
||||
{atomic, ok} = ts_ext_data:create_table(TableOpts),
|
||||
ok.
|
608
src/ts_api.erl
Normal file
@ -0,0 +1,608 @@
|
||||
-module(ts_api).
|
||||
-compile(export_all).
|
||||
|
||||
-include("ts_db_records.hrl").
|
||||
-include("yaws_api.hrl").
|
||||
|
||||
out(YArg) ->
|
||||
% retreive the session data
|
||||
Session = ts_api_session:get_session(YArg),
|
||||
|
||||
%get the app mod data
|
||||
PathString = YArg#arg.appmoddata,
|
||||
|
||||
% split the path
|
||||
PathElements = case PathString of
|
||||
undefined -> []; %handle no end slash: /ts_api
|
||||
_Any -> string:tokens(PathString, "/")
|
||||
end,
|
||||
|
||||
% process the request
|
||||
case catch dispatch_request(YArg, Session, PathElements) of
|
||||
{'EXIT', Err} ->
|
||||
% TODO: log error internally
|
||||
error_logger:error_report("TimeStamper: ~p", [Err]),
|
||||
io:format("Error: ~n~p", [Err]),
|
||||
make_json_500(YArg, Err);
|
||||
Other -> Other
|
||||
end.
|
||||
|
||||
% ================================== %
|
||||
% ======== DISPATCH METHODS ======== %
|
||||
% ================================== %
|
||||
|
||||
%% Entry point to the TimeStamper API dispatch system
|
||||
dispatch_request(YArg, _Session, []) -> make_json_404(YArg, [{"see_docs", "/ts_api_doc"}]);
|
||||
|
||||
dispatch_request(YArg, Session, [H|T]) ->
|
||||
|
||||
case {Session, H} of
|
||||
{_, "login"} -> do_login(YArg);
|
||||
{_, "logout"} -> do_logout(YArg);
|
||||
|
||||
{not_logged_in, _} -> make_json_401(YArg);
|
||||
{session_expired, _} -> make_json_401(YArg, [{"error", "session expired"}]);
|
||||
|
||||
{_S, "app"} -> dispatch_app(YArg, Session, T);
|
||||
{_S, "users"} -> dispatch_user(YArg, Session, T);
|
||||
{_S, "timelines"} -> dispatch_timeline(YArg, Session, T);
|
||||
{_S, "entries"} -> dispatch_entry(YArg, Session, T);
|
||||
{_S, _Other} -> make_json_404(YArg, [{"see_docs", "/ts_api_doc/"}])
|
||||
end.
|
||||
|
||||
% -------- Dispatch for /app -------- %
|
||||
|
||||
dispatch_app(YArg, Session, Params) ->
|
||||
HTTPMethod = (YArg#arg.req)#http_request.method,
|
||||
|
||||
case {HTTPMethod, Params} of
|
||||
|
||||
{'OPTIONS', ["user_summary", _]} -> make_CORS_options(YArg, "GET");
|
||||
|
||||
{'GET', ["user_summary", UsernameStr]} ->
|
||||
case {Session#ts_api_session.username,
|
||||
UsernameStr} of
|
||||
|
||||
{Username, Username} -> get_user_summary(YArg, Username);
|
||||
_ -> make_json_401(YArg)
|
||||
end;
|
||||
|
||||
{_BadMethod, ["user_summary", _UsernameStr]} ->
|
||||
make_json_405(YArg, [{"see_docs", "/ts_api_docs/app.html"}]);
|
||||
|
||||
_Other -> make_json_404(YArg, [{"see_docs", "/ts_api_docs/app.html"}])
|
||||
|
||||
end.
|
||||
|
||||
% -------- Dispatch for /user -------- %
|
||||
|
||||
dispatch_user(YArg, Session, []) ->
|
||||
dispatch_user(YArg, Session, [Session#ts_api_session.username]);
|
||||
|
||||
dispatch_user(YArg, Session, [Username]) ->
|
||||
HTTPMethod = (YArg#arg.req)#http_request.method,
|
||||
|
||||
% compare to the logged-in user
|
||||
case {HTTPMethod, Session#ts_api_session.username} of
|
||||
|
||||
{'OPTIONS', Username} -> make_CORS_options(YArg, "GET, PUT");
|
||||
{'GET', Username} -> get_user(YArg, Username);
|
||||
{'PUT', Username} -> put_user(YArg, Username);
|
||||
|
||||
{_BadMethod, Username} ->
|
||||
make_json_405(YArg, [{"see_docs", "/ts_api_doc/users.html"}]);
|
||||
|
||||
_Other -> make_json_401(YArg, [{"see_docs", "/ts_api_doc/users.html"}])
|
||||
end.
|
||||
|
||||
% -------- Dispatch for /timeline -------- %
|
||||
|
||||
dispatch_timeline(YArg, _Session, []) ->
|
||||
make_json_404(YArg, [{"see_docs", "/ts_api_doc/timelines.html"}]);
|
||||
|
||||
dispatch_timeline(YArg, Session, [Username|_T] = PathElements) ->
|
||||
|
||||
case Session#ts_api_session.username of
|
||||
Username -> dispatch_timeline(YArg, PathElements);
|
||||
_Other -> make_json_404(YArg, [{"see_docs", "/ts_api_doc/users.html"}])
|
||||
end.
|
||||
|
||||
% just username, list timelines
|
||||
dispatch_timeline(YArg, [Username]) ->
|
||||
HTTPMethod = (YArg#arg.req)#http_request.method,
|
||||
|
||||
case HTTPMethod of
|
||||
'OPTIONS' -> make_CORS_options(YArg, "GET");
|
||||
'GET' -> list_timelines(YArg, Username);
|
||||
_Other -> make_json_405(YArg, [{"see_docs", "/ts_api_doc/timelines.html"}])
|
||||
end;
|
||||
|
||||
dispatch_timeline(YArg, [Username, TimelineId]) ->
|
||||
HTTPMethod = (YArg#arg.req)#http_request.method,
|
||||
|
||||
case HTTPMethod of
|
||||
'OPTIONS'-> make_CORS_options(YArg, "GET, PUT, DELETE");
|
||||
'GET' -> get_timeline(YArg, Username, TimelineId);
|
||||
'PUT' -> put_timeline(YArg, Username, TimelineId);
|
||||
'DELETE' -> delete_timeline(YArg, Username, TimelineId);
|
||||
_Other -> make_json_405(YArg, [{"see_docs", "/ts_api_doc/timelines.html"}])
|
||||
end;
|
||||
|
||||
dispatch_timeline(YArg, _Other) ->
|
||||
make_json_404(YArg, [{"see_docs", "/ts_api_doc/timelines.html"}]).
|
||||
|
||||
% -------- Dispatch for /entry -------- %
|
||||
|
||||
dispatch_entry(YArg, _Session, []) ->
|
||||
make_json_404(YArg, [{"see_docs", "/ts_aip_doc/entries.html"}]);
|
||||
|
||||
dispatch_entry(YArg, Session, [Username|_T] = PathElements) ->
|
||||
|
||||
case Session#ts_api_session.username of
|
||||
Username -> dispatch_entry(YArg, PathElements);
|
||||
_Other -> make_json_404(YArg, [{"see_docs", "/ts_api_doc/entries.html"}])
|
||||
end.
|
||||
|
||||
dispatch_entry(YArg, [Username, TimelineId]) ->
|
||||
HTTPMethod = (YArg#arg.req)#http_request.method,
|
||||
|
||||
case HTTPMethod of
|
||||
'OPTIONS' -> make_CORS_options(YArg, "GET, POST");
|
||||
'GET' -> list_entries(YArg, Username, TimelineId);
|
||||
'POST' -> post_entry(YArg, Username, TimelineId);
|
||||
_Other -> make_json_405(YArg, [{"see_docs", "/ts_api_doc/entries.html"}])
|
||||
end;
|
||||
|
||||
dispatch_entry(YArg, [Username, TimelineId, UrlEntryId]) ->
|
||||
EntryId = list_to_integer(UrlEntryId), % TODO: catch non-numbers
|
||||
HTTPMethod = (YArg#arg.req)#http_request.method,
|
||||
|
||||
case HTTPMethod of
|
||||
'OPTIONS'-> make_CORS_options(YArg, "GET, PUT, DELETE");
|
||||
'GET' -> get_entry(YArg, Username, TimelineId, EntryId);
|
||||
'PUT' -> put_entry(YArg, Username, TimelineId, EntryId);
|
||||
'DELETE' -> delete_entry(YArg, Username, TimelineId, EntryId);
|
||||
_Other -> make_json_405(YArg, [{"see_docs", "/ts_api_doc/entries.html"}])
|
||||
end;
|
||||
|
||||
dispatch_entry(YArg, _Other) ->
|
||||
make_json_404(YArg, [{"see_docs", "/ts_api_doc/entries.html"}]).
|
||||
|
||||
% ============================== %
|
||||
% ======== IMPLEMENTATION ====== %
|
||||
% ============================== %
|
||||
|
||||
do_login(YArg) ->
|
||||
EJSON = parse_json_body(YArg),
|
||||
|
||||
{struct, Fields} = EJSON,
|
||||
|
||||
case {lists:keyfind(username, 1, Fields),
|
||||
lists:keyfind(password, 1, Fields)} of
|
||||
|
||||
% username and password found
|
||||
{{username, Username}, {password, Password}} ->
|
||||
|
||||
% check the uname, password
|
||||
case ts_user:check_credentials(Username, Password) of
|
||||
% they are good
|
||||
true ->
|
||||
{CookieVal, _Session} = ts_api_session:new(Username),
|
||||
|
||||
[{header, {set_cookie, io_lib:format(
|
||||
"ts_api_session=~s; Path=/",
|
||||
[CookieVal])}},
|
||||
{header, ["Access-Control-Allow-Origin: ", get_origin_header(YArg)]},
|
||||
{header, ["Access-Control-Allow-Credentials: ", "true"]},
|
||||
{content, "application/json",
|
||||
json:encode({struct, [{"status", "ok"}]})}];
|
||||
|
||||
% they are not good
|
||||
false -> make_json_401(YArg, [{"error",
|
||||
"bad username/password combination"}])
|
||||
end;
|
||||
|
||||
_Other -> make_json_400(YArg, [{"see_docs", "/ts_api_doc/login.html"}])
|
||||
end.
|
||||
|
||||
do_logout(YArg) ->
|
||||
Cookie = (YArg#arg.headers)#headers.cookie,
|
||||
CookieVal = yaws_api:find_cookie_val("ts_api_session", Cookie),
|
||||
ts_api_session:logout(CookieVal),
|
||||
[{status, 200},
|
||||
{header, ["Access-Control-Allow-Origin: ", get_origin_header(YArg)]},
|
||||
{header, ["Access-Control-Allow-Credentials: ", "true"]}].
|
||||
|
||||
get_user_summary(YArg, Username) ->
|
||||
% find user record
|
||||
case ts_user:lookup(Username) of
|
||||
|
||||
% no user record, barf
|
||||
no_record -> make_json_404(YArg);
|
||||
|
||||
% found user record, let us build the return
|
||||
User ->
|
||||
|
||||
% get user extended data properties
|
||||
UserExtData = ts_ext_data:get_properties(User),
|
||||
% convert to intermediate JSON form
|
||||
EJSONUser = ts_json:record_to_ejson(User, UserExtData),
|
||||
|
||||
% get the user's timelins
|
||||
Timelines = ts_timeline:list(Username, 0, 100),
|
||||
|
||||
% get each timeline's extended data and convert to EJSON
|
||||
EJSONTimelines = {array,
|
||||
lists:map(
|
||||
fun(Timeline) ->
|
||||
ts_json:record_to_ejson(Timeline,
|
||||
ts_ext_data:get_properties(Timeline))
|
||||
end,
|
||||
Timelines)},
|
||||
|
||||
% write response out
|
||||
make_json_200(YArg, {struct,
|
||||
[{user, EJSONUser},
|
||||
{timelines, EJSONTimelines}
|
||||
]})
|
||||
end.
|
||||
|
||||
get_user(YArg, Username) ->
|
||||
% find the user record
|
||||
case ts_user:lookup(Username) of
|
||||
% no such user, barf
|
||||
no_record -> make_json_404(YArg);
|
||||
% found, return a 200 with the record
|
||||
User -> make_json_200_record(YArg, User)
|
||||
end.
|
||||
|
||||
put_user(YArg, Username) ->
|
||||
|
||||
% parse the POST data
|
||||
EJSON = parse_json_body(YArg),
|
||||
|
||||
{UR, ExtData} =
|
||||
try ts_json:ejson_to_record_strict(#ts_user{username=Username}, EJSON)
|
||||
catch throw:{InputError, StackTrace} ->
|
||||
error_logger:error_report("Bad input in put_user/2: ~p",
|
||||
[InputError]),
|
||||
throw(make_json_400(YArg, [{request_error, InputError}]))
|
||||
end,
|
||||
|
||||
% update the record (we do not support creating users via the API right now)
|
||||
{ok, UpdatedRec} = ts_user:update(UR, ExtData),
|
||||
|
||||
% return a 200
|
||||
make_json_200_record(YArg, UpdatedRec).
|
||||
|
||||
list_timelines(YArg, Username) ->
|
||||
% pull out the POST data
|
||||
QueryData = yaws_api:parse_query(YArg),
|
||||
|
||||
% read or default the Start
|
||||
Start = case lists:keyfind("start", 1, QueryData) of
|
||||
{"start", StartVal} -> list_to_integer(StartVal);
|
||||
false -> 0
|
||||
end,
|
||||
|
||||
% read or default the Length
|
||||
Length = case lists:keyfind(length, 1, QueryData) of
|
||||
{"length", LengthVal} ->
|
||||
erlang:min(list_to_integer(LengthVal), 50);
|
||||
false -> 50
|
||||
end,
|
||||
|
||||
% list the timelines from the database
|
||||
Timelines = ts_timeline:list(Username, Start, Length),
|
||||
|
||||
% convert them all to their EJSON form, adding in extended data for each
|
||||
EJSONTimelines = {array, lists:map(
|
||||
fun (Timeline) ->
|
||||
ts_json:record_to_ejson(Timeline,
|
||||
ts_ext_data:get_properties(Timeline))
|
||||
end,
|
||||
Timelines)},
|
||||
|
||||
% return response
|
||||
make_json_200(YArg, EJSONTimelines).
|
||||
|
||||
get_timeline(YArg, Username, TimelineId) ->
|
||||
% look for timeline
|
||||
case ts_timeline:lookup(Username, TimelineId) of
|
||||
% no such timeline, return 404
|
||||
no_record -> make_json_404(YArg, [{"error", "no such timeline"}]);
|
||||
% return the timeline data
|
||||
Timeline -> make_json_200_record(YArg, Timeline)
|
||||
end.
|
||||
|
||||
put_timeline(YArg, Username, TimelineId) ->
|
||||
% parse the POST data
|
||||
EJSON = parse_json_body(YArg),
|
||||
%{struct, Fields} = EJSON,
|
||||
|
||||
% parse into a timeline record
|
||||
{TR, ExtData} =
|
||||
try ts_json:ejson_to_record_strict(
|
||||
#ts_timeline{ref={Username, TimelineId}}, EJSON)
|
||||
% we can not parse it, tell the user
|
||||
catch throw:{InputError, _StackTrace} ->
|
||||
error_logger:error_report("Bad input: ~p", [InputError]),
|
||||
throw(make_json_400(YArg, [{request_error, InputError}]))
|
||||
end,
|
||||
|
||||
% write the changes.
|
||||
ts_timeline:write(TR, ExtData),
|
||||
|
||||
% return a 200
|
||||
make_json_200_record(YArg, TR).
|
||||
|
||||
delete_timeline(_YArg, _Username, _TimelineId) -> {status, 405}.
|
||||
|
||||
list_entries(YArg, Username, TimelineId) ->
|
||||
% pull out the POST data
|
||||
QueryData = yaws_api:parse_query(YArg),
|
||||
|
||||
% first determine if we are listing by date
|
||||
case {ts_timeline:lookup(Username, TimelineId),
|
||||
lists:keyfind("byDate", 1, QueryData)} of
|
||||
|
||||
{no_record, _ByDateField} -> make_json_404(
|
||||
[{"error", "no such timeline"},
|
||||
{"see_docs", "/ts_api_doc/entries.html#LIST"}]);
|
||||
|
||||
% listing by date range
|
||||
{Timeline, {"byDate", "true"}} ->
|
||||
|
||||
% look for the start date; default to the beginning of the timeline
|
||||
StartDate = case lists:keyfind("startDate", 1, QueryData) of
|
||||
% TODO: error handling if the date is badly formatted
|
||||
{"startDate", StartDateVal} -> ts_json:decode_datetime(StartDateVal);
|
||||
false -> Timeline#ts_timeline.created
|
||||
end,
|
||||
|
||||
% look for end date; default to right now
|
||||
EndDate = case lists:keyfind("endDate", 1, QueryData) of
|
||||
% TODO: error handling if the date is badly formatted
|
||||
{"endDate", EndDateVal} -> ts_json:decode_datetime(EndDateVal);
|
||||
false -> calendar:now_to_universal_time(erlang:now())
|
||||
end,
|
||||
|
||||
% read sort order and list entries
|
||||
Entries = case lists:keyfind("order", 1, QueryData) of
|
||||
% descending sort order
|
||||
{"order", "desc"} -> ts_entry:list_desc(
|
||||
{Username, TimelineId}, StartDate, EndDate);
|
||||
|
||||
% ascending order--{other, asc}--and default
|
||||
_Other -> ts_entry:list_asc(
|
||||
{Username, TimelineId}, StartDate, EndDate)
|
||||
end,
|
||||
|
||||
EJSONEntries = {array, lists:map(
|
||||
fun (Entry) ->
|
||||
ts_json:record_to_ejson(Entry,
|
||||
ts_ext_data:get_properties(Entry))
|
||||
end,
|
||||
Entries)},
|
||||
|
||||
make_json_200(YArg, EJSONEntries);
|
||||
|
||||
% listing by table position
|
||||
_Other ->
|
||||
|
||||
% read or default the Start
|
||||
Start = case lists:keyfind("start", 1, QueryData) of
|
||||
{"start", StartVal} -> list_to_integer(StartVal);
|
||||
false -> 0
|
||||
end,
|
||||
|
||||
% read or default the Length
|
||||
Length = case lists:keyfind("length", 1, QueryData) of
|
||||
{"length", LengthVal} ->
|
||||
erlang:min(list_to_integer(LengthVal), 500);
|
||||
false -> 50
|
||||
end,
|
||||
|
||||
% read sort order and list entries
|
||||
Entries = case lists:keyfind("order", 1, QueryData) of
|
||||
{"order", "desc"} -> ts_entry:list_desc(
|
||||
{Username, TimelineId}, Start, Length);
|
||||
|
||||
_UnknownOrder -> ts_entry:list_asc(
|
||||
{Username, TimelineId}, Start, Length)
|
||||
end,
|
||||
|
||||
EJSONEntries = {array, lists:map(
|
||||
fun (Entry) ->
|
||||
ts_json:record_to_ejson(Entry,
|
||||
ts_ext_data:get_properties(Entry))
|
||||
end,
|
||||
Entries)},
|
||||
|
||||
make_json_200(YArg, EJSONEntries)
|
||||
end.
|
||||
|
||||
get_entry(YArg, Username, TimelineId, EntryId) ->
|
||||
case ts_entry:lookup(Username, TimelineId, EntryId) of
|
||||
% no such entry
|
||||
no_record -> make_json_404(YArg, [{"error", "no such entry"}]);
|
||||
% return the entry data
|
||||
Entry -> make_json_200_record(YArg, Entry)
|
||||
end.
|
||||
|
||||
post_entry(YArg, Username, TimelineId) ->
|
||||
|
||||
% parse the request body
|
||||
EJSON = parse_json_body(YArg),
|
||||
|
||||
% parse into ts_entry record
|
||||
{ER, ExtData} = try ts_json:ejson_to_record_strict(
|
||||
#ts_entry{ref = {Username, TimelineId, undefined}}, EJSON)
|
||||
catch _:InputError ->
|
||||
error_logger:error_report("Bad input: ~p", [InputError]),
|
||||
throw(make_json_400(YArg, [{request_error, InputError}]))
|
||||
end,
|
||||
|
||||
case ts_entry:new(ER, ExtData) of
|
||||
% record created
|
||||
{ok, CreatedRecord} ->
|
||||
|
||||
[{status, 201}, make_json_200_record(YArg, CreatedRecord)];
|
||||
|
||||
OtherError ->
|
||||
error_logger:error_report("Could not create entry: ~p", [OtherError]),
|
||||
make_json_500(YArg, OtherError)
|
||||
end.
|
||||
|
||||
put_entry(YArg, Username, TimelineId, EntryId) ->
|
||||
|
||||
% parse the POST data
|
||||
EJSON = parse_json_body(YArg),
|
||||
|
||||
% parse into ts_entry record
|
||||
{ER, ExtData} = try ts_json:ejson_to_record_strict(
|
||||
#ts_entry{ref={Username, TimelineId, EntryId}}, EJSON)
|
||||
catch _:InputError ->
|
||||
error_logger:error_report("Bad input: ~p", [InputError]),
|
||||
throw(make_json_400(YArg))
|
||||
end,
|
||||
|
||||
ts_entry:write(ER, ExtData),
|
||||
make_json_200_record(YArg, ER).
|
||||
|
||||
delete_entry(YArg, Username, TimelineId, EntryId) ->
|
||||
|
||||
% find the record to delete
|
||||
case ts_entry:lookup(Username, TimelineId, EntryId) of
|
||||
|
||||
no_record -> make_json_404(YArg);
|
||||
|
||||
Record ->
|
||||
% try to delete
|
||||
case ts_entry:delete(Record) of
|
||||
ok -> {status, 200};
|
||||
Error ->
|
||||
error_logger:error_report("Error occurred deleting entry record: ~p", [Error]),
|
||||
make_json_500(YArg, Error)
|
||||
end
|
||||
end.
|
||||
|
||||
% ============================== %
|
||||
% ======== UTIL METHODS ======== %
|
||||
% ============================== %
|
||||
|
||||
parse_json_body(YArg) ->
|
||||
case catch json:decode([], binary_to_list(YArg#arg.clidata)) of
|
||||
{done, {ok, EJSON}, _} -> EJSON;
|
||||
Error ->
|
||||
% TODO: log error internally
|
||||
error_logger:error_report("Error parsing JSON request body: ~p", [Error]),
|
||||
throw(make_json_400(YArg))
|
||||
end.
|
||||
|
||||
get_origin_header(YArg) ->
|
||||
Headers = (YArg#arg.headers)#headers.other,
|
||||
case lists:keyfind("Origin", 3, Headers) of
|
||||
false -> "*";
|
||||
{http_header, 0, "Origin", _, Origin} -> Origin;
|
||||
_ -> make_json_500(YArg, "Unrecognized Origin header.")
|
||||
end.
|
||||
|
||||
make_CORS_options(YArg, AllowedMethods) ->
|
||||
[{status, 200},
|
||||
{header, ["Access-Control-Allow-Origin: ", get_origin_header(YArg)]},
|
||||
{header, ["Access-Control-Allow-Methods: ", AllowedMethods]},
|
||||
{header, ["Access-Control-Allow-Credentials: ", "true"]}].
|
||||
|
||||
make_CORS_options(_YArg, AllowedOrigins, AllowedMethods) ->
|
||||
[{status, 200},
|
||||
{header, ["Access-Control-Allow-Origin: ", AllowedOrigins]},
|
||||
{header, ["Access-Control-Allow-Methods: ", AllowedMethods]},
|
||||
{header, ["Access-Control-Allow-Credentials: ", "true"]}].
|
||||
|
||||
%% Create a JSON 200 response.
|
||||
make_json_200(YArg, EJSONResponse) ->
|
||||
JSONResponse = json:encode(EJSONResponse),
|
||||
[{content, "application/json", JSONResponse},
|
||||
{header, ["Access-Control-Allow-Origin: ", get_origin_header(YArg)]},
|
||||
{header, ["Access-Control-Allow-Credentials: ", "true"]}].
|
||||
|
||||
|
||||
make_json_200_record(YArg, Record) ->
|
||||
RecordExtData = ts_ext_data:get_properties(Record),
|
||||
EJSON = ts_json:record_to_ejson(Record, RecordExtData),
|
||||
JSONResponse = json:encode(EJSON),
|
||||
[{content, "application/json", JSONResponse},
|
||||
{header, ["Access-Control-Allow-Origin: ", get_origin_header(YArg)]},
|
||||
{header, ["Access-Control-Allow-Credentials: ", "true"]}].
|
||||
|
||||
make_json_400(YArg) -> make_json_400(YArg, []).
|
||||
make_json_400(YArg, Fields) ->
|
||||
F1 = case lists:keyfind(status, 1, Fields) of
|
||||
false -> Fields ++ [{"status", "bad request"}];
|
||||
_Else -> Fields
|
||||
end,
|
||||
|
||||
[{status, 400}, {content, "application/json", json:encode({struct, F1})},
|
||||
{header, ["Access-Control-Allow-Origin: ", get_origin_header(YArg)]},
|
||||
{header, ["Access-Control-Allow-Credentials: ", "true"]}].
|
||||
|
||||
make_json_401(YArg) -> make_json_401(YArg, []).
|
||||
make_json_401(YArg, Fields) ->
|
||||
% add default status if not provided
|
||||
F1 = case lists:keyfind(status, 1, Fields) of
|
||||
false -> Fields ++ [{"status", "unauthorized"}];
|
||||
_Else -> Fields
|
||||
end,
|
||||
|
||||
[{status, 401},
|
||||
{header, ["Access-Control-Allow-Origin: ", get_origin_header(YArg)]},
|
||||
{header, ["Access-Control-Allow-Credentials: ", "true"]},
|
||||
{content, "application/json", json:encode({struct, F1})}].
|
||||
|
||||
%% Create a JSON 404 response.
|
||||
make_json_404(YArg) -> make_json_404(YArg, []).
|
||||
make_json_404(YArg, Fields) ->
|
||||
% add default status if not provided
|
||||
F1 = case lists:keyfind(status, 1, Fields) of
|
||||
false -> Fields ++ [{"status", "not found"}];
|
||||
_Else -> Fields
|
||||
end,
|
||||
|
||||
% add the path they requested
|
||||
F2 = F1 ++ [{path, element(2, (YArg#arg.req)#http_request.path)}],
|
||||
|
||||
[{status, 404}, {content, "application/json", json:encode({struct, F2})},
|
||||
{header, ["Access-Control-Allow-Origin: ", get_origin_header(YArg)]},
|
||||
{header, ["Access-Control-Allow-Credentials: ", "true"]}].
|
||||
|
||||
make_json_405(YArg) -> make_json_405(YArg, []).
|
||||
make_json_405(YArg, Fields) ->
|
||||
% add default status if not provided
|
||||
F1 = case lists:keyfind(status, 1, Fields) of
|
||||
false -> Fields ++ [{"status", "method not allowed"}];
|
||||
_Else -> Fields
|
||||
end,
|
||||
|
||||
% add the path they requested
|
||||
% F2 = F1 ++ [{path, io_lib:format("~p", [(YArg#arg.req)#http_request.path])}],
|
||||
|
||||
[{status, 405}, {content, "application/json", json:encode({struct, F1})},
|
||||
{header, ["Access-Control-Allow-Origin: ", get_origin_header(YArg)]},
|
||||
{header, ["Access-Control-Allow-Credentials: ", "true"]}].
|
||||
|
||||
make_json_500(YArg, Error) ->
|
||||
io:format("Error: ~n~p", [Error]),
|
||||
EJSON = {struct, [
|
||||
{"status", "internal server error"},
|
||||
{"error", lists:flatten(io_lib:format("~p", [Error]))}]},
|
||||
[{status, 500}, {content, "application/json", json:encode(EJSON)},
|
||||
{header, ["Access-Control-Allow-Origin: ", get_origin_header(YArg)]},
|
||||
{header, ["Access-Control-Allow-Credentials: ", "true"]}].
|
||||
|
||||
make_json_500(YArg) ->
|
||||
EJSON = {struct, [
|
||||
{"status", "internal server error"}]},
|
||||
[{status, 500}, {content, "application/json", json:encode(EJSON)},
|
||||
{header, ["Access-Control-Allow-Origin: ", get_origin_header(YArg)]},
|
||||
{header, ["Access-Control-Allow-Credentials: ", "true"]}].
|
37
src/ts_api_session.erl
Normal file
@ -0,0 +1,37 @@
|
||||
-module(ts_api_session).
|
||||
-compile(export_all).
|
||||
|
||||
-include("ts_db_records.hrl").
|
||||
-include("yaws_api.hrl").
|
||||
|
||||
% 6 hours: sec min hr
|
||||
-define(TTL, 60*60* 6).
|
||||
|
||||
new(Username) ->
|
||||
Session = #ts_api_session{ username = Username },
|
||||
|
||||
CookieVal = yaws_api:new_cookie_session(Session, ?TTL),
|
||||
{CookieVal, Session}.
|
||||
|
||||
logout(CookieVal) ->
|
||||
yaws_api:delete_cookie_session(CookieVal).
|
||||
|
||||
get_session(YArg) ->
|
||||
% get the cookie header
|
||||
Cookie = (YArg#arg.headers)#headers.cookie,
|
||||
|
||||
% look up the cookie in the session server
|
||||
case yaws_api:find_cookie_val("ts_api_session", Cookie) of
|
||||
|
||||
% no cookie, not logged in
|
||||
[] -> not_logged_in;
|
||||
|
||||
% found the cookie
|
||||
CookieVal ->
|
||||
|
||||
% get the session data
|
||||
case yaws_api:cookieval_to_opaque(CookieVal) of
|
||||
{error, _Err} -> session_expired;
|
||||
{ok, Session} -> Session
|
||||
end
|
||||
end.
|
126
src/ts_common.erl
Normal file
@ -0,0 +1,126 @@
|
||||
-module(ts_common).
|
||||
-export([new/1, new/2, update/1, update/2, update_record/2, do_set_ext_data/2,
|
||||
list/3, order_datetimes/2]).
|
||||
|
||||
-include_lib("stdlib/include/qlc.hrl").
|
||||
|
||||
new(Record) ->
|
||||
{atomic, Result} = mnesia:transaction(fun() -> do_new(Record) end),
|
||||
Result.
|
||||
|
||||
new(Record, ExtData) when is_list(ExtData) ->
|
||||
{atomic, Result} = mnesia:transaction(fun() ->
|
||||
case do_new(Record) of
|
||||
{error, Err} -> mnesia:abort({"Cannot create new record.", Err});
|
||||
NewRecord -> case do_set_ext_data(Record, ExtData) of
|
||||
ok -> NewRecord;
|
||||
Error -> mnesia:abort(Error)
|
||||
end
|
||||
end
|
||||
end),
|
||||
Result.
|
||||
|
||||
% required to be wrapped in a transaction
|
||||
do_new(Record) ->
|
||||
Table = element(1, Record),
|
||||
|
||||
% check for existing record
|
||||
case mnesia:read(Table, element(2, Record)) of
|
||||
% record exists
|
||||
[ExistingRecord] -> {error, {record_exists, ExistingRecord}};
|
||||
|
||||
[] -> mnesia:write(Record)
|
||||
end.
|
||||
|
||||
update(Record) ->
|
||||
{atomic, Result} = mnesia:transaction(fun() -> do_update(Record) end),
|
||||
Result.
|
||||
|
||||
update(Record, ExtData) when is_list(ExtData) ->
|
||||
{atomic, Result} = mnesia:transaction(fun() ->
|
||||
case do_update(Record) of
|
||||
{error, Err} -> mnesia:abort({"Cannot update record.", Err});
|
||||
UpdatedRecord -> case do_set_ext_data(Record, ExtData) of
|
||||
ok -> UpdatedRecord;
|
||||
Error -> mnesia:abort({"Cannot update record.", Error})
|
||||
end
|
||||
end
|
||||
end),
|
||||
Result.
|
||||
|
||||
do_update(Record) ->
|
||||
Table = element(1, Record),
|
||||
Key = element(2, Record),
|
||||
|
||||
% look for existing record
|
||||
case mnesia:read(Table, Key) of
|
||||
% record does not exist, cannot update
|
||||
[] -> no_record;
|
||||
% record does exist, update
|
||||
[ExistingRecord] ->
|
||||
mnesia:write(update_record(ExistingRecord, Record))
|
||||
end.
|
||||
|
||||
update_record(Record, UpdateData) ->
|
||||
update_record(tuple_to_list(Record), tuple_to_list(UpdateData), []).
|
||||
|
||||
update_record([], [], Updated) -> list_to_tuple(lists:reverse(Updated));
|
||||
update_record([Field|RecordFields], [FieldReplacement|UpdateData], Acc) ->
|
||||
UpdatedField = case FieldReplacement of
|
||||
undefined -> Field;
|
||||
NewValue -> NewValue
|
||||
end,
|
||||
|
||||
update_record(RecordFields, UpdateData, [UpdatedField|Acc]).
|
||||
|
||||
%% list <Length> number of records, skipping the first <Start>
|
||||
list(Table, Start, Length)
|
||||
when is_atom(Table) and is_integer(Start) and is_integer(Length) ->
|
||||
list(qlc:q([A || A <- mnesia:table(Table)]), Start, Length);
|
||||
|
||||
list(Query, Start, Length) ->
|
||||
{atomic, Result} = mnesia:transaction(fun() ->
|
||||
% create a cursor for the query
|
||||
C = qlc:cursor(Query),
|
||||
|
||||
% skip the first Start records
|
||||
if Start > 0 -> qlc:next_answers(C, Start);
|
||||
true -> ok
|
||||
end,
|
||||
|
||||
% return Length records
|
||||
List = qlc:next_answers(C, Length),
|
||||
|
||||
% free cursor
|
||||
ok = qlc:delete_cursor(C),
|
||||
|
||||
List
|
||||
end),
|
||||
Result.
|
||||
|
||||
% should be wrapped in a transaction
|
||||
do_set_ext_data(Record, ExtData) when is_list(ExtData) ->
|
||||
lists:foreach(
|
||||
fun({Key, Val}) ->
|
||||
ok = ts_ext_data:set_property(Record, Key, Val)
|
||||
end,
|
||||
ExtData),
|
||||
ok.
|
||||
|
||||
% This is somewhat ridiculous.
|
||||
order_datetimes({{Y1, Mon1, D1}, {H1, Min1, S1}},
|
||||
{{Y2, Mon2, D2}, {H2, Min2, S2}}) ->
|
||||
|
||||
% this is actually a deep-nested set of case statements, but it seems
|
||||
% cleaner to keep the indentation level and follow a strict pattern
|
||||
|
||||
% compare field.If /=, return that value, else drop through to next field
|
||||
case Y1 = Y2 of false -> Y1 > Y2; true ->
|
||||
case Mon1 = Mon2 of false -> Mon1 > Mon2; true ->
|
||||
case D1 = D2 of false -> D1 > D2; true ->
|
||||
case H1 = H2 of flase -> H1 > H2; true ->
|
||||
case Min1 = Min2 of false -> Min1 > Min2; true ->
|
||||
case S1 = S2 of false -> S1 > S2; true -> true
|
||||
|
||||
% sec min hr day mon year
|
||||
end end end end end end.
|
50
src/ts_db_records.hrl
Normal file
@ -0,0 +1,50 @@
|
||||
-record(ts_user, {
|
||||
username,
|
||||
pwd,
|
||||
pwd_salt,
|
||||
name,
|
||||
email,
|
||||
join_date
|
||||
}).
|
||||
|
||||
% ts_user.ext_data format is
|
||||
% [{key, val}, {key, val}]
|
||||
%
|
||||
% Valid key/value pairs are:
|
||||
%
|
||||
% * last_timeline/TimelineId
|
||||
%
|
||||
% list() representing a timeline owned by this user
|
||||
%
|
||||
% * entry_exclusions/EntryExclusions
|
||||
%
|
||||
% list() of regular expressions to be excluded from display, seperated by '|'
|
||||
|
||||
-record(ts_timeline, {
|
||||
ref, % {username, timelineid}
|
||||
created,% {{year, month, day}, {hour, minute, second}}
|
||||
desc
|
||||
}).
|
||||
|
||||
-record(ts_entry, {
|
||||
ref, % {username, timelineid, entryid}
|
||||
uuid, % UUID for this entry across all implementations and copies of
|
||||
% this timeline.
|
||||
timestamp, % gregorian seconds
|
||||
mark, % String description of entry
|
||||
notes % String with further notes about the entry
|
||||
}).
|
||||
|
||||
-record(ts_api_session, {
|
||||
username
|
||||
}).
|
||||
|
||||
% extensible data for arbitrary entities
|
||||
-record(ts_ext_data, {
|
||||
ref, % {ref, key}: reference with item ref and property key
|
||||
value % value
|
||||
}).
|
||||
%-record(ts_session, {
|
||||
%session_id,
|
||||
%expires,
|
||||
%username
|
116
src/ts_entry.erl
Normal file
@ -0,0 +1,116 @@
|
||||
-module(ts_entry).
|
||||
-export([create_table/1, new/1, new/2, update/1, update/2, write/1, write/2,
|
||||
delete/1, lookup/3, list_asc/3, list_desc/3]).
|
||||
|
||||
-include("ts_db_records.hrl").
|
||||
-include_lib("stdlib/include/qlc.hrl").
|
||||
|
||||
create_table(TableOpts) ->
|
||||
mnesia:create_table(ts_entry,
|
||||
TableOpts ++ [{attributes, record_info(fields, ts_entry)},
|
||||
{type, ordered_set}, {index, [timestamp]}]).
|
||||
|
||||
new(ER = #ts_entry{}) ->
|
||||
{atomic, NewRow} = mnesia:transaction(fun() -> do_new(ER) end),
|
||||
{ok, NewRow}.
|
||||
|
||||
new(ER = #ts_entry{}, ExtData) when is_list(ExtData) ->
|
||||
{atomic, NewRow} = mnesia:transaction(fun() ->
|
||||
NewRow = do_new(ER),
|
||||
ts_common:do_set_ext_data(ER, ExtData),
|
||||
NewRow end),
|
||||
{ok, NewRow}.
|
||||
|
||||
do_new(ER = #ts_entry{}) ->
|
||||
{Username, TimelineId, _} = ER#ts_entry.ref,
|
||||
NextId = id_counter:next_counter(ts_entry_id),
|
||||
RowWithRef = ER#ts_entry{ref = {Username, TimelineId, NextId}},
|
||||
NewRow = case RowWithRef#ts_entry.uuid of
|
||||
undefined -> RowWithRef#ts_entry{uuid = uuid:uuid4()};
|
||||
_ -> RowWithRef
|
||||
end,
|
||||
ok = mnesia:write(NewRow),
|
||||
NewRow.
|
||||
|
||||
update(ER = #ts_entry{}) -> ts_common:update(ER).
|
||||
|
||||
update(ER = #ts_entry{}, ExtData) when is_list(ExtData) ->
|
||||
ts_common:update(ER, ExtData).
|
||||
|
||||
write(ER = #ts_entry{}) -> mnesia:dirty_write(ER).
|
||||
|
||||
write(ER = #ts_entry{}, ExtData) ->
|
||||
{atomic, Result} = mnesia:transaction(fun() ->
|
||||
ok = mnesia:write(ER),
|
||||
ok = ts_common:do_set_ext_data(ER, ExtData)
|
||||
end),
|
||||
Result.
|
||||
|
||||
lookup(Username, TimelineId, EntryId) ->
|
||||
case mnesia:dirty_read(ts_entry, {Username, TimelineId, EntryId}) of
|
||||
[] -> no_record;
|
||||
[Entry] -> Entry
|
||||
end.
|
||||
|
||||
delete(ER = #ts_entry{}) -> mnesia:dirty_delete_object(ER).
|
||||
|
||||
list({Username, Timeline}, Start, Length, OrderFun)
|
||||
when is_integer(Start) and is_integer(Length) ->
|
||||
|
||||
{atomic, Entries} = mnesia:transaction(fun() ->
|
||||
% match the username and timeline
|
||||
MatchHead = #ts_entry{ref = {Username, Timeline, '_'}, _='_'},
|
||||
|
||||
% select all records that match
|
||||
mnesia:select(ts_entry, [{MatchHead, [], ['$_']}])
|
||||
end),
|
||||
|
||||
% sort
|
||||
SortedEntries = lists:sort(OrderFun, Entries),
|
||||
|
||||
% return only the range selected.
|
||||
% TODO: can we do this without selecting all entries?
|
||||
case length(SortedEntries) > Start of
|
||||
true -> lists:sublist(SortedEntries, Start + 1, Length);
|
||||
false -> []
|
||||
end;
|
||||
|
||||
list({Username, Timeline}, StartDateTime, EndDateTime, OrderFun) ->
|
||||
|
||||
% compute the seconds from datetimes
|
||||
StartSeconds = calendar:datetime_to_gregorian_seconds(StartDateTime),
|
||||
EndSeconds = calendar:datetime_to_gregorian_seconds(EndDateTime),
|
||||
|
||||
% select all entries from the timeline that are within the time range
|
||||
{atomic, Entries} = mnesia:transaction(fun() ->
|
||||
|
||||
% match the username and timeline id
|
||||
MatchHead = #ts_entry{ref = {Username, Timeline, '_'}, timestamp='$1', _='_'},
|
||||
|
||||
% guards for the time range
|
||||
StartGuard = {'>=', '$1', StartSeconds},
|
||||
EndGuard = {'<', '$1', EndSeconds},
|
||||
|
||||
mnesia:select(ts_entry, [{MatchHead, [StartGuard, EndGuard], ['$_']}])
|
||||
end),
|
||||
|
||||
% sort
|
||||
lists:sort(OrderFun, Entries).
|
||||
|
||||
list_asc(TimelineRef, Start, Length)
|
||||
when is_integer(Start) and is_integer(Length) ->
|
||||
list(TimelineRef, Start, Length, fun timestamp_asc/2);
|
||||
|
||||
list_asc(TimelineRef, StartDateTime, EndDateTime) ->
|
||||
list(TimelineRef, StartDateTime, EndDateTime, fun timestamp_asc/2).
|
||||
|
||||
list_desc(TimelineRef, Start, Length)
|
||||
when is_integer(Start) and is_integer(Length) ->
|
||||
list(TimelineRef, Start, Length, fun timestamp_desc/2);
|
||||
|
||||
list_desc(TimelineRef, StartDateTime, EndDateTime) ->
|
||||
list(TimelineRef, StartDateTime, EndDateTime, fun timestamp_desc/2).
|
||||
|
||||
timestamp_asc(E1, E2) -> E1#ts_entry.timestamp > E2#ts_entry.timestamp.
|
||||
|
||||
timestamp_desc(E1, E2) -> E1#ts_entry.timestamp < E2#ts_entry.timestamp.
|
50
src/ts_ext_data.erl
Normal file
@ -0,0 +1,50 @@
|
||||
-module(ts_ext_data).
|
||||
-export([create_table/1, set_property/3, get_property/2, get_properties/1]).
|
||||
|
||||
-include("ts_db_records.hrl").
|
||||
|
||||
create_table(TableOpts) ->
|
||||
mnesia:create_table(ts_ext_data,
|
||||
TableOpts ++ [{attributes, record_info(fields, ts_ext_data)},
|
||||
{type, ordered_set}]).
|
||||
|
||||
% set last timeline
|
||||
set_property(Rec=#ts_user{}, last_timeline, LastTimelineId) ->
|
||||
do_set_property(Rec#ts_user.username, last_timeline, LastTimelineId);
|
||||
|
||||
% Set exclusion_list for a User account
|
||||
set_property(Rec=#ts_user{}, entry_exclusions, ExclusionList) ->
|
||||
do_set_property(Rec#ts_user.username, entry_exclusions, ExclusionList);
|
||||
|
||||
% Set exclusion_list for a Timeline entry
|
||||
set_property(Rec=#ts_timeline{}, entry_exclusions, ExclusionList) ->
|
||||
do_set_property(Rec#ts_timeline.ref, entry_exclusions, ExclusionList);
|
||||
|
||||
set_property(Rec, Key, _Value) ->
|
||||
throw(lists:flatten(io_lib:format("Property '~s' not available for a ~s record.",
|
||||
[Key, element(1, Rec)]))).
|
||||
|
||||
get_property(Ref, PropKey) ->
|
||||
{atomic, Result} = mnesia:transaction(fun() ->
|
||||
case mnesia:read(ts_ext_data, {Ref, PropKey}) of
|
||||
[] -> not_set;
|
||||
[Property] -> Property#ts_ext_data.value
|
||||
end
|
||||
end),
|
||||
Result.
|
||||
|
||||
get_properties(Rec) ->
|
||||
Ref = element(2, Rec),
|
||||
{atomic, Result} = mnesia:transaction(fun() ->
|
||||
MatchHead = #ts_ext_data{ref = {Ref, '_'}, _='_'},
|
||||
mnesia:select(ts_ext_data, [{MatchHead, [], ['$_']}])
|
||||
end),
|
||||
lists:map(fun(ExtData = #ts_ext_data{}) ->
|
||||
{Ref, Key} = ExtData#ts_ext_data.ref,
|
||||
{Key, ExtData#ts_ext_data.value} end, Result).
|
||||
|
||||
do_set_property(Ref, PropKey, Val) ->
|
||||
{atomic, _Result} = mnesia:transaction(fun() ->
|
||||
mnesia:write(#ts_ext_data{ref = {Ref, PropKey}, value = Val})
|
||||
end),
|
||||
ok.
|
216
src/ts_json.erl
Normal file
@ -0,0 +1,216 @@
|
||||
-module(ts_json).
|
||||
-export([encode_record/2, record_to_ejson/2,
|
||||
ejson_to_record/2, ejson_to_record/3,
|
||||
ejson_to_record_strict/2, ejson_to_record_strict/3,
|
||||
decode_datetime/1]).
|
||||
|
||||
-include("ts_db_records.hrl").
|
||||
|
||||
encode_record(Record, ExtData) -> lists:flatten(json:encode(record_to_ejson(Record, ExtData))).
|
||||
|
||||
% User JSON record required structure:
|
||||
% {"id": "john_doe",
|
||||
% "name": "John Doe",
|
||||
% "email": "john.doe@example.com",
|
||||
% "join_date": "2011-01-01T12:00.000Z"}
|
||||
|
||||
record_to_ejson(Record=#ts_user{}, ExtData) ->
|
||||
{struct, [
|
||||
{id, Record#ts_user.username},
|
||||
{name, Record#ts_user.name},
|
||||
{email, Record#ts_user.email},
|
||||
{join_date, encode_datetime(Record#ts_user.join_date)}] ++
|
||||
lists:map(fun ext_data_to_ejson/1, ExtData)};
|
||||
|
||||
% Timeline JSON record stucture:
|
||||
% {"user_id": "john_doe",
|
||||
% "id": "personal",
|
||||
% "created": "2011-01-01T14:00.000Z",
|
||||
% "description:"Personal time-tracking."}
|
||||
|
||||
record_to_ejson(Record=#ts_timeline{}, ExtData) ->
|
||||
% pull out the username and timeline id
|
||||
{Username, TimelineId} = Record#ts_timeline.ref,
|
||||
|
||||
% create the EJSON struct
|
||||
{struct, [
|
||||
{user_id, Username},
|
||||
{id, TimelineId},
|
||||
{created, encode_datetime(Record#ts_timeline.created)},
|
||||
{description, Record#ts_timeline.desc}] ++
|
||||
lists:map(fun ext_data_to_ejson/1, ExtData)};
|
||||
|
||||
% Entry JSON record structure:
|
||||
% {"user_id": "john_doe",
|
||||
% "timeline_id": "personal",
|
||||
% "id": "1",
|
||||
% "timestamp": "2011-01-01T14:01.000Z",
|
||||
% "mark": "Workout.",
|
||||
% "uuid": "98ed5198-0c98-47c7-96c9-52b57a2c605a",
|
||||
% "notes": "First workout after a long break."}
|
||||
|
||||
record_to_ejson(Record=#ts_entry{}, ExtData) ->
|
||||
% pull out the username, timeline id, and entry id
|
||||
{Username, TimelineId, EntryId} = Record#ts_entry.ref,
|
||||
|
||||
% convert the timestamp to a date-time
|
||||
DateTime = calendar:gregorian_seconds_to_datetime(Record#ts_entry.timestamp),
|
||||
|
||||
% create the EJSON struct
|
||||
{struct, [
|
||||
{user_id, Username},
|
||||
{timeline_id, TimelineId},
|
||||
{id, EntryId},
|
||||
{timestamp, encode_datetime(DateTime)},
|
||||
{mark, Record#ts_entry.mark},
|
||||
{uuid, uuid:to_string(Record#ts_entry.uuid)},
|
||||
{notes, Record#ts_entry.notes}] ++
|
||||
lists:map(fun ext_data_to_ejson/1, ExtData)}.
|
||||
|
||||
encode_datetime({{Year, Month, Day}, {Hour, Minute, Second}}) ->
|
||||
lists:flatten(io_lib:format("~4.10.0B-~2.10.0B-~2.10.0BT~2.10.0B:~2.10.0B:~2.10.0B.~3.10.0BZ",
|
||||
[Year, Month, Day, Hour, Minute, Second, 000])).
|
||||
|
||||
ext_data_to_ejson({Key, Value}) ->
|
||||
case Key of
|
||||
entry_exclusions -> {Key, {array, Value}};
|
||||
_Other -> {Key, Value}
|
||||
end.
|
||||
|
||||
ejson_to_record(Empty, {struct, EJSONFields}) ->
|
||||
construct_record(Empty, EJSONFields, []).
|
||||
|
||||
ejson_to_record(Empty, Ref, EJSON) ->
|
||||
{Constructed, ExtData} = ejson_to_record(Empty, EJSON),
|
||||
{setelement(2, Constructed, Ref), ExtData}.
|
||||
|
||||
ejson_to_record_strict(Empty=#ts_user{}, EJSON) ->
|
||||
{Constructed, ExtData} = ejson_to_record(Empty, EJSON),
|
||||
case Constructed of
|
||||
#ts_user{name = undefined} -> throw("Missing user 'name' field.");
|
||||
#ts_user{email = undefined} -> throw("Missing user 'email' field.");
|
||||
#ts_user{join_date = undefined} ->
|
||||
throw("Missing user 'join_date' field.");
|
||||
_Other -> {Constructed, ExtData}
|
||||
end;
|
||||
|
||||
ejson_to_record_strict(Empty=#ts_timeline{}, EJSON) ->
|
||||
{Constructed, ExtData} = ejson_to_record(Empty, EJSON),
|
||||
case Constructed of
|
||||
#ts_timeline{created = undefined} ->
|
||||
throw("Missing timeline 'created' field.");
|
||||
#ts_timeline{desc = undefined} ->
|
||||
throw("Missing timeline 'description' field.");
|
||||
_Other -> {Constructed, ExtData}
|
||||
end;
|
||||
|
||||
ejson_to_record_strict(Empty=#ts_entry{}, EJSON) ->
|
||||
{Constructed, ExtData} = ejson_to_record(Empty, EJSON),
|
||||
case Constructed of
|
||||
#ts_entry{timestamp = undefined} ->
|
||||
throw("Missing timelne 'timestamp' field.");
|
||||
#ts_entry{mark = undefined} ->
|
||||
throw("Missing timeline 'mark' field.");
|
||||
#ts_entry{notes = undefined} ->
|
||||
throw("Missing timeline 'notes' field/");
|
||||
_Other -> {Constructed, ExtData}
|
||||
end.
|
||||
|
||||
ejson_to_record_strict(Empty, Ref, EJSON) ->
|
||||
{Constructed, ExtData} = ejson_to_record_strict(Empty, EJSON),
|
||||
{setelement(2, Constructed, Ref), ExtData}.
|
||||
|
||||
construct_record(User=#ts_user{}, [{Key, Value}|Fields], ExtData) ->
|
||||
case Key of
|
||||
id -> construct_record(User#ts_user{username = Value},
|
||||
Fields, ExtData);
|
||||
name -> construct_record(User#ts_user{name = Value}, Fields, ExtData);
|
||||
email -> construct_record(User#ts_user{email = Value}, Fields, ExtData);
|
||||
join_date -> construct_record(
|
||||
User#ts_user{join_date = decode_datetime(Value)},
|
||||
Fields, ExtData);
|
||||
_Other ->
|
||||
ExtDataProp = ejson_to_ext_data({Key, Value}),
|
||||
construct_record(User, Fields, [ExtDataProp|ExtData])
|
||||
end;
|
||||
|
||||
construct_record(Timeline=#ts_timeline{}, [{Key, Value}|Fields], ExtData) ->
|
||||
case Key of
|
||||
user_id ->
|
||||
{_, TimelineId} = Timeline#ts_timeline.ref,
|
||||
construct_record(Timeline#ts_timeline{ref = {Value, TimelineId}},
|
||||
Fields, ExtData);
|
||||
id ->
|
||||
{Username, _} = Timeline#ts_timeline.ref,
|
||||
construct_record(Timeline#ts_timeline{ref = {Username, Value}},
|
||||
Fields, ExtData);
|
||||
created -> construct_record(
|
||||
Timeline#ts_timeline{created = decode_datetime(Value)},
|
||||
Fields, ExtData);
|
||||
description -> construct_record(Timeline#ts_timeline{desc = Value},
|
||||
Fields, ExtData);
|
||||
_Other ->
|
||||
ExtDataProp = ejson_to_ext_data({Key, Value}),
|
||||
construct_record(Timeline, Fields, [ExtDataProp|ExtData])
|
||||
end;
|
||||
|
||||
construct_record(Entry=#ts_entry{}, [{Key, Value}|Fields], ExtData) ->
|
||||
case Key of
|
||||
user_id ->
|
||||
{_, TimelineId, EntryId} = Entry#ts_entry.ref,
|
||||
construct_record(Entry#ts_entry{ref = {Value, TimelineId, EntryId}},
|
||||
Fields, ExtData);
|
||||
timeline_id ->
|
||||
{Username, _, EntryId} = Entry#ts_entry.ref,
|
||||
construct_record(Entry#ts_entry{ref = {Username, Value, EntryId}},
|
||||
Fields, ExtData);
|
||||
id ->
|
||||
{Username, TimelineId, _} = Entry#ts_entry.ref,
|
||||
construct_record(Entry#ts_entry{ref = {Username, TimelineId, Value}},
|
||||
Fields, ExtData);
|
||||
timestamp -> construct_record(
|
||||
Entry#ts_entry{timestamp = calendar:datetime_to_gregorian_seconds(
|
||||
decode_datetime(Value))},
|
||||
Fields, ExtData);
|
||||
mark -> construct_record(Entry#ts_entry{mark=Value}, Fields, ExtData);
|
||||
uuid -> construct_record(Entry#ts_entry{uuid=uuid:to_binary(Value)}, Fields, ExtData);
|
||||
notes -> construct_record(Entry#ts_entry{notes=Value}, Fields, ExtData);
|
||||
_Other ->
|
||||
ExtDataProp = ejson_to_ext_data({Key, Value}),
|
||||
construct_record(Entry, Fields, [ExtDataProp|ExtData])
|
||||
end;
|
||||
|
||||
construct_record(Record, [], ExtData) -> {Record, ExtData}.
|
||||
|
||||
decode_datetime(DateTimeString) ->
|
||||
% TODO: catch badmatch and badarg on whole function
|
||||
|
||||
[DateString, TimeString] = re:split(DateTimeString, "[TZ]",
|
||||
[{return, list}, trim]),
|
||||
|
||||
[YearString, MonthString, DayString] =
|
||||
re:split(DateString, "-", [{return, list}]),
|
||||
|
||||
[HourString, MinuteString, SecondString] =
|
||||
case re:split(TimeString, "[:\\.]", [{return, list}]) of
|
||||
[HS, MS, SS, _MSS] -> [HS, MS, SS];
|
||||
[HS, MS, SS] -> [HS, MS, SS]
|
||||
end,
|
||||
|
||||
Date = {list_to_integer(YearString), list_to_integer(MonthString),
|
||||
list_to_integer(DayString)},
|
||||
|
||||
Time = {list_to_integer(HourString), list_to_integer(MinuteString),
|
||||
list_to_integer(SecondString)},
|
||||
|
||||
{Date, Time}.
|
||||
|
||||
ejson_to_ext_data({Key, Value}) ->
|
||||
case Key of
|
||||
entry_exclusions ->
|
||||
{array, ExclList} = Value,
|
||||
{Key, ExclList};
|
||||
_Other -> {Key, Value}
|
||||
end.
|
||||
|
||||
|
41
src/ts_timeline.erl
Normal file
@ -0,0 +1,41 @@
|
||||
-module(ts_timeline).
|
||||
-export([create_table/1, new/1, new/2, update/1, update/2, write/1, write/2,
|
||||
lookup/2, list/3]).
|
||||
|
||||
-include("ts_db_records.hrl").
|
||||
-include_lib("stdlib/include/qlc.hrl").
|
||||
|
||||
create_table(TableOpts) ->
|
||||
mnesia:create_table(ts_timeline,
|
||||
TableOpts ++ [{attributes, record_info(fields, ts_timeline)},
|
||||
{type, ordered_set}]).
|
||||
|
||||
new(TR = #ts_timeline{}) -> ts_common:new(TR).
|
||||
|
||||
new(TR = #ts_timeline{}, ExtData) when is_list(ExtData) ->
|
||||
ts_common:new(TR, ExtData).
|
||||
|
||||
update(TR = #ts_timeline{}) -> ts_common:update(TR).
|
||||
|
||||
update(TR = #ts_timeline{}, ExtData) when is_list(ExtData) ->
|
||||
ts_common:update(TR, ExtData).
|
||||
|
||||
write(TR = #ts_timeline{}) -> mnesia:dirty_write(TR).
|
||||
|
||||
write(TR = #ts_timeline{}, ExtData) ->
|
||||
{atomic, Result} = mnesia:transaction(fun() ->
|
||||
ok = mnesia:write(TR),
|
||||
ok = ts_common:do_set_ext_data(TR, ExtData)
|
||||
end),
|
||||
Result.
|
||||
|
||||
lookup(Username, TimelineId) ->
|
||||
case mnesia:dirty_read(ts_timeline, {Username, TimelineId}) of
|
||||
[] -> no_record;
|
||||
[Timeline] -> Timeline
|
||||
end.
|
||||
|
||||
list(Username, Start, Length) ->
|
||||
MatchHead = #ts_timeline{ref = {Username, '_'}, _='_'},
|
||||
Timelines = mnesia:dirty_select(ts_timeline, [{MatchHead, [], ['$_']}]),
|
||||
lists:sublist(Timelines, Start + 1, Length).
|
82
src/ts_user.erl
Normal file
@ -0,0 +1,82 @@
|
||||
-module(ts_user).
|
||||
-export([create_table/1, new/1, new/2, update/1, update/2, lookup/1, list/2,
|
||||
check_credentials/2]).
|
||||
|
||||
-include("ts_db_records.hrl").
|
||||
-include_lib("stdlib/include/qlc.hrl").
|
||||
|
||||
create_table(TableOpts) ->
|
||||
mnesia:create_table(ts_user,
|
||||
TableOpts ++ [{attributes, record_info(fields, ts_user)},
|
||||
{type, ordered_set}]).
|
||||
|
||||
% expects the password in clear
|
||||
new(UR = #ts_user{}) ->
|
||||
{atomic, Result} = mnesia:transaction(fun() -> do_new(UR) end),
|
||||
{ok, Result}.
|
||||
|
||||
new(UR = #ts_user{}, ExtData) ->
|
||||
{atomic, Result} = mnesia:transaction(fun() ->
|
||||
NewUser = do_new(UR),
|
||||
ts_common:do_set_ext_data(UR, ExtData),
|
||||
NewUser end),
|
||||
{ok, Result}.
|
||||
|
||||
do_new(UR = #ts_user{}) ->
|
||||
case mnesia:read(ts_user, UR#ts_user.username) of
|
||||
[ExistingRecord] -> {error, {record_exists, ExistingRecord}};
|
||||
[] ->
|
||||
NewRec = hash_input_record(UR),
|
||||
mnesia:write(NewRec),
|
||||
NewRec
|
||||
end.
|
||||
|
||||
update(UR = #ts_user{}) ->
|
||||
{atomic, Result} = mnesia:transaction(fun() -> do_update(UR) end),
|
||||
{ok, Result}.
|
||||
|
||||
update(UR = #ts_user{}, ExtData) ->
|
||||
{atomic, Result} = mnesia:transaction(fun() ->
|
||||
UpdatedUser = do_update(UR),
|
||||
ts_common:do_set_ext_data(UR, ExtData),
|
||||
UpdatedUser end),
|
||||
{ok, Result}.
|
||||
|
||||
do_update(UR = #ts_user{}) ->
|
||||
case mnesia:read(ts_user, UR#ts_user.username) of
|
||||
[] -> no_record;
|
||||
[Record] ->
|
||||
UpdatedRecord = ts_common:update_record(Record, UR),
|
||||
HashedRecord = case UR#ts_user.pwd of
|
||||
undefined -> UpdatedRecord;
|
||||
_Password -> hash_input_record(UpdatedRecord)
|
||||
end,
|
||||
mnesia:write(HashedRecord),
|
||||
HashedRecord
|
||||
end.
|
||||
|
||||
lookup(Username) ->
|
||||
case mnesia:dirty_read(ts_user, Username) of
|
||||
[] -> no_record;
|
||||
[User] -> User
|
||||
end.
|
||||
|
||||
list(Start, Length) -> ts_common:list(ts_user, Start, Length).
|
||||
|
||||
hash_input_record(User=#ts_user{}) ->
|
||||
% create a new User record
|
||||
Salt = generate_salt(),
|
||||
HashedPwd = hash_pwd(User#ts_user.pwd, Salt),
|
||||
User#ts_user{pwd = HashedPwd, pwd_salt = Salt}.
|
||||
|
||||
generate_salt() -> crypto:rand_bytes(36).
|
||||
|
||||
hash_pwd(Password, Salt) -> crypto:sha(Password ++ Salt).
|
||||
|
||||
check_credentials(Username, Password) ->
|
||||
case lookup(Username) of
|
||||
no_record -> false;
|
||||
User ->
|
||||
HashedInput = hash_pwd(Password, User#ts_user.pwd_salt),
|
||||
HashedInput == User#ts_user.pwd
|
||||
end.
|
155
src/yaws_api.hrl
Normal file
@ -0,0 +1,155 @@
|
||||
%%%----------------------------------------------------------------------
|
||||
%%% File : yaws_api.hrl
|
||||
%%% Author : Claes Wikstrom <klacke@hyber.org>
|
||||
%%% Purpose :
|
||||
%%% Created : 24 Jan 2002 by Claes Wikstrom <klacke@hyber.org>
|
||||
%%%----------------------------------------------------------------------
|
||||
|
||||
-author('klacke@hyber.org').
|
||||
|
||||
-record(arg, {
|
||||
clisock, % the socket leading to the peer client
|
||||
client_ip_port, % {ClientIp, ClientPort} tuple
|
||||
headers, % headers
|
||||
req, % request (possibly rewritten)
|
||||
orig_req, % original request
|
||||
clidata, % The client data (as a binary in POST requests)
|
||||
server_path, % The normalized server path
|
||||
% (pre-querystring part of URI)
|
||||
querydata, % For URIs of the form ...?querydata
|
||||
% equiv of cgi QUERY_STRING
|
||||
appmoddata, % (deprecated - use pathinfo instead) the remainder
|
||||
% of the path leading up to the query
|
||||
docroot, % Physical base location of data for this request
|
||||
docroot_mount, % virtual directory e.g /myapp/ that the docroot
|
||||
% refers to.
|
||||
fullpath, % full deep path to yaws file
|
||||
cont, % Continuation for chunked multipart uploads
|
||||
state, % State for use by users of the out/1 callback
|
||||
pid, % pid of the yaws worker process
|
||||
opaque, % useful to pass static data
|
||||
appmod_prepath, % (deprecated - use prepath instead) path in front
|
||||
% of: <appmod><appmoddata>
|
||||
prepath, % Path prior to 'dynamic' segment of URI.
|
||||
% ie http://some.host/<prepath>/<script-point>/d/e
|
||||
% where <script-point> is an appmod mount point,
|
||||
% or .yaws,.php,.cgi,.fcgi etc script file.
|
||||
pathinfo % Set to '/d/e' when calling c.yaws for the request
|
||||
% http://some.host/a/b/c.yaws/d/e
|
||||
% equiv of cgi PATH_INFO
|
||||
}).
|
||||
|
||||
|
||||
-record(http_request, {method,
|
||||
path,
|
||||
version}).
|
||||
|
||||
-record(http_response, {version,
|
||||
status,
|
||||
phrase}).
|
||||
|
||||
-record(rewrite_response, {status,
|
||||
headers = [],
|
||||
content = <<>>}).
|
||||
|
||||
-record(headers, {
|
||||
connection,
|
||||
accept,
|
||||
host,
|
||||
if_modified_since,
|
||||
if_match,
|
||||
if_none_match,
|
||||
if_range,
|
||||
if_unmodified_since,
|
||||
range,
|
||||
referer,
|
||||
user_agent,
|
||||
accept_ranges,
|
||||
cookie = [],
|
||||
keep_alive,
|
||||
location,
|
||||
content_length,
|
||||
content_type,
|
||||
content_encoding,
|
||||
authorization,
|
||||
transfer_encoding,
|
||||
x_forwarded_for,
|
||||
other = [] % misc other headers
|
||||
}).
|
||||
|
||||
|
||||
|
||||
|
||||
-record(url,
|
||||
{scheme, % undefined means not set
|
||||
host, % undefined means not set
|
||||
port, % undefined means not set
|
||||
path = [],
|
||||
querypart = []}).
|
||||
|
||||
|
||||
-record(setcookie, {key,
|
||||
value,
|
||||
quoted = false,
|
||||
domain,
|
||||
max_age,
|
||||
expires,
|
||||
path,
|
||||
secure = false,
|
||||
http_only = false,
|
||||
extensions = []}).
|
||||
|
||||
|
||||
-record(cookie, {key,
|
||||
value,
|
||||
quoted = false}).
|
||||
|
||||
|
||||
-record(redir_self, {
|
||||
host, % string() - our own host
|
||||
scheme, % http | https
|
||||
scheme_str, % "https://" | "http://"
|
||||
port, % integer() - our own port
|
||||
port_str % "" | ":<int>" - the optional port part
|
||||
% to append to the url
|
||||
}).
|
||||
|
||||
%% Corresponds to the frame sections as in
|
||||
%% http://tools.ietf.org/html/rfc6455#section-5.2
|
||||
%% plus 'data' and 'ws_state'. Used for incoming frames.
|
||||
-record(ws_frame_info, {
|
||||
fin,
|
||||
rsv,
|
||||
opcode,
|
||||
masked,
|
||||
masking_key,
|
||||
length,
|
||||
payload,
|
||||
data, % The unmasked payload. Makes payload redundant.
|
||||
ws_state % The ws_state after unframing this frame.
|
||||
% This is useful for the endpoint to know what type of
|
||||
% fragment a potentially fragmented message is.
|
||||
}).
|
||||
|
||||
%% Used for outgoing frames. No checks are done on the validity of a frame. This
|
||||
%% is the application's responsability to send valid frames.
|
||||
-record(ws_frame, {
|
||||
fin = true,
|
||||
rsv = 0,
|
||||
opcode,
|
||||
payload = <<>>
|
||||
}).
|
||||
|
||||
%%----------------------------------------------------------------------
|
||||
%% The state of a WebSocket connection.
|
||||
%% This is held by the ws owner process and passed in calls to yaws_api.
|
||||
%%----------------------------------------------------------------------
|
||||
-type frag_type() :: text
|
||||
| binary
|
||||
| none. % The WebSocket is not expecting continuation
|
||||
% of any fragmented message.
|
||||
-record(ws_state, {
|
||||
vsn :: integer(), % WebSocket version number
|
||||
sock, % gen_tcp or gen_ssl socket
|
||||
frag_type :: frag_type()
|
||||
}).
|
7
test/ts_test_common.erl
Normal file
@ -0,0 +1,7 @@
|
||||
-module(ts_test_common).
|
||||
-compile(export_all).
|
||||
|
||||
-include_lib("eunit/include/eunit.hrl").
|
||||
-include_lib("../src/ts_db_records.hrl").
|
||||
|
||||
|
25
test/ts_user_tests.erl
Normal file
@ -0,0 +1,25 @@
|
||||
-module(ts_user_tests).
|
||||
%-import(ts_test_common, [assertRecordsEqual/2]).
|
||||
|
||||
-include_lib("eunit/include/eunit.hrl").
|
||||
-include_lib("../src/ts_db_records.hrl").
|
||||
|
||||
setup() -> ts_test_env:setup().
|
||||
|
||||
cleanup(_Status) -> ts_api_env:cleanup(ok).
|
||||
|
||||
% ======== TEST DATA ======== %
|
||||
|
||||
joe_user() -> #ts_user{username=joeuser, name="Joe User", pwd="ohmy",
|
||||
email="JoeUser@users.org",
|
||||
join_date=calendar:now_to_datetime(erlang:now())}.
|
||||
|
||||
% ======== TESTS ======== %
|
||||
|
||||
%new_delete_test_() ->
|
||||
% test data
|
||||
%JoeUser = joe_user(),
|
||||
|
||||
%{setup, fun setup/0, fun cleanup/1, {inorder, [
|
||||
% ?_test(new(JoeUser)),
|
||||
% ?_test(lookup(JoeUser#ts_user.username, JoeUser)),
|
BIN
www/css/dot-luv/images/ui-bg_diagonals-thick_15_0b3e6f_40x40.png
Normal file
After Width: | Height: | Size: 385 B |
BIN
www/css/dot-luv/images/ui-bg_dots-medium_30_0b58a2_4x4.png
Normal file
After Width: | Height: | Size: 114 B |
BIN
www/css/dot-luv/images/ui-bg_dots-small_20_333333_2x2.png
Normal file
After Width: | Height: | Size: 83 B |
BIN
www/css/dot-luv/images/ui-bg_dots-small_30_a32d00_2x2.png
Normal file
After Width: | Height: | Size: 83 B |
BIN
www/css/dot-luv/images/ui-bg_dots-small_40_00498f_2x2.png
Normal file
After Width: | Height: | Size: 83 B |
BIN
www/css/dot-luv/images/ui-bg_flat_0_aaaaaa_40x100.png
Normal file
After Width: | Height: | Size: 180 B |
BIN
www/css/dot-luv/images/ui-bg_flat_40_292929_40x100.png
Normal file
After Width: | Height: | Size: 180 B |
BIN
www/css/dot-luv/images/ui-bg_gloss-wave_20_111111_500x100.png
Normal file
After Width: | Height: | Size: 2.1 KiB |
BIN
www/css/dot-luv/images/ui-icons_00498f_256x240.png
Normal file
After Width: | Height: | Size: 4.3 KiB |
BIN
www/css/dot-luv/images/ui-icons_98d2fb_256x240.png
Normal file
After Width: | Height: | Size: 4.3 KiB |
BIN
www/css/dot-luv/images/ui-icons_9ccdfc_256x240.png
Normal file
After Width: | Height: | Size: 5.2 KiB |
BIN
www/css/dot-luv/images/ui-icons_ffffff_256x240.png
Normal file
After Width: | Height: | Size: 4.3 KiB |
573
www/css/dot-luv/jquery-ui-1.8.10.custom.css
vendored
Normal file
@ -0,0 +1,573 @@
|
||||
/*
|
||||
* jQuery UI CSS Framework 1.8.10
|
||||
*
|
||||
* Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
|
||||
* Dual licensed under the MIT or GPL Version 2 licenses.
|
||||
* http://jquery.org/license
|
||||
*
|
||||
* http://docs.jquery.com/UI/Theming/API
|
||||
*/
|
||||
|
||||
/* Layout helpers
|
||||
----------------------------------*/
|
||||
.ui-helper-hidden { display: none; }
|
||||
.ui-helper-hidden-accessible { position: absolute !important; clip: rect(1px 1px 1px 1px); clip: rect(1px,1px,1px,1px); }
|
||||
.ui-helper-reset { margin: 0; padding: 0; border: 0; outline: 0; line-height: 1.3; text-decoration: none; font-size: 100%; list-style: none; }
|
||||
.ui-helper-clearfix:after { content: "."; display: block; height: 0; clear: both; visibility: hidden; }
|
||||
.ui-helper-clearfix { display: inline-block; }
|
||||
/* required comment for clearfix to work in Opera \*/
|
||||
* html .ui-helper-clearfix { height:1%; }
|
||||
.ui-helper-clearfix { display:block; }
|
||||
/* end clearfix */
|
||||
.ui-helper-zfix { width: 100%; height: 100%; top: 0; left: 0; position: absolute; opacity: 0; filter:Alpha(Opacity=0); }
|
||||
|
||||
|
||||
/* Interaction Cues
|
||||
----------------------------------*/
|
||||
.ui-state-disabled { cursor: default !important; }
|
||||
|
||||
|
||||
/* Icons
|
||||
----------------------------------*/
|
||||
|
||||
/* states and images */
|
||||
.ui-icon { display: block; text-indent: -99999px; overflow: hidden; background-repeat: no-repeat; }
|
||||
|
||||
|
||||
/* Misc visuals
|
||||
----------------------------------*/
|
||||
|
||||
/* Overlays */
|
||||
.ui-widget-overlay { position: absolute; top: 0; left: 0; width: 100%; height: 100%; }
|
||||
|
||||
|
||||
/*
|
||||
* jQuery UI CSS Framework 1.8.10
|
||||
*
|
||||
* Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
|
||||
* Dual licensed under the MIT or GPL Version 2 licenses.
|
||||
* http://jquery.org/license
|
||||
*
|
||||
* http://docs.jquery.com/UI/Theming/API
|
||||
*
|
||||
* To view and modify this theme, visit http://jqueryui.com/themeroller/?ffDefault=Arial,%20sans-serif&fwDefault=bold&fsDefault=1.3em&cornerRadius=4px&bgColorHeader=0b3e6f&bgTextureHeader=08_diagonals_thick.png&bgImgOpacityHeader=15&borderColorHeader=0b3e6f&fcHeader=f6f6f6&iconColorHeader=98d2fb&bgColorContent=111111&bgTextureContent=12_gloss_wave.png&bgImgOpacityContent=20&borderColorContent=000000&fcContent=d9d9d9&iconColorContent=9ccdfc&bgColorDefault=333333&bgTextureDefault=09_dots_small.png&bgImgOpacityDefault=20&borderColorDefault=333333&fcDefault=ffffff&iconColorDefault=9ccdfc&bgColorHover=00498f&bgTextureHover=09_dots_small.png&bgImgOpacityHover=40&borderColorHover=222222&fcHover=ffffff&iconColorHover=ffffff&bgColorActive=292929&bgTextureActive=01_flat.png&bgImgOpacityActive=40&borderColorActive=096ac8&fcActive=75abff&iconColorActive=00498f&bgColorHighlight=0b58a2&bgTextureHighlight=10_dots_medium.png&bgImgOpacityHighlight=30&borderColorHighlight=052f57&fcHighlight=ffffff&iconColorHighlight=ffffff&bgColorError=a32d00&bgTextureError=09_dots_small.png&bgImgOpacityError=30&borderColorError=cd0a0a&fcError=ffffff&iconColorError=ffffff&bgColorOverlay=aaaaaa&bgTextureOverlay=01_flat.png&bgImgOpacityOverlay=0&opacityOverlay=30&bgColorShadow=aaaaaa&bgTextureShadow=01_flat.png&bgImgOpacityShadow=0&opacityShadow=30&thicknessShadow=8px&offsetTopShadow=-8px&offsetLeftShadow=-8px&cornerRadiusShadow=8px
|
||||
*/
|
||||
|
||||
|
||||
/* Component containers
|
||||
----------------------------------*/
|
||||
.ui-widget { font-family: Arial, sans-serif; font-size: 1.3em; }
|
||||
.ui-widget .ui-widget { font-size: 1em; }
|
||||
.ui-widget input, .ui-widget select, .ui-widget textarea, .ui-widget button { font-family: Arial, sans-serif; font-size: 1em; }
|
||||
.ui-widget-content { border: 1px solid #000000; background: #111111 url(images/ui-bg_gloss-wave_20_111111_500x100.png) 50% top repeat-x; color: #d9d9d9; }
|
||||
.ui-widget-content a { color: #d9d9d9; }
|
||||
.ui-widget-header { border: 1px solid #0b3e6f; background: #0b3e6f url(images/ui-bg_diagonals-thick_15_0b3e6f_40x40.png) 50% 50% repeat; color: #f6f6f6; font-weight: bold; }
|
||||
.ui-widget-header a { color: #f6f6f6; }
|
||||
|
||||
/* Interaction states
|
||||
----------------------------------*/
|
||||
.ui-state-default, .ui-widget-content .ui-state-default, .ui-widget-header .ui-state-default { border: 1px solid #333333; background: #333333 url(images/ui-bg_dots-small_20_333333_2x2.png) 50% 50% repeat; font-weight: bold; color: #ffffff; }
|
||||
.ui-state-default a, .ui-state-default a:link, .ui-state-default a:visited { color: #ffffff; text-decoration: none; }
|
||||
.ui-state-hover, .ui-widget-content .ui-state-hover, .ui-widget-header .ui-state-hover, .ui-state-focus, .ui-widget-content .ui-state-focus, .ui-widget-header .ui-state-focus { border: 1px solid #222222; background: #00498f url(images/ui-bg_dots-small_40_00498f_2x2.png) 50% 50% repeat; font-weight: bold; color: #ffffff; }
|
||||
.ui-state-hover a, .ui-state-hover a:hover { color: #ffffff; text-decoration: none; }
|
||||
.ui-state-active, .ui-widget-content .ui-state-active, .ui-widget-header .ui-state-active { border: 1px solid #096ac8; background: #292929 url(images/ui-bg_flat_40_292929_40x100.png) 50% 50% repeat-x; font-weight: bold; color: #75abff; }
|
||||
.ui-state-active a, .ui-state-active a:link, .ui-state-active a:visited { color: #75abff; text-decoration: none; }
|
||||
.ui-widget :active { outline: none; }
|
||||
|
||||
/* Interaction Cues
|
||||
----------------------------------*/
|
||||
.ui-state-highlight, .ui-widget-content .ui-state-highlight, .ui-widget-header .ui-state-highlight {border: 1px solid #052f57; background: #0b58a2 url(images/ui-bg_dots-medium_30_0b58a2_4x4.png) 50% 50% repeat; color: #ffffff; }
|
||||
.ui-state-highlight a, .ui-widget-content .ui-state-highlight a,.ui-widget-header .ui-state-highlight a { color: #ffffff; }
|
||||
.ui-state-error, .ui-widget-content .ui-state-error, .ui-widget-header .ui-state-error {border: 1px solid #cd0a0a; background: #a32d00 url(images/ui-bg_dots-small_30_a32d00_2x2.png) 50% 50% repeat; color: #ffffff; }
|
||||
.ui-state-error a, .ui-widget-content .ui-state-error a, .ui-widget-header .ui-state-error a { color: #ffffff; }
|
||||
.ui-state-error-text, .ui-widget-content .ui-state-error-text, .ui-widget-header .ui-state-error-text { color: #ffffff; }
|
||||
.ui-priority-primary, .ui-widget-content .ui-priority-primary, .ui-widget-header .ui-priority-primary { font-weight: bold; }
|
||||
.ui-priority-secondary, .ui-widget-content .ui-priority-secondary, .ui-widget-header .ui-priority-secondary { opacity: .7; filter:Alpha(Opacity=70); font-weight: normal; }
|
||||
.ui-state-disabled, .ui-widget-content .ui-state-disabled, .ui-widget-header .ui-state-disabled { opacity: .35; filter:Alpha(Opacity=35); background-image: none; }
|
||||
|
||||
/* Icons
|
||||
----------------------------------*/
|
||||
|
||||
/* states and images */
|
||||
.ui-icon { width: 16px; height: 16px; background-image: url(images/ui-icons_9ccdfc_256x240.png); }
|
||||
.ui-widget-content .ui-icon {background-image: url(images/ui-icons_9ccdfc_256x240.png); }
|
||||
.ui-widget-header .ui-icon {background-image: url(images/ui-icons_98d2fb_256x240.png); }
|
||||
.ui-state-default .ui-icon { background-image: url(images/ui-icons_9ccdfc_256x240.png); }
|
||||
.ui-state-hover .ui-icon, .ui-state-focus .ui-icon {background-image: url(images/ui-icons_ffffff_256x240.png); }
|
||||
.ui-state-active .ui-icon {background-image: url(images/ui-icons_00498f_256x240.png); }
|
||||
.ui-state-highlight .ui-icon {background-image: url(images/ui-icons_ffffff_256x240.png); }
|
||||
.ui-state-error .ui-icon, .ui-state-error-text .ui-icon {background-image: url(images/ui-icons_ffffff_256x240.png); }
|
||||
|
||||
/* positioning */
|
||||
.ui-icon-carat-1-n { background-position: 0 0; }
|
||||
.ui-icon-carat-1-ne { background-position: -16px 0; }
|
||||
.ui-icon-carat-1-e { background-position: -32px 0; }
|
||||
.ui-icon-carat-1-se { background-position: -48px 0; }
|
||||
.ui-icon-carat-1-s { background-position: -64px 0; }
|
||||
.ui-icon-carat-1-sw { background-position: -80px 0; }
|
||||
.ui-icon-carat-1-w { background-position: -96px 0; }
|
||||
.ui-icon-carat-1-nw { background-position: -112px 0; }
|
||||
.ui-icon-carat-2-n-s { background-position: -128px 0; }
|
||||
.ui-icon-carat-2-e-w { background-position: -144px 0; }
|
||||
.ui-icon-triangle-1-n { background-position: 0 -16px; }
|
||||
.ui-icon-triangle-1-ne { background-position: -16px -16px; }
|
||||
.ui-icon-triangle-1-e { background-position: -32px -16px; }
|
||||
.ui-icon-triangle-1-se { background-position: -48px -16px; }
|
||||
.ui-icon-triangle-1-s { background-position: -64px -16px; }
|
||||
.ui-icon-triangle-1-sw { background-position: -80px -16px; }
|
||||
.ui-icon-triangle-1-w { background-position: -96px -16px; }
|
||||
.ui-icon-triangle-1-nw { background-position: -112px -16px; }
|
||||
.ui-icon-triangle-2-n-s { background-position: -128px -16px; }
|
||||
.ui-icon-triangle-2-e-w { background-position: -144px -16px; }
|
||||
.ui-icon-arrow-1-n { background-position: 0 -32px; }
|
||||
.ui-icon-arrow-1-ne { background-position: -16px -32px; }
|
||||
.ui-icon-arrow-1-e { background-position: -32px -32px; }
|
||||
.ui-icon-arrow-1-se { background-position: -48px -32px; }
|
||||
.ui-icon-arrow-1-s { background-position: -64px -32px; }
|
||||
.ui-icon-arrow-1-sw { background-position: -80px -32px; }
|
||||
.ui-icon-arrow-1-w { background-position: -96px -32px; }
|
||||
.ui-icon-arrow-1-nw { background-position: -112px -32px; }
|
||||
.ui-icon-arrow-2-n-s { background-position: -128px -32px; }
|
||||
.ui-icon-arrow-2-ne-sw { background-position: -144px -32px; }
|
||||
.ui-icon-arrow-2-e-w { background-position: -160px -32px; }
|
||||
.ui-icon-arrow-2-se-nw { background-position: -176px -32px; }
|
||||
.ui-icon-arrowstop-1-n { background-position: -192px -32px; }
|
||||
.ui-icon-arrowstop-1-e { background-position: -208px -32px; }
|
||||
.ui-icon-arrowstop-1-s { background-position: -224px -32px; }
|
||||
.ui-icon-arrowstop-1-w { background-position: -240px -32px; }
|
||||
.ui-icon-arrowthick-1-n { background-position: 0 -48px; }
|
||||
.ui-icon-arrowthick-1-ne { background-position: -16px -48px; }
|
||||
.ui-icon-arrowthick-1-e { background-position: -32px -48px; }
|
||||
.ui-icon-arrowthick-1-se { background-position: -48px -48px; }
|
||||
.ui-icon-arrowthick-1-s { background-position: -64px -48px; }
|
||||
.ui-icon-arrowthick-1-sw { background-position: -80px -48px; }
|
||||
.ui-icon-arrowthick-1-w { background-position: -96px -48px; }
|
||||
.ui-icon-arrowthick-1-nw { background-position: -112px -48px; }
|
||||
.ui-icon-arrowthick-2-n-s { background-position: -128px -48px; }
|
||||
.ui-icon-arrowthick-2-ne-sw { background-position: -144px -48px; }
|
||||
.ui-icon-arrowthick-2-e-w { background-position: -160px -48px; }
|
||||
.ui-icon-arrowthick-2-se-nw { background-position: -176px -48px; }
|
||||
.ui-icon-arrowthickstop-1-n { background-position: -192px -48px; }
|
||||
.ui-icon-arrowthickstop-1-e { background-position: -208px -48px; }
|
||||
.ui-icon-arrowthickstop-1-s { background-position: -224px -48px; }
|
||||
.ui-icon-arrowthickstop-1-w { background-position: -240px -48px; }
|
||||
.ui-icon-arrowreturnthick-1-w { background-position: 0 -64px; }
|
||||
.ui-icon-arrowreturnthick-1-n { background-position: -16px -64px; }
|
||||
.ui-icon-arrowreturnthick-1-e { background-position: -32px -64px; }
|
||||
.ui-icon-arrowreturnthick-1-s { background-position: -48px -64px; }
|
||||
.ui-icon-arrowreturn-1-w { background-position: -64px -64px; }
|
||||
.ui-icon-arrowreturn-1-n { background-position: -80px -64px; }
|
||||
.ui-icon-arrowreturn-1-e { background-position: -96px -64px; }
|
||||
.ui-icon-arrowreturn-1-s { background-position: -112px -64px; }
|
||||
.ui-icon-arrowrefresh-1-w { background-position: -128px -64px; }
|
||||
.ui-icon-arrowrefresh-1-n { background-position: -144px -64px; }
|
||||
.ui-icon-arrowrefresh-1-e { background-position: -160px -64px; }
|
||||
.ui-icon-arrowrefresh-1-s { background-position: -176px -64px; }
|
||||
.ui-icon-arrow-4 { background-position: 0 -80px; }
|
||||
.ui-icon-arrow-4-diag { background-position: -16px -80px; }
|
||||
.ui-icon-extlink { background-position: -32px -80px; }
|
||||
.ui-icon-newwin { background-position: -48px -80px; }
|
||||
.ui-icon-refresh { background-position: -64px -80px; }
|
||||
.ui-icon-shuffle { background-position: -80px -80px; }
|
||||
.ui-icon-transfer-e-w { background-position: -96px -80px; }
|
||||
.ui-icon-transferthick-e-w { background-position: -112px -80px; }
|
||||
.ui-icon-folder-collapsed { background-position: 0 -96px; }
|
||||
.ui-icon-folder-open { background-position: -16px -96px; }
|
||||
.ui-icon-document { background-position: -32px -96px; }
|
||||
.ui-icon-document-b { background-position: -48px -96px; }
|
||||
.ui-icon-note { background-position: -64px -96px; }
|
||||
.ui-icon-mail-closed { background-position: -80px -96px; }
|
||||
.ui-icon-mail-open { background-position: -96px -96px; }
|
||||
.ui-icon-suitcase { background-position: -112px -96px; }
|
||||
.ui-icon-comment { background-position: -128px -96px; }
|
||||
.ui-icon-person { background-position: -144px -96px; }
|
||||
.ui-icon-print { background-position: -160px -96px; }
|
||||
.ui-icon-trash { background-position: -176px -96px; }
|
||||
.ui-icon-locked { background-position: -192px -96px; }
|
||||
.ui-icon-unlocked { background-position: -208px -96px; }
|
||||
.ui-icon-bookmark { background-position: -224px -96px; }
|
||||
.ui-icon-tag { background-position: -240px -96px; }
|
||||
.ui-icon-home { background-position: 0 -112px; }
|
||||
.ui-icon-flag { background-position: -16px -112px; }
|
||||
.ui-icon-calendar { background-position: -32px -112px; }
|
||||
.ui-icon-cart { background-position: -48px -112px; }
|
||||
.ui-icon-pencil { background-position: -64px -112px; }
|
||||
.ui-icon-clock { background-position: -80px -112px; }
|
||||
.ui-icon-disk { background-position: -96px -112px; }
|
||||
.ui-icon-calculator { background-position: -112px -112px; }
|
||||
.ui-icon-zoomin { background-position: -128px -112px; }
|
||||
.ui-icon-zoomout { background-position: -144px -112px; }
|
||||
.ui-icon-search { background-position: -160px -112px; }
|
||||
.ui-icon-wrench { background-position: -176px -112px; }
|
||||
.ui-icon-gear { background-position: -192px -112px; }
|
||||
.ui-icon-heart { background-position: -208px -112px; }
|
||||
.ui-icon-star { background-position: -224px -112px; }
|
||||
.ui-icon-link { background-position: -240px -112px; }
|
||||
.ui-icon-cancel { background-position: 0 -128px; }
|
||||
.ui-icon-plus { background-position: -16px -128px; }
|
||||
.ui-icon-plusthick { background-position: -32px -128px; }
|
||||
.ui-icon-minus { background-position: -48px -128px; }
|
||||
.ui-icon-minusthick { background-position: -64px -128px; }
|
||||
.ui-icon-close { background-position: -80px -128px; }
|
||||
.ui-icon-closethick { background-position: -96px -128px; }
|
||||
.ui-icon-key { background-position: -112px -128px; }
|
||||
.ui-icon-lightbulb { background-position: -128px -128px; }
|
||||
.ui-icon-scissors { background-position: -144px -128px; }
|
||||
.ui-icon-clipboard { background-position: -160px -128px; }
|
||||
.ui-icon-copy { background-position: -176px -128px; }
|
||||
.ui-icon-contact { background-position: -192px -128px; }
|
||||
.ui-icon-image { background-position: -208px -128px; }
|
||||
.ui-icon-video { background-position: -224px -128px; }
|
||||
.ui-icon-script { background-position: -240px -128px; }
|
||||
.ui-icon-alert { background-position: 0 -144px; }
|
||||
.ui-icon-info { background-position: -16px -144px; }
|
||||
.ui-icon-notice { background-position: -32px -144px; }
|
||||
.ui-icon-help { background-position: -48px -144px; }
|
||||
.ui-icon-check { background-position: -64px -144px; }
|
||||
.ui-icon-bullet { background-position: -80px -144px; }
|
||||
.ui-icon-radio-off { background-position: -96px -144px; }
|
||||
.ui-icon-radio-on { background-position: -112px -144px; }
|
||||
.ui-icon-pin-w { background-position: -128px -144px; }
|
||||
.ui-icon-pin-s { background-position: -144px -144px; }
|
||||
.ui-icon-play { background-position: 0 -160px; }
|
||||
.ui-icon-pause { background-position: -16px -160px; }
|
||||
.ui-icon-seek-next { background-position: -32px -160px; }
|
||||
.ui-icon-seek-prev { background-position: -48px -160px; }
|
||||
.ui-icon-seek-end { background-position: -64px -160px; }
|
||||
.ui-icon-seek-start { background-position: -80px -160px; }
|
||||
/* ui-icon-seek-first is deprecated, use ui-icon-seek-start instead */
|
||||
.ui-icon-seek-first { background-position: -80px -160px; }
|
||||
.ui-icon-stop { background-position: -96px -160px; }
|
||||
.ui-icon-eject { background-position: -112px -160px; }
|
||||
.ui-icon-volume-off { background-position: -128px -160px; }
|
||||
.ui-icon-volume-on { background-position: -144px -160px; }
|
||||
.ui-icon-power { background-position: 0 -176px; }
|
||||
.ui-icon-signal-diag { background-position: -16px -176px; }
|
||||
.ui-icon-signal { background-position: -32px -176px; }
|
||||
.ui-icon-battery-0 { background-position: -48px -176px; }
|
||||
.ui-icon-battery-1 { background-position: -64px -176px; }
|
||||
.ui-icon-battery-2 { background-position: -80px -176px; }
|
||||
.ui-icon-battery-3 { background-position: -96px -176px; }
|
||||
.ui-icon-circle-plus { background-position: 0 -192px; }
|
||||
.ui-icon-circle-minus { background-position: -16px -192px; }
|
||||
.ui-icon-circle-close { background-position: -32px -192px; }
|
||||
.ui-icon-circle-triangle-e { background-position: -48px -192px; }
|
||||
.ui-icon-circle-triangle-s { background-position: -64px -192px; }
|
||||
.ui-icon-circle-triangle-w { background-position: -80px -192px; }
|
||||
.ui-icon-circle-triangle-n { background-position: -96px -192px; }
|
||||
.ui-icon-circle-arrow-e { background-position: -112px -192px; }
|
||||
.ui-icon-circle-arrow-s { background-position: -128px -192px; }
|
||||
.ui-icon-circle-arrow-w { background-position: -144px -192px; }
|
||||
.ui-icon-circle-arrow-n { background-position: -160px -192px; }
|
||||
.ui-icon-circle-zoomin { background-position: -176px -192px; }
|
||||
.ui-icon-circle-zoomout { background-position: -192px -192px; }
|
||||
.ui-icon-circle-check { background-position: -208px -192px; }
|
||||
.ui-icon-circlesmall-plus { background-position: 0 -208px; }
|
||||
.ui-icon-circlesmall-minus { background-position: -16px -208px; }
|
||||
.ui-icon-circlesmall-close { background-position: -32px -208px; }
|
||||
.ui-icon-squaresmall-plus { background-position: -48px -208px; }
|
||||
.ui-icon-squaresmall-minus { background-position: -64px -208px; }
|
||||
.ui-icon-squaresmall-close { background-position: -80px -208px; }
|
||||
.ui-icon-grip-dotted-vertical { background-position: 0 -224px; }
|
||||
.ui-icon-grip-dotted-horizontal { background-position: -16px -224px; }
|
||||
.ui-icon-grip-solid-vertical { background-position: -32px -224px; }
|
||||
.ui-icon-grip-solid-horizontal { background-position: -48px -224px; }
|
||||
.ui-icon-gripsmall-diagonal-se { background-position: -64px -224px; }
|
||||
.ui-icon-grip-diagonal-se { background-position: -80px -224px; }
|
||||
|
||||
|
||||
/* Misc visuals
|
||||
----------------------------------*/
|
||||
|
||||
/* Corner radius */
|
||||
.ui-corner-tl { -moz-border-radius-topleft: 4px; -webkit-border-top-left-radius: 4px; border-top-left-radius: 4px; }
|
||||
.ui-corner-tr { -moz-border-radius-topright: 4px; -webkit-border-top-right-radius: 4px; border-top-right-radius: 4px; }
|
||||
.ui-corner-bl { -moz-border-radius-bottomleft: 4px; -webkit-border-bottom-left-radius: 4px; border-bottom-left-radius: 4px; }
|
||||
.ui-corner-br { -moz-border-radius-bottomright: 4px; -webkit-border-bottom-right-radius: 4px; border-bottom-right-radius: 4px; }
|
||||
.ui-corner-top { -moz-border-radius-topleft: 4px; -webkit-border-top-left-radius: 4px; border-top-left-radius: 4px; -moz-border-radius-topright: 4px; -webkit-border-top-right-radius: 4px; border-top-right-radius: 4px; }
|
||||
.ui-corner-bottom { -moz-border-radius-bottomleft: 4px; -webkit-border-bottom-left-radius: 4px; border-bottom-left-radius: 4px; -moz-border-radius-bottomright: 4px; -webkit-border-bottom-right-radius: 4px; border-bottom-right-radius: 4px; }
|
||||
.ui-corner-right { -moz-border-radius-topright: 4px; -webkit-border-top-right-radius: 4px; border-top-right-radius: 4px; -moz-border-radius-bottomright: 4px; -webkit-border-bottom-right-radius: 4px; border-bottom-right-radius: 4px; }
|
||||
.ui-corner-left { -moz-border-radius-topleft: 4px; -webkit-border-top-left-radius: 4px; border-top-left-radius: 4px; -moz-border-radius-bottomleft: 4px; -webkit-border-bottom-left-radius: 4px; border-bottom-left-radius: 4px; }
|
||||
.ui-corner-all { -moz-border-radius: 4px; -webkit-border-radius: 4px; border-radius: 4px; }
|
||||
|
||||
/* Overlays */
|
||||
.ui-widget-overlay { background: #aaaaaa url(images/ui-bg_flat_0_aaaaaa_40x100.png) 50% 50% repeat-x; opacity: .30;filter:Alpha(Opacity=30); }
|
||||
.ui-widget-shadow { margin: -8px 0 0 -8px; padding: 8px; background: #aaaaaa url(images/ui-bg_flat_0_aaaaaa_40x100.png) 50% 50% repeat-x; opacity: .30;filter:Alpha(Opacity=30); -moz-border-radius: 8px; -webkit-border-radius: 8px; border-radius: 8px; }/*
|
||||
* jQuery UI Resizable 1.8.10
|
||||
*
|
||||
* Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
|
||||
* Dual licensed under the MIT or GPL Version 2 licenses.
|
||||
* http://jquery.org/license
|
||||
*
|
||||
* http://docs.jquery.com/UI/Resizable#theming
|
||||
*/
|
||||
.ui-resizable { position: relative;}
|
||||
.ui-resizable-handle { position: absolute;font-size: 0.1px;z-index: 99999; display: block;}
|
||||
.ui-resizable-disabled .ui-resizable-handle, .ui-resizable-autohide .ui-resizable-handle { display: none; }
|
||||
.ui-resizable-n { cursor: n-resize; height: 7px; width: 100%; top: -5px; left: 0; }
|
||||
.ui-resizable-s { cursor: s-resize; height: 7px; width: 100%; bottom: -5px; left: 0; }
|
||||
.ui-resizable-e { cursor: e-resize; width: 7px; right: -5px; top: 0; height: 100%; }
|
||||
.ui-resizable-w { cursor: w-resize; width: 7px; left: -5px; top: 0; height: 100%; }
|
||||
.ui-resizable-se { cursor: se-resize; width: 12px; height: 12px; right: 1px; bottom: 1px; }
|
||||
.ui-resizable-sw { cursor: sw-resize; width: 9px; height: 9px; left: -5px; bottom: -5px; }
|
||||
.ui-resizable-nw { cursor: nw-resize; width: 9px; height: 9px; left: -5px; top: -5px; }
|
||||
.ui-resizable-ne { cursor: ne-resize; width: 9px; height: 9px; right: -5px; top: -5px;}/*
|
||||
* jQuery UI Selectable 1.8.10
|
||||
*
|
||||
* Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
|
||||
* Dual licensed under the MIT or GPL Version 2 licenses.
|
||||
* http://jquery.org/license
|
||||
*
|
||||
* http://docs.jquery.com/UI/Selectable#theming
|
||||
*/
|
||||
.ui-selectable-helper { position: absolute; z-index: 100; border:1px dotted black; }
|
||||
/*
|
||||
* jQuery UI Accordion 1.8.10
|
||||
*
|
||||
* Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
|
||||
* Dual licensed under the MIT or GPL Version 2 licenses.
|
||||
* http://jquery.org/license
|
||||
*
|
||||
* http://docs.jquery.com/UI/Accordion#theming
|
||||
*/
|
||||
/* IE/Win - Fix animation bug - #4615 */
|
||||
.ui-accordion { width: 100%; }
|
||||
.ui-accordion .ui-accordion-header { cursor: pointer; position: relative; margin-top: 1px; zoom: 1; }
|
||||
.ui-accordion .ui-accordion-li-fix { display: inline; }
|
||||
.ui-accordion .ui-accordion-header-active { border-bottom: 0 !important; }
|
||||
.ui-accordion .ui-accordion-header a { display: block; font-size: 1em; padding: .5em .5em .5em .7em; }
|
||||
.ui-accordion-icons .ui-accordion-header a { padding-left: 2.2em; }
|
||||
.ui-accordion .ui-accordion-header .ui-icon { position: absolute; left: .5em; top: 50%; margin-top: -8px; }
|
||||
.ui-accordion .ui-accordion-content { padding: 1em 2.2em; border-top: 0; margin-top: -2px; position: relative; top: 1px; margin-bottom: 2px; overflow: auto; display: none; zoom: 1; }
|
||||
.ui-accordion .ui-accordion-content-active { display: block; }
|
||||
/*
|
||||
* jQuery UI Autocomplete 1.8.10
|
||||
*
|
||||
* Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
|
||||
* Dual licensed under the MIT or GPL Version 2 licenses.
|
||||
* http://jquery.org/license
|
||||
*
|
||||
* http://docs.jquery.com/UI/Autocomplete#theming
|
||||
*/
|
||||
.ui-autocomplete { position: absolute; cursor: default; }
|
||||
|
||||
/* workarounds */
|
||||
* html .ui-autocomplete { width:1px; } /* without this, the menu expands to 100% in IE6 */
|
||||
|
||||
/*
|
||||
* jQuery UI Menu 1.8.10
|
||||
*
|
||||
* Copyright 2010, AUTHORS.txt (http://jqueryui.com/about)
|
||||
* Dual licensed under the MIT or GPL Version 2 licenses.
|
||||
* http://jquery.org/license
|
||||
*
|
||||
* http://docs.jquery.com/UI/Menu#theming
|
||||
*/
|
||||
.ui-menu {
|
||||
list-style:none;
|
||||
padding: 2px;
|
||||
margin: 0;
|
||||
display:block;
|
||||
float: left;
|
||||
}
|
||||
.ui-menu .ui-menu {
|
||||
margin-top: -3px;
|
||||
}
|
||||
.ui-menu .ui-menu-item {
|
||||
margin:0;
|
||||
padding: 0;
|
||||
zoom: 1;
|
||||
float: left;
|
||||
clear: left;
|
||||
width: 100%;
|
||||
}
|
||||
.ui-menu .ui-menu-item a {
|
||||
text-decoration:none;
|
||||
display:block;
|
||||
padding:.2em .4em;
|
||||
line-height:1.5;
|
||||
zoom:1;
|
||||
}
|
||||
.ui-menu .ui-menu-item a.ui-state-hover,
|
||||
.ui-menu .ui-menu-item a.ui-state-active {
|
||||
font-weight: normal;
|
||||
margin: -1px;
|
||||
}
|
||||
/*
|
||||
* jQuery UI Button 1.8.10
|
||||
*
|
||||
* Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
|
||||
* Dual licensed under the MIT or GPL Version 2 licenses.
|
||||
* http://jquery.org/license
|
||||
*
|
||||
* http://docs.jquery.com/UI/Button#theming
|
||||
*/
|
||||
.ui-button { display: inline-block; position: relative; padding: 0; margin-right: .1em; text-decoration: none !important; cursor: pointer; text-align: center; zoom: 1; overflow: visible; } /* the overflow property removes extra width in IE */
|
||||
.ui-button-icon-only { width: 2.2em; } /* to make room for the icon, a width needs to be set here */
|
||||
button.ui-button-icon-only { width: 2.4em; } /* button elements seem to need a little more width */
|
||||
.ui-button-icons-only { width: 3.4em; }
|
||||
button.ui-button-icons-only { width: 3.7em; }
|
||||
|
||||
/*button text element */
|
||||
.ui-button .ui-button-text { display: block; line-height: 1.4; }
|
||||
.ui-button-text-only .ui-button-text { padding: .4em 1em; }
|
||||
.ui-button-icon-only .ui-button-text, .ui-button-icons-only .ui-button-text { padding: .4em; text-indent: -9999999px; }
|
||||
.ui-button-text-icon-primary .ui-button-text, .ui-button-text-icons .ui-button-text { padding: .4em 1em .4em 2.1em; }
|
||||
.ui-button-text-icon-secondary .ui-button-text, .ui-button-text-icons .ui-button-text { padding: .4em 2.1em .4em 1em; }
|
||||
.ui-button-text-icons .ui-button-text { padding-left: 2.1em; padding-right: 2.1em; }
|
||||
/* no icon support for input elements, provide padding by default */
|
||||
input.ui-button { padding: .4em 1em; }
|
||||
|
||||
/*button icon element(s) */
|
||||
.ui-button-icon-only .ui-icon, .ui-button-text-icon-primary .ui-icon, .ui-button-text-icon-secondary .ui-icon, .ui-button-text-icons .ui-icon, .ui-button-icons-only .ui-icon { position: absolute; top: 50%; margin-top: -8px; }
|
||||
.ui-button-icon-only .ui-icon { left: 50%; margin-left: -8px; }
|
||||
.ui-button-text-icon-primary .ui-button-icon-primary, .ui-button-text-icons .ui-button-icon-primary, .ui-button-icons-only .ui-button-icon-primary { left: .5em; }
|
||||
.ui-button-text-icon-secondary .ui-button-icon-secondary, .ui-button-text-icons .ui-button-icon-secondary, .ui-button-icons-only .ui-button-icon-secondary { right: .5em; }
|
||||
.ui-button-text-icons .ui-button-icon-secondary, .ui-button-icons-only .ui-button-icon-secondary { right: .5em; }
|
||||
|
||||
/*button sets*/
|
||||
.ui-buttonset { margin-right: 7px; }
|
||||
.ui-buttonset .ui-button { margin-left: 0; margin-right: -.3em; }
|
||||
|
||||
/* workarounds */
|
||||
button.ui-button::-moz-focus-inner { border: 0; padding: 0; } /* reset extra padding in Firefox */
|
||||
/*
|
||||
* jQuery UI Dialog 1.8.10
|
||||
*
|
||||
* Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
|
||||
* Dual licensed under the MIT or GPL Version 2 licenses.
|
||||
* http://jquery.org/license
|
||||
*
|
||||
* http://docs.jquery.com/UI/Dialog#theming
|
||||
*/
|
||||
.ui-dialog { position: absolute; padding: .2em; width: 300px; overflow: hidden; }
|
||||
.ui-dialog .ui-dialog-titlebar { padding: .4em 1em; position: relative; }
|
||||
.ui-dialog .ui-dialog-title { float: left; margin: .1em 16px .1em 0; }
|
||||
.ui-dialog .ui-dialog-titlebar-close { position: absolute; right: .3em; top: 50%; width: 19px; margin: -10px 0 0 0; padding: 1px; height: 18px; }
|
||||
.ui-dialog .ui-dialog-titlebar-close span { display: block; margin: 1px; }
|
||||
.ui-dialog .ui-dialog-titlebar-close:hover, .ui-dialog .ui-dialog-titlebar-close:focus { padding: 0; }
|
||||
.ui-dialog .ui-dialog-content { position: relative; border: 0; padding: .5em 1em; background: none; overflow: auto; zoom: 1; }
|
||||
.ui-dialog .ui-dialog-buttonpane { text-align: left; border-width: 1px 0 0 0; background-image: none; margin: .5em 0 0 0; padding: .3em 1em .5em .4em; }
|
||||
.ui-dialog .ui-dialog-buttonpane .ui-dialog-buttonset { float: right; }
|
||||
.ui-dialog .ui-dialog-buttonpane button { margin: .5em .4em .5em 0; cursor: pointer; }
|
||||
.ui-dialog .ui-resizable-se { width: 14px; height: 14px; right: 3px; bottom: 3px; }
|
||||
.ui-draggable .ui-dialog-titlebar { cursor: move; }
|
||||
/*
|
||||
* jQuery UI Slider 1.8.10
|
||||
*
|
||||
* Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
|
||||
* Dual licensed under the MIT or GPL Version 2 licenses.
|
||||
* http://jquery.org/license
|
||||
*
|
||||
* http://docs.jquery.com/UI/Slider#theming
|
||||
*/
|
||||
.ui-slider { position: relative; text-align: left; }
|
||||
.ui-slider .ui-slider-handle { position: absolute; z-index: 2; width: 1.2em; height: 1.2em; cursor: default; }
|
||||
.ui-slider .ui-slider-range { position: absolute; z-index: 1; font-size: .7em; display: block; border: 0; background-position: 0 0; }
|
||||
|
||||
.ui-slider-horizontal { height: .8em; }
|
||||
.ui-slider-horizontal .ui-slider-handle { top: -.3em; margin-left: -.6em; }
|
||||
.ui-slider-horizontal .ui-slider-range { top: 0; height: 100%; }
|
||||
.ui-slider-horizontal .ui-slider-range-min { left: 0; }
|
||||
.ui-slider-horizontal .ui-slider-range-max { right: 0; }
|
||||
|
||||
.ui-slider-vertical { width: .8em; height: 100px; }
|
||||
.ui-slider-vertical .ui-slider-handle { left: -.3em; margin-left: 0; margin-bottom: -.6em; }
|
||||
.ui-slider-vertical .ui-slider-range { left: 0; width: 100%; }
|
||||
.ui-slider-vertical .ui-slider-range-min { bottom: 0; }
|
||||
.ui-slider-vertical .ui-slider-range-max { top: 0; }/*
|
||||
* jQuery UI Tabs 1.8.10
|
||||
*
|
||||
* Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
|
||||
* Dual licensed under the MIT or GPL Version 2 licenses.
|
||||
* http://jquery.org/license
|
||||
*
|
||||
* http://docs.jquery.com/UI/Tabs#theming
|
||||
*/
|
||||
.ui-tabs { position: relative; padding: .2em; zoom: 1; } /* position: relative prevents IE scroll bug (element with position: relative inside container with overflow: auto appear as "fixed") */
|
||||
.ui-tabs .ui-tabs-nav { margin: 0; padding: .2em .2em 0; }
|
||||
.ui-tabs .ui-tabs-nav li { list-style: none; float: left; position: relative; top: 1px; margin: 0 .2em 1px 0; border-bottom: 0 !important; padding: 0; white-space: nowrap; }
|
||||
.ui-tabs .ui-tabs-nav li a { float: left; padding: .5em 1em; text-decoration: none; }
|
||||
.ui-tabs .ui-tabs-nav li.ui-tabs-selected { margin-bottom: 0; padding-bottom: 1px; }
|
||||
.ui-tabs .ui-tabs-nav li.ui-tabs-selected a, .ui-tabs .ui-tabs-nav li.ui-state-disabled a, .ui-tabs .ui-tabs-nav li.ui-state-processing a { cursor: text; }
|
||||
.ui-tabs .ui-tabs-nav li a, .ui-tabs.ui-tabs-collapsible .ui-tabs-nav li.ui-tabs-selected a { cursor: pointer; } /* first selector in group seems obsolete, but required to overcome bug in Opera applying cursor: text overall if defined elsewhere... */
|
||||
.ui-tabs .ui-tabs-panel { display: block; border-width: 0; padding: 1em 1.4em; background: none; }
|
||||
.ui-tabs .ui-tabs-hide { display: none !important; }
|
||||
/*
|
||||
* jQuery UI Datepicker 1.8.10
|
||||
*
|
||||
* Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
|
||||
* Dual licensed under the MIT or GPL Version 2 licenses.
|
||||
* http://jquery.org/license
|
||||
*
|
||||
* http://docs.jquery.com/UI/Datepicker#theming
|
||||
*/
|
||||
.ui-datepicker { width: 17em; padding: .2em .2em 0; display: none; }
|
||||
.ui-datepicker .ui-datepicker-header { position:relative; padding:.2em 0; }
|
||||
.ui-datepicker .ui-datepicker-prev, .ui-datepicker .ui-datepicker-next { position:absolute; top: 2px; width: 1.8em; height: 1.8em; }
|
||||
.ui-datepicker .ui-datepicker-prev-hover, .ui-datepicker .ui-datepicker-next-hover { top: 1px; }
|
||||
.ui-datepicker .ui-datepicker-prev { left:2px; }
|
||||
.ui-datepicker .ui-datepicker-next { right:2px; }
|
||||
.ui-datepicker .ui-datepicker-prev-hover { left:1px; }
|
||||
.ui-datepicker .ui-datepicker-next-hover { right:1px; }
|
||||
.ui-datepicker .ui-datepicker-prev span, .ui-datepicker .ui-datepicker-next span { display: block; position: absolute; left: 50%; margin-left: -8px; top: 50%; margin-top: -8px; }
|
||||
.ui-datepicker .ui-datepicker-title { margin: 0 2.3em; line-height: 1.8em; text-align: center; }
|
||||
.ui-datepicker .ui-datepicker-title select { font-size:1em; margin:1px 0; }
|
||||
.ui-datepicker select.ui-datepicker-month-year {width: 100%;}
|
||||
.ui-datepicker select.ui-datepicker-month,
|
||||
.ui-datepicker select.ui-datepicker-year { width: 49%;}
|
||||
.ui-datepicker table {width: 100%; font-size: .9em; border-collapse: collapse; margin:0 0 .4em; }
|
||||
.ui-datepicker th { padding: .7em .3em; text-align: center; font-weight: bold; border: 0; }
|
||||
.ui-datepicker td { border: 0; padding: 1px; }
|
||||
.ui-datepicker td span, .ui-datepicker td a { display: block; padding: .2em; text-align: right; text-decoration: none; }
|
||||
.ui-datepicker .ui-datepicker-buttonpane { background-image: none; margin: .7em 0 0 0; padding:0 .2em; border-left: 0; border-right: 0; border-bottom: 0; }
|
||||
.ui-datepicker .ui-datepicker-buttonpane button { float: right; margin: .5em .2em .4em; cursor: pointer; padding: .2em .6em .3em .6em; width:auto; overflow:visible; }
|
||||
.ui-datepicker .ui-datepicker-buttonpane button.ui-datepicker-current { float:left; }
|
||||
|
||||
/* with multiple calendars */
|
||||
.ui-datepicker.ui-datepicker-multi { width:auto; }
|
||||
.ui-datepicker-multi .ui-datepicker-group { float:left; }
|
||||
.ui-datepicker-multi .ui-datepicker-group table { width:95%; margin:0 auto .4em; }
|
||||
.ui-datepicker-multi-2 .ui-datepicker-group { width:50%; }
|
||||
.ui-datepicker-multi-3 .ui-datepicker-group { width:33.3%; }
|
||||
.ui-datepicker-multi-4 .ui-datepicker-group { width:25%; }
|
||||
.ui-datepicker-multi .ui-datepicker-group-last .ui-datepicker-header { border-left-width:0; }
|
||||
.ui-datepicker-multi .ui-datepicker-group-middle .ui-datepicker-header { border-left-width:0; }
|
||||
.ui-datepicker-multi .ui-datepicker-buttonpane { clear:left; }
|
||||
.ui-datepicker-row-break { clear:both; width:100%; }
|
||||
|
||||
/* RTL support */
|
||||
.ui-datepicker-rtl { direction: rtl; }
|
||||
.ui-datepicker-rtl .ui-datepicker-prev { right: 2px; left: auto; }
|
||||
.ui-datepicker-rtl .ui-datepicker-next { left: 2px; right: auto; }
|
||||
.ui-datepicker-rtl .ui-datepicker-prev:hover { right: 1px; left: auto; }
|
||||
.ui-datepicker-rtl .ui-datepicker-next:hover { left: 1px; right: auto; }
|
||||
.ui-datepicker-rtl .ui-datepicker-buttonpane { clear:right; }
|
||||
.ui-datepicker-rtl .ui-datepicker-buttonpane button { float: left; }
|
||||
.ui-datepicker-rtl .ui-datepicker-buttonpane button.ui-datepicker-current { float:right; }
|
||||
.ui-datepicker-rtl .ui-datepicker-group { float:right; }
|
||||
.ui-datepicker-rtl .ui-datepicker-group-last .ui-datepicker-header { border-right-width:0; border-left-width:1px; }
|
||||
.ui-datepicker-rtl .ui-datepicker-group-middle .ui-datepicker-header { border-right-width:0; border-left-width:1px; }
|
||||
|
||||
/* IE6 IFRAME FIX (taken from datepicker 1.5.3 */
|
||||
.ui-datepicker-cover {
|
||||
display: none; /*sorry for IE5*/
|
||||
display/**/: block; /*sorry for IE5*/
|
||||
position: absolute; /*must have*/
|
||||
z-index: -1; /*must have*/
|
||||
filter: mask(); /*must have*/
|
||||
top: -4px; /*must have*/
|
||||
left: -4px; /*must have*/
|
||||
width: 200px; /*must have*/
|
||||
height: 200px; /*must have*/
|
||||
}/*
|
||||
* jQuery UI Progressbar 1.8.10
|
||||
*
|
||||
* Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
|
||||
* Dual licensed under the MIT or GPL Version 2 licenses.
|
||||
* http://jquery.org/license
|
||||
*
|
||||
* http://docs.jquery.com/UI/Progressbar#theming
|
||||
*/
|
||||
.ui-progressbar { height:2em; text-align: left; }
|
||||
.ui-progressbar .ui-progressbar-value {margin: -1px; height:100%; }
|
28
www/css/forSize.scss
Normal file
@ -0,0 +1,28 @@
|
||||
@import url(//fonts.googleapis.com/css?family=Maven+Pro:400,700|Exo:400,600|PT+Mono);
|
||||
|
||||
/** ### forSize
|
||||
* This mixin allows us to apply some rules selectively based on the screen
|
||||
* size. There are three primary sizes: `small`, `medium`, and `large`, which
|
||||
* are mutually exclusive. Additionally there are two additional sizes:
|
||||
* `notSmall` and `ultraLarge`. `notSmall`, as the name implies matches any
|
||||
* value which is not the small screen size, so it overlaps with medium,
|
||||
* large, and ultraLarge. `ultraLarge` defines a wider minimum screen size
|
||||
* than large, but neither large nor ultraLarge specify maximum widths,
|
||||
* so ultraLarge is a strict subset of large. A screen large enough to match
|
||||
* ultraLarge will also match large (compare with medium and large: matching
|
||||
* medium means it will not match large, and vice versa). */
|
||||
@mixin forSize($size) {
|
||||
|
||||
@if $size == small {
|
||||
@media screen and (max-width: $smallScreen) { @content; } }
|
||||
@else if $size == notSmall {
|
||||
@media screen and (min-width: $smallScreen + 1) { @content; } }
|
||||
@else if $size == medium {
|
||||
@media screen and (min-width: $smallScreen + 1) and (max-width: $wideScreen - 1) { @content; } }
|
||||
@else if $size == large {
|
||||
@media screen and (min-width: $wideScreen) { @content; } }
|
||||
@else if $size == ultraLarge {
|
||||
@media screen and (min-width: $ultraWideScreen) { @content; } }
|
||||
}
|
||||
|
||||
|
13
www/css/rounded.scss
Normal file
@ -0,0 +1,13 @@
|
||||
/* _rounded.scss */
|
||||
|
||||
@mixin rounded($radius: 10px) {
|
||||
-moz-border-radius: $radius;
|
||||
-webkit-border-radius: $radius;
|
||||
border-radius: $radius;
|
||||
}
|
||||
|
||||
@mixin rounded2($side1, $side2, $radius: 10px) {
|
||||
-moz-border-radius-#{$side1}#{$side2}: $radius;
|
||||
-webkit-border-#{$side1}-#{$side2}-radius: $radius;
|
||||
border-#{$side1}-#{$side2}-radius: $radius;
|
||||
}
|
380
www/css/ts-screen.scss
Normal file
@ -0,0 +1,380 @@
|
||||
/*
|
||||
* author: Jonathan Bernard
|
||||
* TimeStamper main CSS for screen media types
|
||||
*/
|
||||
|
||||
$smallScreen: 640px;
|
||||
$wideScreen: 1000px;
|
||||
$ultraWideScreen: 1400px;
|
||||
|
||||
@import "rounded";
|
||||
@import "forSize";
|
||||
|
||||
$darkTxt: #222;
|
||||
$lightTxt: #eee;
|
||||
$darkBg: #222;
|
||||
$lightBg: #eee;
|
||||
$medBg: #CCC;
|
||||
|
||||
* {
|
||||
color: inherit;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
-moz-box-sizing: border-box;
|
||||
-webkit-box-sizing: border-box;
|
||||
box-sizing: border-box; }
|
||||
|
||||
/* HTML5 elements */
|
||||
article,aside,details,figcaption,figure,
|
||||
footer,header,hgroup,menu,nav,section {
|
||||
display:block; }
|
||||
|
||||
body {
|
||||
color: $darkTxt;
|
||||
margin: auto;
|
||||
line-height: 1.4; }
|
||||
|
||||
.hidden { display: none; }
|
||||
|
||||
input {
|
||||
border: solid thin lighten($darkTxt, 20%);
|
||||
-webkit-box-shadow: inset 0px 2px 4px #CCC;
|
||||
box-shadow: inset 0px 2px 4px #CCC;
|
||||
font-family: Cantarell; }
|
||||
|
||||
#top {
|
||||
background: $darkBg;
|
||||
color: $lightTxt;
|
||||
opacity: 1;
|
||||
padding-top: 0.5rem; }
|
||||
|
||||
#timeline {
|
||||
|
||||
border-bottom: thin solid $lightBg;
|
||||
font-family: Arvo;
|
||||
|
||||
.timeline-desc-input, .timeline-id-input {
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
color: $darkBg;
|
||||
display: none; }
|
||||
|
||||
&.edit-id {
|
||||
.timeline-id-input { display: inline-block; }
|
||||
.timeline-id { display: none; } }
|
||||
|
||||
&.edit-desc {
|
||||
.timeline-desc-input { display: inline-block; }
|
||||
.timeline-desc { display: none; } }
|
||||
|
||||
}
|
||||
|
||||
#entry-list {
|
||||
|
||||
padding-bottom: 1rem;
|
||||
|
||||
.day-seperator {
|
||||
|
||||
background: $medBg;
|
||||
color: $darkBg;
|
||||
font-family: Cantarell;
|
||||
font-weight: bold;
|
||||
margin: 1rem 0 0 0;
|
||||
padding: 0 2rem;
|
||||
|
||||
h5 { color: #667; } }
|
||||
|
||||
#new-entry {
|
||||
margin: 0.5rem 0 0 0;
|
||||
padding: 0 2rem; }
|
||||
|
||||
.timestamp, .timestamp-input, .duration { text-align: right; }
|
||||
|
||||
.entry {
|
||||
font-family: Cantarell;
|
||||
padding: 0 2rem;
|
||||
|
||||
.mark {
|
||||
position: relative;
|
||||
|
||||
img.expand-entry, img.collapse-entry {
|
||||
display: none;
|
||||
left: -20px;
|
||||
position: absolute;
|
||||
top: 6px; } }
|
||||
|
||||
&:hover .mark img.expand-entry, &.show-notes img.collapse-entry { display: inline; }
|
||||
|
||||
.mark-input, .timestamp-input,
|
||||
&.show-notes:hover img.expand-entry { display: none; }
|
||||
|
||||
.notes {
|
||||
display: none;
|
||||
font-family: Cantarell;
|
||||
font-size: small;
|
||||
padding: 0 0 0 1em;
|
||||
|
||||
:first-child { margin-top: 0; }
|
||||
|
||||
.notes-input, pre, code { font-family: 'Anonymous Pro'; } }
|
||||
|
||||
.notes * { width: 100%; }
|
||||
|
||||
&.edit-mark {
|
||||
.mark-input { display: inline-block; }
|
||||
.mark { display: none; } }
|
||||
|
||||
&.edit-timestamp {
|
||||
.timestamp-input { display: inline-block; }
|
||||
.timestamp { display: none; } }
|
||||
|
||||
.notes-input { display: none; }
|
||||
|
||||
&.edit-notes .notes-input { display: block; }
|
||||
|
||||
&.edit-notes .notes-text { display: none; }
|
||||
|
||||
&.current .duration {
|
||||
-webkit-animation: pulse 1s infinite alternate;
|
||||
-moz-animation: pulse 1s infinite alternate;
|
||||
-o-animation: pulse 1s infinite alternate;
|
||||
animation: pulse 1s infinite alternate; } } }
|
||||
|
||||
|
||||
.dialog {
|
||||
|
||||
background: white;
|
||||
background: rgba(255, 255, 255, 0.5);
|
||||
color: $lightTxt;
|
||||
|
||||
position: fixed;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
|
||||
width: 100%;
|
||||
height: 100em;
|
||||
|
||||
z-index: 10;
|
||||
|
||||
div.container {
|
||||
|
||||
background: $darkBg;
|
||||
border-radius: 10px;
|
||||
font-family: Cantarell;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
margin-top: 4em;
|
||||
padding: 1em;
|
||||
width: 20em;
|
||||
|
||||
h2 {
|
||||
border-bottom: thin solid $lightBg;
|
||||
font-family: Arvo;
|
||||
padding-bottom: 0.5em;
|
||||
margin-bottom: 0.5em; }
|
||||
|
||||
label {
|
||||
display: inline-block;
|
||||
width: 6em; }
|
||||
|
||||
input { color: $darkTxt; }
|
||||
|
||||
.button-panel {
|
||||
margin-top: 0.5em;
|
||||
overflow: hidden;
|
||||
|
||||
.validate-tips { font-size: 1em; }
|
||||
|
||||
.dialog-button {
|
||||
|
||||
float: right;
|
||||
padding-left: 1em;
|
||||
|
||||
font-family: Arvo;
|
||||
font-size: 1.2em;
|
||||
|
||||
a { color: $lightBg; } } } } }
|
||||
|
||||
#login.dialog {
|
||||
background: white;
|
||||
opacity: 1; }
|
||||
|
||||
.drop-menu {
|
||||
position: relative;
|
||||
|
||||
.drop-menu-items {
|
||||
display: none;
|
||||
list-style: none;
|
||||
position: absolute;
|
||||
|
||||
li {
|
||||
display: inline-block;
|
||||
padding-left: 0.5em; } }
|
||||
|
||||
&:hover .drop-menu-items, .drop-menu-items:hover { display: block; }
|
||||
|
||||
a {
|
||||
display: inline-block;
|
||||
text-decoration: none;
|
||||
|
||||
&:hover { text-decoration: underline; } } }
|
||||
|
||||
|
||||
.footer {
|
||||
|
||||
background: $darkBg;
|
||||
color: $lightTxt;
|
||||
font-family: Bentham;
|
||||
padding: 1rem 0;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
|
||||
a {
|
||||
color: lighten($lightTxt, 20%);
|
||||
text-decoration: none;
|
||||
|
||||
&:hover { text-decoration: underline; } } }
|
||||
|
||||
.logo {
|
||||
font-family: Bentham;
|
||||
text-decoration: overline underline; }
|
||||
|
||||
@include forSize(notSmall) {
|
||||
body { width: 75%; }
|
||||
|
||||
#top {
|
||||
position: fixed;
|
||||
top: 0px;
|
||||
width: 75%;
|
||||
z-index: 1; }
|
||||
|
||||
#timeline {
|
||||
font-size: 1.5em;
|
||||
padding: 0 2rem;
|
||||
|
||||
.timeline-desc {
|
||||
display: inline-block;
|
||||
width: 70%; }
|
||||
|
||||
.timeline-desc-input { width: 70% }
|
||||
|
||||
.timeline-id { display: inline-block; }
|
||||
|
||||
.drop-menu {
|
||||
text-align: right;
|
||||
display: inline-block;
|
||||
width: 29%;
|
||||
|
||||
.drop-menu-items {
|
||||
text-align: right;
|
||||
right: 0;
|
||||
width: 172.41%;
|
||||
|
||||
.new-timeline-link {
|
||||
padding-right: 0.5em;
|
||||
font-size: medium;
|
||||
|
||||
a { text-decoration: underline; }
|
||||
|
||||
img {
|
||||
position: relative;
|
||||
top: 4px;
|
||||
left: -4px; } }
|
||||
|
||||
.timeline-link {
|
||||
padding: 0 0.5em;
|
||||
font-size: medium;
|
||||
border-left: thin solid white } } } }
|
||||
|
||||
#user {
|
||||
|
||||
font-family: "Josefin Sans";
|
||||
margin-top: 0.3rem;
|
||||
padding: 0 0 0.3rem 2rem;
|
||||
width: 100%;
|
||||
|
||||
.fullname, .username {
|
||||
display: inline-block;
|
||||
font-size: larger; }
|
||||
|
||||
.fullname-input {
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
color: $darkBg;
|
||||
display: none; }
|
||||
|
||||
&.edit-fullname{
|
||||
.fullname-input { display: inline-block; }
|
||||
.fullname { display: none; } }
|
||||
|
||||
.user-menu { display: inline-block; }
|
||||
|
||||
.user-menu .user-menu-items {
|
||||
list-style: none;
|
||||
display: none;
|
||||
|
||||
li {
|
||||
display: inline-block;
|
||||
padding-left: 0.5em; }
|
||||
|
||||
a { text-decoration: none; }
|
||||
a:hover { text-decoration: underline; }
|
||||
}
|
||||
|
||||
.user-menu:hover .user-menu-items, .user-menu-items:hover { display: inline-block; } }
|
||||
|
||||
#entry-list {
|
||||
margin: 6rem 0 0 0;
|
||||
|
||||
.timestamp, .timestamp-input, .duration { width: 14%; }
|
||||
.mark, .mark-input, .notes { width: 70%; }
|
||||
|
||||
h4, h5, .entry div { display: inline-block; }
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@include forSize(small) {
|
||||
body { width: 100%; }
|
||||
|
||||
#timeline {
|
||||
font-size: 1.3em;
|
||||
padding-bottom: 0.5rem;
|
||||
|
||||
.timeline-desc { display: block; }
|
||||
|
||||
.timeline-desc, .timeline-desc-input {
|
||||
text-align: center;
|
||||
width: 100%; }
|
||||
|
||||
.timeline-id, .timeline-id-input { display: none; } }
|
||||
|
||||
#user { display: none; }
|
||||
|
||||
#entry-list {
|
||||
.timestamp, .timestamp-input { width: 18%; }
|
||||
.mark, .notes { width: 80%; }
|
||||
.mark-input { width: 98% }
|
||||
.duration { display: none; }
|
||||
h4.mark, h5.mark, h4.timestamp, h5.timestamp,
|
||||
div.mark, div.timestamp{ display: inline-block; }
|
||||
}
|
||||
}
|
||||
|
||||
@-webkit-keyframes pulse {
|
||||
from { opacity: 1; }
|
||||
to { opacity: 0.2; } }
|
||||
|
||||
@-moz-keyframes pulse {
|
||||
from { opacity: 1; }
|
||||
to { opacity: 0.2; } }
|
||||
|
||||
@-o-keyframes pulse {
|
||||
from { opacity: 1; }
|
||||
to { opacity: 0.2; } }
|
||||
|
||||
@keyframes pulse {
|
||||
from { opacity: 1; }
|
||||
to { opacity: 0.2; } }
|
||||
|
4
www/html_only.yaws
Normal file
@ -0,0 +1,4 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
</html>
|
BIN
www/img/br_down_icon_16.png
Normal file
After Width: | Height: | Size: 3.5 KiB |
BIN
www/img/br_down_icon_24.png
Normal file
After Width: | Height: | Size: 3.6 KiB |
BIN
www/img/br_down_icon_32.png
Normal file
After Width: | Height: | Size: 3.7 KiB |
BIN
www/img/br_down_icon_48.png
Normal file
After Width: | Height: | Size: 4.0 KiB |
BIN
www/img/br_up_icon_16.png
Normal file
After Width: | Height: | Size: 3.4 KiB |