commit 8c4bc816946c43ee2e884f933e73703b380e2095 Author: Bel LaPointe Date: Tue Nov 12 13:45:32 2019 -0700 Unittesting begins diff --git a/.gitignore b/.gitignore new file mode 100755 index 0000000..0e79a76 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +gollum +mytinytodo +**.sw* +**/**.sw* +*.sw* +**/*.sw* +todo-server +exec-todo-server +notes-server +exec-notes-server diff --git a/config/config.go b/config/config.go new file mode 100755 index 0000000..60753eb --- /dev/null +++ b/config/config.go @@ -0,0 +1,45 @@ +package config + +import ( + "fmt" + "local/args" + "os" + "strings" +) + +var ( + Port string + StoreType string + StoreAddr string + StoreUser string + StorePass string + Public string +) + +func init() { + Refresh() +} + +func Refresh() { + if strings.Contains(fmt.Sprint(os.Args), "-test") { + return + } + + as := args.NewArgSet() + as.Append(args.STRING, "port", "port to listen on", "39909") + as.Append(args.STRING, "store", "type of store", "map") + as.Append(args.STRING, "storeaddr", "addr of store", "") + as.Append(args.STRING, "storeuser", "user of store", "") + as.Append(args.STRING, "storepass", "pass of store", "") + as.Append(args.STRING, "public", "url of php server", "http://localhost:38808") + if err := as.Parse(); err != nil { + panic(err) + } + + Port = ":" + strings.TrimPrefix(as.Get("port").GetString(), ":") + StoreType = as.Get("store").GetString() + StoreAddr = as.Get("storeaddr").GetString() + StoreUser = as.Get("storeuser").GetString() + StorePass = as.Get("storepass").GetString() + Public = as.Get("public").GetString() +} diff --git a/feed/feed.go b/feed/feed.go new file mode 100644 index 0000000..95647c4 --- /dev/null +++ b/feed/feed.go @@ -0,0 +1,17 @@ +package feed + +import ( + "errors" + "local/storage" + "net/url" +) + +type Feed struct{} + +func New(db storage.DB, values url.Values) (*Feed, error) { + return nil, errors.New("not impl") +} + +func (f *Feed) Read(p []byte) (int, error) { + return 0, errors.New("not impl") +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..dfeda0c --- /dev/null +++ b/main.go @@ -0,0 +1,17 @@ +package main + +import ( + "local/todo-server/config" + "local/todo-server/server" + "net/http" +) + +func main() { + s := server.New() + if err := s.Routes(); err != nil { + panic(err) + } + if err := http.ListenAndServe(config.Port, s); err != nil { + panic(err) + } +} diff --git a/public/images/COPYRIGHT b/public/images/COPYRIGHT new file mode 100755 index 0000000..336c5d7 --- /dev/null +++ b/public/images/COPYRIGHT @@ -0,0 +1,13 @@ +Image files page_white_text.png and calendar.png and some icons in buttons.png +are (or based on) icons from Silk Icons set by Mark James (http://www.famfamfam.com/lab/icons/silk/), +licensed under the Creative Commons Attribution 2.5 License (http://creativecommons.org/licenses/by/2.5/). + +Some icons in buttons.png are based on "Silk Companion 1" icons set by Damien Guard +(http://damieng.com/creative/icons/silk-companion-1-icons), licensed under the + Creative Commons Attribution 2.5 License (http://creativecommons.org/licenses/by/2.5/). + +Icons in mzl.png are (or based on) icons from Mozilla Source Code, +(http://www.mozilla.org/MPL/#source-code), licensed under the terms +of tri-license (MPL 1.1/GPL 2.0/LGPL 2.1). + +Other images in this directory were made by Max Pozdeev and licensed under the terms of GNU GPL v2+. \ No newline at end of file diff --git a/public/images/arrdown.gif b/public/images/arrdown.gif new file mode 100755 index 0000000..8ea51d9 Binary files /dev/null and b/public/images/arrdown.gif differ diff --git a/public/images/arrdown2.gif b/public/images/arrdown2.gif new file mode 100755 index 0000000..5934ee4 Binary files /dev/null and b/public/images/arrdown2.gif differ diff --git a/public/images/closetag.gif b/public/images/closetag.gif new file mode 100755 index 0000000..bfea142 Binary files /dev/null and b/public/images/closetag.gif differ diff --git a/public/images/corner_left.gif b/public/images/corner_left.gif new file mode 100755 index 0000000..f01063a Binary files /dev/null and b/public/images/corner_left.gif differ diff --git a/public/images/corner_right.gif b/public/images/corner_right.gif new file mode 100755 index 0000000..1619019 Binary files /dev/null and b/public/images/corner_right.gif differ diff --git a/public/images/icons.gif b/public/images/icons.gif new file mode 100755 index 0000000..84e18e0 Binary files /dev/null and b/public/images/icons.gif differ diff --git a/public/images/index.html b/public/images/index.html new file mode 100755 index 0000000..31ed4cb --- /dev/null +++ b/public/images/index.html @@ -0,0 +1 @@ +Direct access disallowed! \ No newline at end of file diff --git a/public/images/loading1.gif b/public/images/loading1.gif new file mode 100755 index 0000000..7525a26 Binary files /dev/null and b/public/images/loading1.gif differ diff --git a/public/images/loading1_24.gif b/public/images/loading1_24.gif new file mode 100755 index 0000000..7d7dbbc Binary files /dev/null and b/public/images/loading1_24.gif differ diff --git a/public/images/tab_hover.gif b/public/images/tab_hover.gif new file mode 100755 index 0000000..170362d Binary files /dev/null and b/public/images/tab_hover.gif differ diff --git a/public/index.php b/public/index.php new file mode 100755 index 0000000..bd60df7 --- /dev/null +++ b/public/index.php @@ -0,0 +1,358 @@ + + + + + + <?php mttinfo('title'); ?> + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +

+ +
+ +
+
+
+
+
+ + + + + + + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + + +
+ + + \ No newline at end of file diff --git a/public/pda.css b/public/pda.css new file mode 100755 index 0000000..3599855 --- /dev/null +++ b/public/pda.css @@ -0,0 +1,190 @@ +body { + margin: 0px; + font-size: 100%; +} + +h2 { + margin: 0; + margin-bottom: 2px; + font-size: 1em; +} + +h3 { + margin-bottom: 4px; + padding: 4px 0; +} + +#body { + margin-left: 1px; + margin-right: 1px; + padding: 1px; + padding-bottom: 12px; +} + +#bar_login, #bar_logout { + padding-right: 1px; +} + +#tabs ul { + margin-top: 0px; +} + +#tabs ul li { + width: 70px; + margin-right: 1px; +} + +.tab-content { + padding: 4px; +} + +#htab_search { + width: 40%; + max-width: 190px; +} + +.mtt-searchbox { + float: right; +} + +.mtt-searchbox td { + width: 40%; +} + +#toolbar.mtt-intask #htab_search { + display: none; +} + +#toolbar.mtt-insearch #htab_newtask { + display: none; +} + +#toolbar.mtt-insearch #htab_search { + width: 100%; +} + +#toolbar.mtt-insearch .mtt-searchbox td { + width: 40%; +} + +#tasklist li { + padding: 0.5em 3px; + overflow: hidden; +} + +.task-actions { + display: none; +} + +.task-date { + display: none; +} + +.task-note-actions { + display: block; + padding-top: 8px; +} + +.task-note-block { + margin-left: 0px; + border-left: 1px solid #777777; + background: none; + padding-left: 4px; + margin-top: 1px; + padding-top: 0px; + display: none; +} + +.task-note-area textarea { + width: 95%; +} + +.task-middle { + margin-right: 0px; +} + +#tasklist li .task-through { + white-space: nowrap; + overflow: hidden; +} + +#tasklist li:hover { + background-color: #ffffff; +} + +#tasklist li.task-expanded .task-note-block { + display: none; +} + +/*#tasklist li.task-expanded .task-toggle { background-position:-32px 0; }*/ +#tasklist li.clicked { + background-color: #f6f6f6; +} + +#tasklist li.clicked .task-actions { + display: block; +} + +#tasklist li.clicked .task-through { + white-space: normal; + display: inline; +} + +#tasklist li.clicked.task-has-note .task-note-block { + display: block; +} + +/*#tasklist li.clicked.task-has-note .task-toggle { background-position:-48px 0; } */ +#tasklist li.clicked.doubleclicked.task-has-note .task-note-block { + display: none; +} + +.task-toggle { + display: none; +} + +.task-middle { + margin-left: 25px; +} + +#page_taskedit { + max-width: 99.5%; + border: none; + position: static; + padding: 0; +} + +#page_taskedit .form-table { + width: 100%; +} + +#page_taskedit .form-row .in500 { + color: #444444; +} + +#page_taskedit .form-row textarea { + height: 70px; +} + +#loading { + padding: 0px; + padding-top: 1px; + padding-right: 1px; + height: 16px; + overflow: hidden; +} + +#loading img { /*width:8px; height:8px;*/ +} + +#tagcloud { + max-width: 100%; +} + +.mtt-settings-table .in350 { + min-width: 50px; +} + +.mtt-notes-showhide { + display: none; +} diff --git a/public/print.css b/public/print.css new file mode 100755 index 0000000..36ff976 --- /dev/null +++ b/public/print.css @@ -0,0 +1,116 @@ +html { + height: 0; +} + +body { + height: 0; + min-height: 0; + margin: 0; +} + +h2 { + display: none; +} + +h3 { + border-bottom: 2px solid #777777; +} + +#lists { + display: none; +} + +#toolbar { + display: none; +} + +.small-bar { + display: none; +} + +.task-actions { + display: none; +} + +#bar { + display: none; +} + +#taskviewcontainer { + border: none; +} + +#taskviewcontainer img { + display: none; +} + +#tasklist { + list-style-type: decimal; + list-style-position: outside; +} + +#tasklist li { + padding-left: 0px; + margin-left: 30px; + border-bottom: none; + padding-bottom: 8px; +} + +div.task-note-block { + border-left: 1px solid #777777; + background: none; + padding-left: 5px; + margin-top: 5px; + padding-top: 0px; + font-size: 9pt; + color: #333333; +} + +.task-middle { + margin-left: 0px; + margin-right: 3px; +} + +.task-left { + display: none; +} + +.task-date { + white-space: nowrap; + margin-left: 10px; +} + +#tasklist li.today, #tasklist li.past { + background-color: #ffffff; + border-color: #dedede; +} + +.task-prio { + font-weight: bold; +} + +li.task-completed { + opacity: 1; +} + +#footer_content { + border-top: 1px solid #777777; + background: none; +} + +#footer_content a { + text-decoration: none; + color: #000000; +} + +#tagcloudbtn { + display: none; +} + +.mtt-notes-showhide { + display: none; +} + +#taskview img { + display: none; +} \ No newline at end of file diff --git a/public/style.css b/public/style.css new file mode 100755 index 0000000..ec22136 --- /dev/null +++ b/public/style.css @@ -0,0 +1,1358 @@ +/* + This file is a part of myTinyTodo. + Copyright 2009-2010 Max Pozdeev +*/ + +/* default style */ + +html { + height: 100%; + overflow-y: scroll; +} + +body { + margin: 0px; + padding: 0px; + height: 100%; + min-height: 100%; + background-color: #fff; + font-family: arial; + font-size: 10pt; +} + +#wrapper { + margin: 0px auto; + max-width: 950px; + height: 100%; +} + +#container { + height: auto !important; + height: 100%; + min-height: 100%; +} + +#mtt_body { + padding: 8px; + padding-bottom: 16px; +} + +td, th, input, textarea, select { + font-family: arial; + font-size: 1em; +} + +form { + display: inline; +} + +h2, h3 { + margin: 0; +} + +h2 { + font-size: 1.5em; + float: left; + padding-right: 10px; + background-color: #ffffff; +} + +h3 { + border-bottom: 2px solid #B5D5FF; + margin-bottom: 10px; + padding: 6px 0; + font-size: 1.1em; +} + +#page_tasks h3 { + padding-left: 4px; + padding-right: 4px; +} + +a { + color: #0000ff; + cursor: pointer; + text-decoration: underline; +} + +#space { + height: 30px; +} + +#footer { + height: 30px; + margin-top: -30px; +} + +#footer_content { + background-color: #b5d5ff; + padding: 5px; + font-size: 0.8em; +} + +#footer_content a { + color: #000000; +} + +#bar { + border-bottom: 1px solid #b5d5ff; + padding-bottom: 5px; + height: 15px; +} + +.bar-menu { + float: right; +} + +.nodecor { + text-decoration: none; +} + +#bar_logout { + display: none; +} + +#bar_auth { + display: none; +} + +#authform { + overflow: hidden; + z-index: 100; + background-color: #f9f9f9; + border: 1px solid #cccccc; + padding: 5px; + width: 160px; +} + +#authform div { + padding: 2px 0px; +} + +#authform div.h { + font-weight: bold; +} + +#loading { + float: left; + padding-top: 5px; + background-color: #ffffff; + display: none; + padding-right: 6px; + width: 16px; + height: 16px; + background: url(images/loading1.gif) no-repeat; +} + +#msg { + float: left; +} + +#msg .msg-text { + padding: 1px 4px; + font-weight: bold; + cursor: pointer; +} + +#msg .msg-details { + padding: 1px 4px; + background-color: #fff; + display: none; + max-width: 700px; +} + +#msg.mtt-error .msg-text { + background-color: #ff3333; +} + +#msg.mtt-error .msg-details { + border: 1px solid #ff3333; +} + +#msg.mtt-info .msg-text { + background-color: #EFC300; +} + +#msg.mtt-info .msg-details { + border: 1px solid #EFC300; +} + +.mtt-tabs { + list-style: none; + padding: 0; + margin: 0; +} + +.mtt-tabs li { + margin: 1px 3px 0 0; + float: left; + border-left: 1px solid #ededed; + background: #fbfbfb url(images/tab_hover.gif) no-repeat top right; +} + +.mtt-tab a { + position: relative; + margin: 0; + font-size: 0.9em; + font-weight: bold; + text-decoration: none; + text-align: center; + white-space: nowrap; + color: #444444; + display: block; + height: 21px; + padding: 6px 6px 0px 2px; + outline: none; + vertical-align: top; +} + +.mtt-tab a span { + display: inline-block; + min-width: 75px; + max-width: 195px; + cursor: pointer; + padding: 0; + overflow: hidden; +} + +.mtt-tab .list-action { + display: none; + float: left; + position: absolute; + top: 6px; + right: 5px; + width: 15px; + height: 15px; + background: transparent url(images/icons.gif) 0 0 no-repeat; + cursor: pointer; +} + +.mtt-tab .list-action:hover, .mtt-tab .list-action.mtt-menu-button-active { + background-position: -16px 0; +} + +.mtt-tab.mtt-tabs-selected span { + margin-right: 16px; +} + +.mtt-tab.mtt-tabs-selected .list-action { + display: block; +} + +.mtt-tab.mtt-tabs-selected { + background: #ededed url(images/corner_right.gif) no-repeat top right; + border-left: 1px solid #ededed; +} + +.mtt-tabs li:hover a { + color: #888888; +} + +.mtt-tabs.mtt-tabs-only-one li { + display: none; +} + +.mtt-tabs.mtt-tabs-only-one li.mtt-tabs-selected { + display: block; +} + +#mtt_body.readonly li.mtt-tabs-selected span { + margin-right: 0; +} + +.mtt-tabs-hidden { + display: none; +} + +.mtt-tabs-alltasks { + margin: 1px 3px 0 3px; + float: right; + border-right: 1px solid #ededed; + background: #fbfbfb url(images/tab_hover.gif) no-repeat top left; +} + +.mtt-tabs-alltasks.mtt-tab a { + padding: 6px 2px 0px 6px; +} + +.mtt-tabs-alltasks.mtt-tab.mtt-tabs-selected { + border-left: none; + border-right: 1px solid #ededed; + background: #ededed url(images/corner_left.gif) no-repeat top left; +} + +#tabs_buttons { + float: right; + padding-top: 4px; + padding-bottom: 2px; + padding-left: 2px; + padding-right: 2px; + border: 1px solid #ededed; + border-bottom: none; + margin-top: 1px; + -moz-border-radius-topright: 5px; + -webkit-border-top-right-radius: 5px; + border-top-right-radius: 5px; +} + +.mtt-tabs-button { + float: left; + font-size: 0.9em; + padding: 1px; /* makes button bigger */ + border: 1px solid transparent; /* preallocate space for :hover border */ +} + +.mtt-tabs-button span { + display: block; + width: 16px; + height: 16px; +} + +.mtt-tabs-button:hover, .mtt-tabs-button.mtt-menu-button-active { + border: 1px solid #ccc; + -moz-border-radius: 3px; + -webkit-border-radius: 3px; + border-radius: 3px; +} + +.mtt-tabs-add-button { + float: left; + margin-top: 1px; + padding: 6px 2px 0px 2px; + font-size: 0.9em; + height: 21px; + border-left: 1px solid #ededed; + background: #fbfbfb url(images/tab_hover.gif) no-repeat top right; +} + +.mtt-tabs-add-button:hover { + cursor: pointer; +} + +.mtt-tabs-add-button > span { + display: block; + width: 16px; + height: 16px; + background: url(images/buttons.png) 0 0 no-repeat; +} + +.mtt-tabs-add-button:hover > span { + background-position: -16px 0; +} + +#mtt_body.readonly .mtt-tabs-add-button { + display: none; +} + +.mtt-tabs-select-button > span { + background: url(images/icons.gif) -64px 0 no-repeat; +} + +.mtt-tabs-select-button:hover > span, .mtt-tabs-select-button.mtt-menu-button-active > span { + background-position: -80px 0; +} + +#mtt_body.no-lists #toolbar > * { + visibility: hidden; +} + +.mtt-htabs { + clear: both; + padding: 8px; + border-bottom: 2px solid #DEDEDE; + background: #ededed; +} + +.mtt-img-button { + width: 16px; + height: 16px; + padding: 2px; + border: 1px solid transparent; + display: inline-block; +} + +.mtt-img-button:hover { + border: 1px solid #ccc; + -moz-border-radius: 3px; + -webkit-border-radius: 3px; + border-radius: 3px; +} + +.mtt-img-button span { + display: inline-block; + width: 16px; + height: 16px; +} + +.arrdown { + display: inline-block; + height: 7px; + width: 9px; + background: url(images/arrdown.gif); +} + +.arrdown2 { + display: inline-block; + height: 7px; + width: 7px; + background: url(images/arrdown2.gif); +} + +/* Quick Task Add */ + +.mtt-taskbox td.mtt-tb-cell { + padding: 0px; + width: 450px; +} + +.mtt-tb-c { + position: relative; + padding-left: 22px; /*input padding+border*/ +} + +#task { + color: #444444; + background: #fff; + height: 1.35em; + padding: 2px; + padding-right: 18px; + border: 1px inset #F0F0F0; + width: 100%; + margin-left: -22px; +} + +#task_placeholder span { + display: none; + color: #ccc; + position: absolute; + left: 0; + top: 0; + height: 1.35em; + line-height: 1.35em; + padding: 3px; /*input top and left padding+border*/ +} + +#task_placeholder.placeholding span { + display: inline-block; +} + +.mtt-taskbox-icon { + width: 16px; + height: 16px; + position: absolute; + top: 50%; + margin-top: -8px; +} + +.mtt-taskbox-icon.mtt-icon-submittask { + background: url(images/mzl.png) 0px -32px no-repeat; + right: 4px; +} + +#newtask_adv span { + background: url(images/buttons.png) 0 -48px no-repeat; +} + +#newtask_adv:hover span { + background-position: -16px -48px; +} + +#mtt_body.show-all-tasks #htab_newtask, #mtt_body.readonly #htab_newtask { + display: none; +} + +/* Live Search */ +#htab_search { + float: right; +} + +#search { + color: #444444; + background: #fff; + height: 1.35em; + padding: 2px 18px; + width: 100%; + margin-left: -38px; /*padding+border*/ + border: 1px inset #F0F0F0; + -moz-border-radius: 10px; + -webkit-border-radius: 10px; + border-radius: 10px; +} + +#search_close { + display: none; +} + +.mtt-searchbox td { + padding: 0px; + width: 180px; +} + +.mtt-searchbox-c { + position: relative; + padding-left: 38px; /*input padding+border*/ +} + +.mtt-searchbox-icon { + width: 16px; + height: 16px; + position: absolute; + top: 50%; + margin-top: -8px; +} + +.mtt-searchbox-icon.mtt-icon-search { + background: url(images/mzl.png) 0px -16px no-repeat; + left: 4px; +} + +.mtt-searchbox-icon.mtt-icon-cancelsearch { + background: url(images/mzl.png) 0px 0px no-repeat; + right: 4px; +} + +#searchbar { + font-size: 1em; + font-weight: normal; + display: none; + margin-top: 5px; +} + +#searchbarkeyword { + font-weight: bold; +} + +/* */ +#mtt_body.no-lists #page_tasks h3 > * { + visibility: hidden; +} + +.mtt-notes-showhide { + font-size: 0.8em; + font-weight: normal; + margin-left: 2px; + margin-right: 2px; +} + +.mtt-notes-showhide a { + text-decoration: none; + border-bottom: 1px dotted; +} + +#mtt_filters { + font-size: 0.8em; + font-weight: normal; +} + +.tag-filter { + margin-left: 3px; + margin-right: 3px; +} + +.tag-filter-exclude { + text-decoration: line-through; +} + +.mtt-filter-header { + font-weight: bold; + margin-right: .33em; +} + +.mtt-filter-close { + cursor: pointer; + position: relative; + top: 2px; + margin-left: 3px; + display: inline-block; + width: 10px; + height: 10px; + background: url(images/closetag.gif) 0 0 no-repeat; +} + +.task-left { + float: left; +} + +.task-toggle { + visibility: hidden; + margin-top: 2px; + cursor: pointer; + width: 15px; + height: 15px; + float: left; + background: url(images/icons.gif) -64px -16px no-repeat; +} + +li.task-has-note .task-toggle { + visibility: visible; +} + +li.task-expanded .task-toggle { + background-position: -80px -16px; +} + +.task-middle { + margin-left: 40px; + margin-right: 20px; +} + +#tasklist { + list-style-type: none; + margin: 0; + padding: 0; +} + +#tasklist li { + padding: 6px 2px 6px 6px; + border-bottom: 1px solid #DEDEDE; + min-height: 18px; + margin-bottom: 1px; + background-color: #fff; +} + +#tasklist li:hover { + background-color: #f6f6f6; +} + +.task-actions { + float: right; + width: 20px; + text-align: right; +} + +.task-date { + color: #999999; + font-size: 0.8em; + margin-left: 4px; + display: none; +} + +.task-date-completed { + color: #999999; + display: none; + margin-left: 5px; +} + +.show-inline-date .task-date { + display: inline; +} + +.show-inline-date li.task-completed .task-date-completed { + display: inline; +} + +.show-inline-date li.task-completed .task-date { + display: none; +} + +.task-through { + overflow: hidden; +} + +.task-through-right { + float: right; +} + +.task-title a { + color: #000000; +} + +.task-title a:hover { + color: #af0000; +} + +#mtt_body.readonly #tasklist li .task-actions { + display: none; +} + +.task-listname { + background-color: #eee; + color: #555; + padding: 0px 3px; +} + +.task-tags { + padding: 0px 2px; +} + +.task-tags .tag { + font-size: 0.8em; + font-weight: bold; + color: #333333; + text-decoration: underline; +} + +.task-tags .tag:hover { +} + +.duedate { + color: #333333; + padding: 0px; + padding-left: 1px; + margin-left: 5px; +} + +li.task-completed .duedate { /*font-size:0.8em;*/ + display: none; +} + +#tasklist li.soon .duedate { + color: #008000; +} + +#tasklist li.today .duedate { + color: #FF0000; +} + +#tasklist li.past .duedate { + color: #A52A2A; +} + +li.task-completed .task-middle { + color: #777777; +} + +li.task-completed .task-through { + text-decoration: line-through; +} + +li.task-completed .task-title a { + color: #777777; +} + +#tasklist li.task-completed { + opacity: 0.6; + filter: alpha(opacity=60); +} + +#tasklist li.task-completed:hover { + opacity: 1.0; + filter: alpha(opacity=100); +} + +#tasklist li.not-in-tagpreview { + opacity: 0.1; + filter: alpha(opacity=10); +} + +#tasklist li.mtt-task-placeholder { + min-height: 0px; + padding: 0px; + height: 18px; + line-height: 18px; + background-color: #ddd; + border: 1px solid #aaa; + -moz-border-radius: 5px; + -webkit-border-radius: 5px; + border-radius: 5px; +} + +a.taskactionbtn { + display: block; + float: right; + height: 15px; + width: 15px; + text-decoration: none; + background: url(images/icons.gif) 0 0 no-repeat; + display: none; +} + +li:hover a.taskactionbtn, a.taskactionbtn.mtt-menu-button-active { + background-position: -16px 0; + display: block; +} + +#tasklist.filter-past li, #tasklist.filter-today li, #tasklist.filter-soon li { + display: none; +} + +#tasklist.filter-past li.past, #tasklist.filter-today li.today, #tasklist.filter-soon li.soon { + display: block; +} + +#tasklist.filter-past li.task-completed, #tasklist.filter-today li.task-completed, #tasklist.filter-soon li.task-completed { + display: none; +} + +.task-note-block { + margin-left: 2px; + color: #777777; + background: url(images/page_white_text.png) left 2px no-repeat; + padding-left: 19px; + padding-top: 2px; + min-height: 16px; + margin-top: 2px; + display: none; +} + +li.task-expanded .task-note-block { + display: block; +} + +li.task-completed .task-note-block .task-note { + text-decoration: line-through; +} + +.task-note-area { + display: none; + margin-bottom: 5px; +} + +.task-note-area textarea { + color: #999999; + width: 100%; + display: block; + height: 65px; +} + +.task-note-actions { + font-size: 0.8em; +} + +.hidden { + display: none; +} + +.invisible { + visibility: hidden; +} + +.in500 { + width: 500px; + color: #444444; +} + +.in100 { + width: 100px; + color: #444444; +} + +.task-note span a { + color: #777777; +} + +.task-note span a:hover { + color: #af0000; +} + +.task-prio { + padding-left: 2px; + padding-right: 2px; + margin-left: 0px; + margin-right: 5px; + cursor: default; +} + +.prio-neg { + background-color: #3377ff; + color: #ffffff; +} + +.prio-pos { + background-color: #ff3333; + color: #ffffff; +} + +.prio-pos-1 { + background-color: #ff7700; + color: #ffffff; +} + +.prio-zero { /*background-color:#dedede;*/ +} + +.task-prio.prio-zero { + display: none; +} + +.form-row { + margin-top: 8px; +} + +.form-row .h { + font-weight: bold; + color: #333333; +} + +.form-row-short-end { + clear: both; +} + +#page_taskedit .form-row .in500 { + width: 99%; +} + +#page_taskedit .form-row textarea.in500 { + height: 200px; /*resize:none;*/ +} + +#page_taskedit .form-row-short { + float: left; + margin-right: 12px; +} + +#page_taskedit .form-bottom-buttons { + text-align: center; +} + +#alltags .tag { + font-weight: bold; + color: #333333; +} + +#alltags .tag:hover { + background-color: #999988; + color: white; +} + +.alltags-cell { + width: 1%; + white-space: nowrap; + padding-left: 5px; +} + +#page_taskedit.mtt-inadd .mtt-inedit { + display: none; +} + +#page_taskedit.mtt-inedit .mtt-inadd { + display: none; +} + +#taskedit-date { + font-size: 1em; + font-weight: normal; + display: inline; + color: #777; + margin-left: 8px; +} + +a.mtt-back-button { + font-size: 0.8em; +} + +/* autocomplete */ +.ac_results { + padding: 0px; + border: 1px solid #cccccc; + background-color: #f9f9f9; + overflow: hidden; + z-index: 99999; + -moz-box-shadow: 1px 2px 5px rgba(0, 0, 0, 0.5); + -webkit-box-shadow: 1px 2px 5px rgba(0, 0, 0, 0.5); +} + +.ac_results ul { + width: 100%; + list-style-position: outside; + list-style: none; + padding: 0; + margin: 0; +} + +.ac_results li { + margin: 0px; + padding: 2px 5px; + cursor: default; + display: block; + line-height: 16px; + overflow: hidden; +} + +.ac_over { + background-color: #316AC5; + color: white; +} + +#priopopup { + overflow: hidden; + z-index: 100; + background-color: #f9f9f9; + border: 1px solid #cccccc; + padding: 5px; +} + +#priopopup span { + cursor: pointer; + border: 1px solid #f9f9f9; +} + +#priopopup .prio-zero:hover { + border-color: #dedede; +} + +#priopopup .prio-neg:hover { + border-color: #3377ff; +} + +#priopopup .prio-pos:hover { + border-color: #ff3333; +} + +#priopopup .prio-pos-1:hover { + border-color: #ff7700; +} + +#tagcloudbtn { + margin-right: 2px; + font-size: 0.8em; + font-weight: normal; + padding: 2px; + float: right; +} + +#mtt_body.show-all-tasks #tagcloudbtn { + display: none; +} + +#tagcloudload { + display: none; + height: 24px; + background: url(images/loading1_24.gif) center no-repeat; +} + +#tagcloud { + overflow: hidden; + z-index: 100; + background-color: #f9f9f9; + border: 1px solid #cccccc; + padding: 5px; + width: 100%; + max-width: 450px; + margin: 0px 7px 7px 7px; + text-align: center; + -moz-box-shadow: 1px 2px 5px rgba(0, 0, 0, 0.5); + -webkit-box-shadow: 1px 2px 5px rgba(0, 0, 0, 0.5); +} + +#tagcloud .tag { + margin: 1px 0px; + padding: 2px; + line-height: 140%; + color: black; +} + +#tagcloud .tag:hover { + background-color: #999988; + color: white; +} + +#tagcloud .w0 { + font-size: 80%; +} + +#tagcloud .w1 { + font-size: 90%; +} + +#tagcloud .w2 { + font-size: 100%; +} + +#tagcloud .w3 { + font-size: 110%; +} + +#tagcloud .w4 { + font-size: 120%; +} + +#tagcloud .w5 { + font-size: 130%; +} + +#tagcloud .w6 { + font-size: 140%; +} + +#tagcloud .w7 { + font-size: 150%; +} + +#tagcloud .w8 { + font-size: 160%; +} + +#tagcloud .w9 { + font-size: 170%; +} + +#tagcloudcancel { + float: right; +} + +#tagcloudcancel span { + background: url(images/buttons.png) 0 -32px no-repeat; +} + +#tagcloudcancel span:hover { + background-position: -16px -32px; +} + +#taskview { + padding: 2px; +} + +.ui-datepicker { + width: 190px; + z-index: 202; + border: 1px solid #cccccc; + background: #ffffff; + display: none; + padding: 2px; + -moz-box-shadow: 1px 2px 5px rgba(0, 0, 0, 0.5); + -webkit-box-shadow: 1px 2px 5px rgba(0, 0, 0, 0.5); + box-shadow: 1px 2px 5px rgba(0, 0, 0, 0.5); + -moz-border-radius: 5px; + -webkit-border-radius: 5px; + border-radius: 5px; +} + +.ui-datepicker-trigger { + cursor: pointer; + vertical-align: text-bottom; + margin-left: 1px; +} + +.ui-datepicker-calendar { + width: 100%; + border-collapse: collapse; +} + +.ui-datepicker-calendar thead th { + text-align: center; + padding: 0px; + font-size: 0.9em; +} + +.ui-datepicker-calendar tbody td { + text-align: right; + padding: 1px; +} + +.ui-datepicker-calendar td a { + display: block; + text-decoration: none; + color: #444444; + border: 1px solid #cccccc; + background-color: #f9f9f9; + color: #111; + padding: 1px; +} + +.ui-datepicker-calendar td.ui-datepicker-current-day a { + background-color: #EAF5FF; + color: #222222; + border-color: #5980FF; +} + +.ui-datepicker-calendar td.ui-datepicker-today a { + color: #fff; + background-color: #ccc; +} + +.ui-datepicker-calendar td a:hover { + border-color: #5980FF; +} + +.ui-datepicker-header { + padding: 3px 0px; +} + +.ui-datepicker-prev { + position: absolute; + left: 2px; + height: 20px; + text-decoration: none; +} + +.ui-datepicker-next { + position: absolute; + right: 2px; + height: 20px; + text-decoration: none; +} + +.ui-datepicker-title { + text-align: center; + line-height: 20px; +} + +.ui-icon { + width: 16px; + height: 16px; + text-indent: -99999px; + overflow: hidden; +} + +.ui-datepicker .ui-icon-circle-triangle-w { + display: block; + position: absolute; + top: 50%; + margin-top: -8px; + left: 50%; + background: url(images/icons.gif) -48px -16px no-repeat; +} + +.ui-datepicker .ui-icon-circle-triangle-e { + display: block; + position: absolute; + top: 50%; + margin-top: -8px; + right: 50%; + background: url(images/icons.gif) -32px -16px no-repeat; +} + +.mtt-menu-button { + -moz-user-select: none; + -webkit-user-select: none; + cursor: pointer; + border: 1px solid transparent; +} + +.mtt-menu-button:hover, .mtt-menu-button.mtt-menu-button-active { + border: 1px solid #ccc; + -moz-border-radius: 3px; + -webkit-border-radius: 3px; + border-radius: 3px; +} + +.mtt-menu-container { + overflow: hidden; + z-index: 100; + background-color: #f9f9f9; + border: 1px solid #cccccc; + padding: 2px 0px; + -moz-box-shadow: 1px 2px 5px rgba(0, 0, 0, 0.5); + -webkit-box-shadow: 1px 2px 5px rgba(0, 0, 0, 0.5); + box-shadow: 1px 2px 5px rgba(0, 0, 0, 0.5); + -moz-border-radius: 5px; + -webkit-border-radius: 5px; + border-radius: 5px; + margin: 0px 7px 7px 0px; /* for shadows */ +} + +.mtt-menu-container ul { + list-style: none; + padding: 0; + margin: 0; +} + +.mtt-menu-container li { + margin: 1px 0px; + cursor: default; + color: #000; + white-space: nowrap; + padding: 0.15em 0px; + padding-left: 24px; + padding-right: 18px; + position: relative; +} + +.mtt-menu-container li:hover, .mtt-menu-container li.mtt-menu-item-active { + background-color: #316AC5; + color: white; + background: -moz-linear-gradient(#5373fc, #3157f4); + background: -webkit-gradient(linear, left top, left bottom, from(#5373fc), to(#3157f4)); +} + +.mtt-menu-container li.mtt-item-disabled, .mtt-menu-container li.mtt-item-disabled a { + color: #ACA899; +} + +.mtt-menu-container a { + display: block; + cursor: default; + text-decoration: none; + outline: none; + color: #000; +} + +.mtt-menu-container li:hover a { + color: white; +} + +.mtt-menu-container li.mtt-menu-delimiter { + height: 0px; + line-height: 0; + border-bottom: 1px solid #cccccc; + margin: 2px -1px; + padding: 0px; + font-size: 0px; +} + +.mtt-menu-container .menu-icon { + width: 16px; + height: 16px; + position: absolute; + left: 4px; + top: 50%; + margin-top: -8px; +} + +li.mtt-item-checked .menu-icon { + background: url(images/icons.gif) -16px -16px; +} + +li.mtt-menu-indicator .submenu-icon { + position: absolute; + right: 2px; + top: 50%; + margin-top: -8px; + width: 16px; + height: 16px; + background: url(images/icons.gif) -32px -16px no-repeat; +} + +li.mtt-item-hidden { + display: none; +} + +#slmenucontainer li.mtt-list-hidden a { + font-style: italic; +} + +#cmenulistscontainer li.mtt-list-hidden { + font-style: italic; +} + +#btnRssFeed .menu-icon { + background: url(images/buttons.png) -16px -64px no-repeat; +} + +#btnRssFeed.mtt-item-disabled .menu-icon { + background: url(images/buttons.png) 0px -64px no-repeat; +} + +.mtt-settings-table { + width: 100%; + border-collapse: collapse; +} + +.mtt-settings-table th, .mtt-settings-table td { + border-bottom: 1px solid #dedede; + padding: 8px; + vertical-align: top; +} + +.mtt-settings-table .form-buttons { + border-bottom: none; + text-align: center; +} + +.mtt-settings-table th { + text-align: left; + width: 210px; + padding-left: 8px; +} + +.mtt-settings-table .descr { + font-size: 0.8em; + font-weight: normal; + color: #222; +} + +.mtt-settings-table .in350 { + min-width: 350px; +} +body { filter: invert(80%); background-color: #222; } diff --git a/public/style_rtl.css b/public/style_rtl.css new file mode 100755 index 0000000..713c752 --- /dev/null +++ b/public/style_rtl.css @@ -0,0 +1,137 @@ +body { + direction: rtl; +} + +h2 { + float: right; + padding-left: 10px; + padding-right: 0px; +} + +.bar-menu { + float: left; +} + +#loading { + float: right; +} + +#lists .mtt-tabs { + float: right; +} + +.mtt-tabs li { + float: right; + margin: 1px 0 0 3px; + border-left: none; + border-right: 1px solid #ededed; + background: #fbfbfb url(images/tab_hover.gif) no-repeat top left; +} + +.mtt-tabs li.mtt-tabs-selected { + border-left: none; + border-right: 1px solid #ededed; + background: #ededed url(images/corner_left.gif) no-repeat top left; +} + +#tabs_buttons { + float: left; +} + +.mtt-tabs-button { + float: right; +} + +#tagcloudbtn { + float: left; +} + +#htab_search { + float: left; +} + +#task { + padding: 2px 2px 2px 18px; +} + +#task_placeholder span { + right: 0px; + left: auto; +} + +.mtt-taskbox-icon.mtt-icon-submittask { + left: 4px; + right: auto; +} + +.mtt-searchbox-icon.mtt-icon-search { + right: 4px; + left: auto; +} + +.mtt-searchbox-icon.mtt-icon-cancelsearch { + left: 4px; + right: auto; +} + +.task-actions { + float: left; + text-align: left; + width: 15px; +} + +.task-left { + float: right; +} + +.task-toggle { + float: right; +} + +.task-middle { + margin-right: 40px; + margin-left: 25px; +} + +.task-date { + margin-right: 4px; +} + +.duedate { + float: left; + margin-left: 0; + margin-right: 5px; +} + +.duedate-arrow { + display: none; +} + +.duedate:before { + content: '\2190'; +} + +.task-through-right { + float: left; +} + +#taskedit-date { + float: left; +} + +#page_taskedit .form-row-short { + float: right; + margin-left: 12px; + margin-right: 0; +} + +.task-prio { + float: right; + margin-left: 4px; + margin-right: 0; +} + +.alltags-cell { + padding-left: 0; + padding-right: 5px; +} \ No newline at end of file diff --git a/server/ajax/ajax.go b/server/ajax/ajax.go new file mode 100644 index 0000000..75b1a92 --- /dev/null +++ b/server/ajax/ajax.go @@ -0,0 +1,73 @@ +package ajax + +import ( + "local/storage" + "local/todo-server/config" + "net/http" +) + +type Ajax struct { + DB storage.DB +} + +func New() (*Ajax, error) { + db, err := storage.New(storage.TypeFromString(config.StoreType), config.StoreAddr, config.StoreUser, config.StorePass) + return &Ajax{ + DB: db, + }, err +} + +func (a *Ajax) HandleAjax(w http.ResponseWriter, r *http.Request) { + params := r.URL.Query() + var foo func(http.ResponseWriter, *http.Request) error + if v := params.Get("loadLists"); v != "" { + foo = a.loadLists + } else if v := params.Get("loadTasks"); v != "" { + foo = a.loadTasks + } else if v := params.Get("newTask"); v != "" { + foo = a.newTask + } else if v := params.Get("fullNewTask"); v != "" { + foo = a.newTask + } else if v := params.Get("deleteTask"); v != "" { + foo = a.deleteTask + } else if v := params.Get("completeTask"); v != "" { + foo = a.completeTask + } else if v := params.Get("editNote"); v != "" { + foo = a.editNote + } else if v := params.Get("editTask"); v != "" { + foo = a.editTask + } else if v := params.Get("changeOrder"); v != "" { + foo = a.changeOrder + } else if v := params.Get("suggestTags"); v != "" { + foo = a.suggestTags + } else if v := params.Get("setPrio"); v != "" { + foo = a.setPrio + } else if v := params.Get("tagCloud"); v != "" { + foo = a.tagCloud + } else if v := params.Get("addList"); v != "" { + foo = a.addList + } else if v := params.Get("renameList"); v != "" { + foo = a.renameList + } else if v := params.Get("deleteList"); v != "" { + foo = a.deleteList + } else if v := params.Get("setSort"); v != "" { + foo = a.setSort + } else if v := params.Get("publishList"); v != "" { + foo = a.publishList + } else if v := params.Get("moveTask"); v != "" { + foo = a.moveTask + } else if v := params.Get("changeListOrder"); v != "" { + foo = a.changeListOrder + } else if v := params.Get("parseTaskStr"); v != "" { + foo = a.parseTaskStr + } else if v := params.Get("clearCompletedInList"); v != "" { + foo = a.clearCompletedInList + } else if v := params.Get("setShowNotesInList"); v != "" { + foo = a.setShowNotesInList + } else if v := params.Get("setHideList"); v != "" { + foo = a.setHideList + } + if err := foo(w, r); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } +} diff --git a/server/ajax/ajax_test.go b/server/ajax/ajax_test.go new file mode 100644 index 0000000..4e28680 --- /dev/null +++ b/server/ajax/ajax_test.go @@ -0,0 +1,20 @@ +package ajax + +import ( + "local/todo-server/config" + "testing" +) + +func TestNew(t *testing.T) { + mockAjax() +} + +func mockAjax() *Ajax { + config.StoreType = "map" + ajax, err := New() + if err != nil { + panic(err) + } + ajax.storageSetCur("list") + return ajax +} diff --git a/server/ajax/form/form.go b/server/ajax/form/form.go new file mode 100644 index 0000000..00e284b --- /dev/null +++ b/server/ajax/form/form.go @@ -0,0 +1,60 @@ +package form + +import ( + "bytes" + "encoding/json" + "html" + "io" + "io/ioutil" + "net/http" + "strconv" + "strings" + "time" +) + +type readCloser struct { + io.Reader +} + +func (rc readCloser) Close() error { + return nil +} + +func Get(r *http.Request, k string) string { + s := r.FormValue(k) + if s == "" { + b, _ := ioutil.ReadAll(r.Body) + var m map[string]json.RawMessage + if err := json.Unmarshal(b, &m); err != nil { + return "" + } + v, _ := m[k] + s = strings.TrimPrefix(strings.TrimSuffix(string(v), `"`), `"`) + r.Body = readCloser{bytes.NewReader(b)} + } + s = html.UnescapeString(s) + s = strings.ReplaceAll(s, "\r", "") + return s +} + +func ToInt(s string) int { + v, _ := strconv.Atoi(s) + return v +} + +func ToStrArr(k string) []string { + arr := strings.Split(k, ",") + outArr := []string{} + for i := range arr { + s := strings.TrimSpace(arr[i]) + if len(s) > 0 { + outArr = append(outArr, s) + } + } + return outArr +} + +func ToTime(s string) time.Time { + v, _ := time.Parse("2006-01-02 15:04:05", s) + return v +} diff --git a/server/ajax/form/form_test.go b/server/ajax/form/form_test.go new file mode 100644 index 0000000..1535e7a --- /dev/null +++ b/server/ajax/form/form_test.go @@ -0,0 +1,132 @@ +package form + +import ( + "net/http" + "net/http/httptest" + "strings" + "testing" +) + +func TestGet(t *testing.T) { + r := testReq() + if v := Get(r, "a"); v != "b" { + t.Error(v) + } + if v := Get(r, "c"); v != "4" { + t.Error(v) + } + if v := Get(r, "d"); v != "e, f,g" { + t.Error(v) + } +} + +func TestToStrArr(t *testing.T) { + cases := []struct { + in string + out int + }{ + { + in: "4", + out: 1, + }, + { + in: "a,b,c", + out: 3, + }, + { + in: " a, b, c ", + out: 3, + }, + { + in: "a,b, c", + out: 3, + }, + { + in: "a,b", + out: 2, + }, + { + in: "a,", + out: 1, + }, + { + in: "a", + out: 1, + }, + { + in: "", + out: 0, + }, + } + + for _, c := range cases { + if len(ToStrArr(c.in)) != c.out { + t.Error(c) + } + } +} + +func TestToTime(t *testing.T) { + cases := []struct { + in string + out string + }{ + { + in: "2001-02-03 04:05:06", + out: "2001-02-03 04:05:06", + }, + { + in: "5", + out: "0001-01-01 00:00:00", + }, + } + + for _, c := range cases { + time := ToTime(c.in) + if v := time.Format("2006-01-02 15:04:05"); v != c.out { + t.Error(c, v) + } + } +} + +func TestToInt(t *testing.T) { + cases := []struct { + in string + out int + }{ + { + in: "4", + out: 4, + }, + { + in: "a", + out: 0, + }, + { + in: "", + out: 0, + }, + { + in: "-1", + out: -1, + }, + { + in: "5", + out: 5, + }, + } + + for _, c := range cases { + if ToInt(c.in) != c.out { + t.Error(c) + } + } +} + +func testReq() *http.Request { + return httptest.NewRequest("POST", "/path/to", strings.NewReader(`{ + "a": "b", + "c": 4, + "d": "e, f,g" + }`)) +} diff --git a/server/ajax/list.go b/server/ajax/list.go new file mode 100644 index 0000000..c3cbe64 --- /dev/null +++ b/server/ajax/list.go @@ -0,0 +1,52 @@ +package ajax + +import ( + "errors" + "net/http" +) + +type List struct{} + +func (a *Ajax) loadLists(w http.ResponseWriter, r *http.Request) error { + return errors.New("not impl") +} + +func (a *Ajax) changeOrder(w http.ResponseWriter, r *http.Request) error { + return errors.New("not impl") +} + +func (a *Ajax) addList(w http.ResponseWriter, r *http.Request) error { + return errors.New("not impl") +} + +func (a *Ajax) renameList(w http.ResponseWriter, r *http.Request) error { + return errors.New("not impl") +} + +func (a *Ajax) deleteList(w http.ResponseWriter, r *http.Request) error { + return errors.New("not impl") +} + +func (a *Ajax) setSort(w http.ResponseWriter, r *http.Request) error { + return errors.New("not impl") +} + +func (a *Ajax) publishList(w http.ResponseWriter, r *http.Request) error { + return errors.New("not impl") +} + +func (a *Ajax) changeListOrder(w http.ResponseWriter, r *http.Request) error { + return errors.New("not impl") +} + +func (a *Ajax) clearCompletedInList(w http.ResponseWriter, r *http.Request) error { + return errors.New("not impl") +} + +func (a *Ajax) setShowNotesInList(w http.ResponseWriter, r *http.Request) error { + return errors.New("not impl") +} + +func (a *Ajax) setHideList(w http.ResponseWriter, r *http.Request) error { + return errors.New("not impl") +} diff --git a/server/ajax/list/list.go b/server/ajax/list/list.go new file mode 100644 index 0000000..f8e8b57 --- /dev/null +++ b/server/ajax/list/list.go @@ -0,0 +1,56 @@ +package ajax + +import ( + "errors" + "net/http" +) + +type List struct{} + +func New(r *http.Request) (*List, error) { + return &List{}, errors.New("not impl") +} + +func (l *List) loadLists() error { + return errors.New("not impl") +} + +func (l *List) changeOrder() error { + return errors.New("not impl") +} + +func (l *List) addList() error { + return errors.New("not impl") +} + +func (l *List) renameList() error { + return errors.New("not impl") +} + +func (l *List) deleteList() error { + return errors.New("not impl") +} + +func (l *List) setSort() error { + return errors.New("not impl") +} + +func (l *List) publishList() error { + return errors.New("not impl") +} + +func (l *List) changeListOrder() error { + return errors.New("not impl") +} + +func (l *List) clearCompletedInList() error { + return errors.New("not impl") +} + +func (l *List) setShowNotesInList() error { + return errors.New("not impl") +} + +func (l *List) setHideList() error { + return errors.New("not impl") +} diff --git a/server/ajax/storage.go b/server/ajax/storage.go new file mode 100644 index 0000000..6542201 --- /dev/null +++ b/server/ajax/storage.go @@ -0,0 +1,115 @@ +package ajax + +import ( + "bytes" + "encoding/gob" + "local/todo-server/server/ajax/form" + "local/todo-server/server/ajax/task" + "net/http" + "path" + "strings" +) + +func (a *Ajax) Cur(r *http.Request) (string, string, []string) { + listID, _ := a.storageGetCur() + taskID := form.Get(r, "id") + tags, _ := a.storageGetCurTags() + return listID, taskID, tags +} + +func (a *Ajax) storageListTasks(listID string, filters ...func(t *task.Task) bool) ([]*task.Task, error) { + results, err := a.DB.List(nil, listID+"/", listID+"/}") + if err != nil { + return nil, err + } + tasks := []*task.Task{} + for _, result := range results { + taskID := strings.TrimPrefix(result, listID+"/") + task, err := a.storageGetTask(listID, taskID) + if err != nil { + return nil, err + } + filtered := true + for _, f := range filters { + if !f(task) { + filtered = false + break + } + } + if filtered { + tasks = append(tasks, task) + } + } + return tasks, nil +} + +func (a *Ajax) storageGetTask(listID, taskID string) (*task.Task, error) { + var task task.Task + err := a.storageGet(path.Join(listID, taskID), &task) + return &task, err +} + +func (a *Ajax) storageSetTask(listID, taskID string, task *task.Task) error { + return a.storageSet(path.Join(listID, taskID), *task) +} + +func (a *Ajax) storageDelTask(listID, taskID string) error { + return a.storageDel(path.Join(listID, taskID)) +} + +func (a *Ajax) storageGetList(listID string) (*List, error) { + var list List + err := a.storageGet(listID, &list) + return &list, err +} + +func (a *Ajax) storageSetList(listID string, list *List) error { + return a.storageSet(listID, *list) +} + +func (a *Ajax) storageDelList(listID string) error { + return a.storageDel(listID) +} + +func (a *Ajax) storageSetCurTags(tags []string) error { + return a.storageSet("currentTags", tags) +} + +func (a *Ajax) storageGetCurTags() ([]string, error) { + var tags []string + err := a.storageGet("currentTags", &tags) + return tags, err +} + +func (a *Ajax) storageSetCur(listID string) error { + return a.storageSet("currentList", listID) +} + +func (a *Ajax) storageGetCur() (string, error) { + var listID string + err := a.storageGet("currentList", &listID) + return listID, err +} + +func (a *Ajax) storageSet(key string, value interface{}) error { + buff := bytes.NewBuffer(nil) + encoder := gob.NewEncoder(buff) + if err := encoder.Encode(value); err != nil { + return err + } + return a.DB.Set(key, buff.Bytes()) +} + +func (a *Ajax) storageGet(key string, value interface{}) error { + b, err := a.DB.Get(key) + if err != nil { + return err + } + buff := bytes.NewBuffer(b) + decoder := gob.NewDecoder(buff) + return decoder.Decode(value) +} + +func (a *Ajax) storageDel(key string) error { + return a.DB.Set(key, nil) +} diff --git a/server/ajax/tag.go b/server/ajax/tag.go new file mode 100644 index 0000000..744ec0d --- /dev/null +++ b/server/ajax/tag.go @@ -0,0 +1,14 @@ +package ajax + +import ( + "errors" + "net/http" +) + +func (a *Ajax) suggestTags(w http.ResponseWriter, r *http.Request) error { + return errors.New("not impl") +} + +func (a *Ajax) tagCloud(w http.ResponseWriter, r *http.Request) error { + return errors.New("not impl") +} diff --git a/server/ajax/task.go b/server/ajax/task.go new file mode 100644 index 0000000..6fd84aa --- /dev/null +++ b/server/ajax/task.go @@ -0,0 +1,128 @@ +package ajax + +import ( + "encoding/json" + "errors" + "fmt" + "local/todo-server/server/ajax/form" + "local/todo-server/server/ajax/task" + "net/http" + "strings" +) + +func (a *Ajax) loadTasks(w http.ResponseWriter, r *http.Request) error { + listID, _, _ := a.Cur(r) + filterComplete := func(t *task.Task) bool { + if form.Get(r, "compl") == "" { + return true + } + return t.Complete == (form.Get(r, "compl") != "0") + } + filterTags := func(t *task.Task) bool { + if form.Get(r, "t") == "" { + return true + } + whitelistTags := form.ToStrArr(form.Get(r, "t")) + if len(whitelistTags) == 0 { + return true + } + whitelistTagMap := make(map[string]struct{}) + for _, tag := range whitelistTags { + whitelistTagMap[tag] = struct{}{} + } + for _, tag := range t.Tags { + if _, ok := whitelistTagMap[tag]; ok { + return true + } + } + return false + } + filterSubstr := func(t *task.Task) bool { + substr := form.Get(r, "s") + return substr == "" || strings.Contains(fmt.Sprintf("%+v", t), substr) + } + tasks, err := a.storageListTasks(listID, filterComplete, filterTags, filterSubstr) + if err != nil { + return err + } + return json.NewEncoder(w).Encode(map[string]interface{}{"list": tasks}) +} + +func (a *Ajax) newTask(w http.ResponseWriter, r *http.Request) error { + listID, task, err := a.makeTask(r) + if err != nil { + return err + } + return a.storageSetTask(listID, task.UUID, task) +} + +func (a *Ajax) makeTask(r *http.Request) (string, *task.Task, error) { + listID, _, tags := a.Cur(r) + task, err := task.New(r) + if err != nil { + return "", nil, err + } + task.AppendTags(tags) + return listID, task, nil +} + +func (a *Ajax) deleteTask(w http.ResponseWriter, r *http.Request) error { + listID, taskID, _ := a.Cur(r) + return a.storageDelTask(listID, taskID) +} + +func (a *Ajax) completeTask(w http.ResponseWriter, r *http.Request) error { + listID, taskID, _ := a.Cur(r) + task, err := a.storageGetTask(listID, taskID) + if err != nil { + return err + } + task.SetComplete(form.Get(r, "compl") == "1") + return a.storageSetTask(listID, taskID, task) +} + +func (a *Ajax) editNote(w http.ResponseWriter, r *http.Request) error { + listID, taskID, _ := a.Cur(r) + task, err := a.storageGetTask(listID, taskID) + if err != nil { + return err + } + task.SetNote(form.Get(r, "note")) + return a.storageSetTask(listID, taskID, task) +} + +func (a *Ajax) editTask(w http.ResponseWriter, r *http.Request) error { + listID, task, err := a.makeTask(r) + if err != nil { + return err + } + _, taskID, _ := a.Cur(r) + task.UUID = taskID + task.ID = task.UUID + return a.storageSetTask(listID, task.UUID, task) +} + +func (a *Ajax) setPrio(w http.ResponseWriter, r *http.Request) error { + listID, taskID, _ := a.Cur(r) + task, err := a.storageGetTask(listID, taskID) + if err != nil { + return err + } + task.SetPrio(form.ToInt(form.Get(r, "prio"))) + return a.storageSetTask(listID, taskID, task) +} + +func (a *Ajax) moveTask(w http.ResponseWriter, r *http.Request) error { + listID, taskID, _ := a.Cur(r) + toList := form.Get(r, "to") + task, err := a.storageGetTask(listID, taskID) + if err != nil { + return err + } + a.storageSetTask(listID, taskID, nil) + return a.storageSetTask(toList, taskID, task) +} + +func (a *Ajax) parseTaskStr(w http.ResponseWriter, r *http.Request) error { + return errors.New("not impl") +} diff --git a/server/ajax/task/task.go b/server/ajax/task/task.go new file mode 100644 index 0000000..a2945e8 --- /dev/null +++ b/server/ajax/task/task.go @@ -0,0 +1,100 @@ +package task + +import ( + "errors" + "local/todo-server/server/ajax/form" + "net/http" + "regexp" + "strings" + "time" + + "github.com/google/uuid" +) + +type Task struct { + ID string + UUID string + Title string + Priority int + Tags []string + Created time.Time + Edited time.Time + + Completed time.Time + Complete bool + Note []string + Due time.Time +} + +func New(r *http.Request) (*Task, error) { + task := &Task{ + UUID: uuid.New().String(), + Title: form.Get(r, "title"), + Priority: form.ToInt(form.Get(r, "prio")), + Tags: form.ToStrArr(form.Get(r, "tags")), + Created: time.Now(), + Edited: time.Now(), + + Due: form.ToTime(form.Get(r, "duedate")), + } + task.ID = task.UUID + task.SetNote(form.Get(r, "note")) + return task, task.validate() +} + +func (t *Task) AppendTags(tags []string) { + t.touch() + t.Tags = append(t.Tags, tags...) +} + +func (t *Task) SetComplete(state bool) { + t.touch() + t.Complete = state + if t.Complete { + t.Completed = time.Now() + } else { + t.Completed = time.Time{} + } +} + +func (t *Task) SetPrio(prio int) { + t.touch() + t.Priority = prio +} + +func (t *Task) SetNote(note string) { + t.touch() + t.Note = strings.Split(note, "\n") +} + +func (t *Task) touch() { + t.Edited = time.Now() +} + +func (t *Task) validate() error { + if t.Title == "" { + return errors.New("task cannot have nil title") + } + if err := t.smartSyntax(); err != nil { + return err + } + return nil +} + +func (t *Task) smartSyntax() error { + re := regexp.MustCompile(`^(/([+-]{0,1}\d+)?/)?(.*?)(\s+/([^/]*)/$)?$|`) + matches := re.FindAllStringSubmatch(t.Title, 1)[0] + if len(matches) != 6 { + return nil + } + if matches[1] != "" { + t.Priority = form.ToInt(matches[1]) + } + if matches[3] != "" { + t.Title = matches[3] + } + if matches[5] != "" { + t.Tags = form.ToStrArr(matches[5]) + } + return nil +} diff --git a/server/ajax/task/task_test.go b/server/ajax/task/task_test.go new file mode 100644 index 0000000..ea40b64 --- /dev/null +++ b/server/ajax/task/task_test.go @@ -0,0 +1,59 @@ +package task + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "testing" +) + +func TestNew(t *testing.T) { + if _, err := New(toReq(map[string]interface{}{})); err != nil { + t.Error(err) + } + if _, err := New(toReq(map[string]interface{}{"title": ""})); err == nil { + t.Error(err) + } + if task, err := New(toReq(map[string]interface{}{"title": "i want dogs /a,b,c/"})); err != nil { + t.Error(err) + } else if task.Title != "i want dogs" { + t.Error(task.Title) + } else if fmt.Sprint(task.Tags) != "[a b c]" { + t.Error(task.Tags) + } else { + was := task.Edited + task.touch() + if was == task.Edited { + t.Error(was) + } + was = task.Edited + task.SetNote("hell\nno") + if was == task.Edited { + t.Error(was) + } else if len(task.Note) != 2 { + t.Error(task.Note) + } + } +} + +func toReq(m map[string]interface{}) *http.Request { + if m == nil { + m = map[string]interface{}{} + } + els := map[string]interface{}{ + "title": "title", + "prio": 1, + "tags": "a, b,c", + "duedate": "2010-02-03 05:06:07", + "note": "hello\nworld\ni\nam a note\nand\ni have\nlots\nof\nlines", + } + for k := range els { + if _, ok := m[k]; !ok { + m[k] = els[k] + } + } + b, _ := json.Marshal(m) + return httptest.NewRequest("POST", "/paht", bytes.NewReader(b)) +} diff --git a/server/ajax/task_test.go b/server/ajax/task_test.go new file mode 100644 index 0000000..bf78efe --- /dev/null +++ b/server/ajax/task_test.go @@ -0,0 +1,47 @@ +package ajax + +import ( + "encoding/json" + "local/todo-server/server/ajax/task" + "net/http" + "net/http/httptest" + "testing" +) + +func TestAjaxLoadTasks(t *testing.T) { + a := mockAjax() + + func() { + w := httptest.NewRecorder() + r := httptest.NewRequest("GET", "/", nil) + a.loadTasks(w, r) + var result struct { + List []string `json:"list"` + } + if v := w.Code; v != http.StatusOK { + t.Error(v) + } else if err := json.NewDecoder(w.Body).Decode(&result); err != nil { + t.Error(err) + } else if len(result.List) != 0 { + t.Error(result) + } + }() + + a.storageSetTask("list", "task", &task.Task{Title: "hi"}) + + func() { + w := httptest.NewRecorder() + r := httptest.NewRequest("GET", "/", nil) + a.loadTasks(w, r) + var result struct { + List []task.Task `json:"list"` + } + if v := w.Code; v != http.StatusOK { + t.Error(v) + } else if err := json.NewDecoder(w.Body).Decode(&result); err != nil { + t.Error(err) + } else if len(result.List) != 1 { + t.Error(result) + } + }() +} diff --git a/server/routes.go b/server/routes.go new file mode 100644 index 0000000..ae85bce --- /dev/null +++ b/server/routes.go @@ -0,0 +1,57 @@ +package server + +import ( + "fmt" + "io" + "local/router" + "local/todo-server/config" + "log" + "net/http" + "net/http/httputil" + "net/url" + "os" + "path" +) + +func (s *Server) Routes() error { + routes := []struct { + path string + handler http.HandlerFunc + }{ + { + path: fmt.Sprintf("%s%s", router.Wildcard, router.Wildcard), + handler: s.phpProxy, + }, + { + path: fmt.Sprintf("/ajax.php"), + handler: s.HandleAjax, + }, + } + + for _, route := range routes { + if err := s.Add(route.path, route.handler); err != nil { + return err + } + } + return nil +} + +func (s *Server) index(w http.ResponseWriter, r *http.Request) { + f, err := os.Open(path.Join(config.Public, "index.php")) + if err != nil { + http.Error(w, err.Error(), http.StatusNotFound) + return + } + defer f.Close() + io.Copy(w, f) +} + +func (s *Server) phpProxy(w http.ResponseWriter, r *http.Request) { + url, err := url.Parse(config.Public) + if err != nil { + log.Println(err) + } else { + proxy := httputil.NewSingleHostReverseProxy(url) + proxy.ServeHTTP(w, r) + } +} diff --git a/server/server.go b/server/server.go new file mode 100644 index 0000000..fd2aac4 --- /dev/null +++ b/server/server.go @@ -0,0 +1,22 @@ +package server + +import ( + "local/router" + "local/todo-server/server/ajax" +) + +type Server struct { + *ajax.Ajax + *router.Router +} + +func New() *Server { + ajax, err := ajax.New() + if err != nil { + panic(err) + } + return &Server{ + Ajax: ajax, + Router: router.New(), + } +} diff --git a/testdata/tables b/testdata/tables new file mode 100644 index 0000000..fb851e5 --- /dev/null +++ b/testdata/tables @@ -0,0 +1,34 @@ +# lists + +**id int +**uuid char36 +* ow int +* name char50 +* d_created int +* d_edited int +* sorting tinyint +* published tinyint +* taskview int + +# todolist + +**id int +**uuid char36 +**list_id int +* d_created int +* d_completed int +* d_edited int +* compl tinyint +* title char250 +* note text +* prio tinyint +* ow int +* tags char600 +* tags_ids char250 +* duedate time.date + +# tag2task + +* tag_id int +* task_id int +* list_id int