Merge Web application.

This commit is contained in:
Jonathan Bernard 2024-08-04 20:45:13 -05:00
commit 338eab1c96
139 changed files with 18071 additions and 0 deletions

2
.gitignore vendored
View File

@ -1,3 +1,5 @@
.gradle/
*.sw?
build/
.sass-cache
*.build.tar.gz

26
.ide/change_workspace.pl Executable file
View 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);
}

View 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
View 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
View 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

View 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

View 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
View 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

View 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

View 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
View 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

Binary file not shown.

BIN
db/test/LATEST.LOG Normal file

Binary file not shown.

BIN
db/test/id_counter.DCD Normal file

Binary file not shown.

BIN
db/test/id_counter.DCL Normal file

Binary file not shown.

BIN
db/test/schema.DAT Normal file

Binary file not shown.

1
db/test/ts_entry.DCD Normal file
View File

@ -0,0 +1 @@
cXM

BIN
db/test/ts_entry.DCL Normal file

Binary file not shown.

BIN
db/test/ts_ext_data.DCD Normal file

Binary file not shown.

BIN
db/test/ts_timeline.DCD Normal file

Binary file not shown.

BIN
db/test/ts_timeline.DCL Normal file

Binary file not shown.

BIN
db/test/ts_user.DCD Normal file

Binary file not shown.

97
doc/api.rst Normal file
View 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
View 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
View 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
View File

@ -0,0 +1,4 @@
Refactor models and views.
==========================
Try to find the behavior that is common to mobile and desktop versions.

View File

@ -0,0 +1,2 @@
Add UI for note taking.
=======================

View File

@ -0,0 +1,9 @@
Add Markdown converter for notes.
=================================
Brief description.
========= ==========
Created: 2011-05-15
Resolved: 2011-05-15
========= ==========

View 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
========= ==========

View 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
========= ==========

View 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
========= ==========

View 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
========= ==========

View File

@ -0,0 +1,9 @@
Fix timeline menu UI.
=====================
UI for timeline menu does not work.
========= ==========
Created: 2011-05-15
Resolved: 2011-06-07
========= ==========

View File

@ -0,0 +1,9 @@
Implement timeline selection.
=============================
Allow the user to change timelines.
========= ==========
Created: 2011-05-15
Resolved: 2011-06-08
========= ==========

View 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
========= ==========

View 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
========= ==========

View 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
========= ==========

View 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
========= ==========

View 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
========= ==========

View 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
========= ==========

View 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
========= ==========

View 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
========= ==========

View 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
========= ==========

View File

@ -0,0 +1,7 @@
Implement timeline creation.
============================
========= ==========
Created: 2011-06-01
Resolved: 2011-06-07
========= ==========

View 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
========= ==========

View 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
========= ==========

View 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
========= ==========

View File

@ -0,0 +1,7 @@
Constrain width of notes fields to width of mark.
=================================================
========= ==========
Created: 2011-06-10
Resolved: 2011-06-17
====================

View 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
========= ==========

View 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
========= ==========

View 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
========= ==========

View 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
========= ==========

View 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
========= ==========

View 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
========= ==========

View 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
========= ==========

View File

@ -0,0 +1 @@
Prototype out mobile workflow.

34
doc/model.txt Normal file
View 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

Binary file not shown.

5
doc/sampl-urls.txt Normal file
View 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
View 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
View 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

Binary file not shown.

5
login.post Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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)),

Binary file not shown.

After

Width:  |  Height:  |  Size: 385 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 180 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 180 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

View 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
View 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
View 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
View 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
View File

@ -0,0 +1,4 @@
<!DOCTYPE html>
<html>
</html>

BIN
www/img/br_down_icon_16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

BIN
www/img/br_down_icon_24.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

BIN
www/img/br_down_icon_32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

BIN
www/img/br_down_icon_48.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

BIN
www/img/br_up_icon_16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Some files were not shown because too many files have changed in this diff Show More