Unittesting begins

master
Bel LaPointe 2019-11-12 13:45:32 -07:00
commit 8c4bc81694
35 changed files with 3231 additions and 0 deletions

10
.gitignore vendored Executable file
View File

@ -0,0 +1,10 @@
gollum
mytinytodo
**.sw*
**/**.sw*
*.sw*
**/*.sw*
todo-server
exec-todo-server
notes-server
exec-notes-server

45
config/config.go Executable file
View File

@ -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()
}

17
feed/feed.go Normal file
View File

@ -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")
}

17
main.go Normal file
View File

@ -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)
}
}

13
public/images/COPYRIGHT Executable file
View File

@ -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+.

BIN
public/images/arrdown.gif Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 B

BIN
public/images/arrdown2.gif Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 B

BIN
public/images/closetag.gif Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 B

BIN
public/images/corner_left.gif Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 B

BIN
public/images/corner_right.gif Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 B

BIN
public/images/icons.gif Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 330 B

1
public/images/index.html Executable file
View File

@ -0,0 +1 @@
Direct access disallowed!

BIN
public/images/loading1.gif Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

BIN
public/images/loading1_24.gif Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

BIN
public/images/tab_hover.gif Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 270 B

358
public/index.php Executable file
View File

@ -0,0 +1,358 @@
<?php header("Content-type: text/html; charset=utf-8"); ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<title><?php mttinfo('title'); ?></title>
<link rel="stylesheet" type="text/css" href="<?php mttinfo('template_url'); ?>style.css?v=1.4.3" media="all"/>
<?php if (Config::get('rtl')): ?>
<link rel="stylesheet" type="text/css" href="<?php mttinfo('template_url'); ?>style_rtl.css?v=1.4.3" media="all"/>
<?php endif; ?>
<?php if (isset($_GET['pda'])): ?>
<meta name="viewport" id="viewport" content="width=device-width"/>
<link rel="stylesheet" type="text/css" href="<?php mttinfo('template_url'); ?>pda.css?v=1.4.3" media="all"/>
<?php else: ?>
<link rel="stylesheet" type="text/css" href="<?php mttinfo('template_url'); ?>print.css?v=1.4.3" media="print"/>
<?php endif; ?>
</head>
<body>
<script type="text/javascript" src="<?php mttinfo('mtt_url'); ?>jquery/jquery-1.4.4.min.js"></script>
<script type="text/javascript" src="<?php mttinfo('mtt_url'); ?>jquery/jquery-ui-1.8.7.custom.min.js"></script>
<script type="text/javascript" src="<?php mttinfo('mtt_url'); ?>jquery/jquery.autocomplete-1.1.js"></script>
<script type="text/javascript" src="<?php mttinfo('mtt_url'); ?>mytinytodo.js?v=1.4.3"></script>
<script type="text/javascript" src="<?php mttinfo('mtt_url'); ?>mytinytodo_lang.php?v=1.4.3"></script>
<script type="text/javascript" src="<?php mttinfo('mtt_url'); ?>mytinytodo_ajax_storage.js?v=1.4.3"></script>
<script type="text/javascript">
$().ready(function () {
if (isMobileDevice() && getGetParamValue(window.location.href, 'pda') == null) {
window.location.href += '/?pda';
}
<?php if(isset($_GET['pda'])): ?>
$('body').width(screen.width);
$(window).resize(function () {
$('body').width(screen.width);
});
<?php endif; ?>
mytinytodo.mttUrl = "<?php mttinfo('mtt_url'); ?>";
mytinytodo.templateUrl = "<?php mttinfo('template_url'); ?>";
mytinytodo.db = new mytinytodoStorageAjax(mytinytodo);
mytinytodo.init({
needAuth: <?php echo $needAuth ? "true" : "false"; ?>,
isLogged: <?php echo ($needAuth && is_logged()) ? "true" : "false"; ?>,
showdate: <?php echo (Config::get('showdate') && !isset($_GET['pda'])) ? "true" : "false"; ?>,
singletab: <?php echo (isset($_GET['singletab']) || isset($_GET['pda'])) ? "true" : "false"; ?>,
duedatepickerformat: "<?php echo htmlspecialchars(Config::get('dateformat2')); ?>",
firstdayofweek: <?php echo (int)Config::get('firstdayofweek'); ?>,
autotag: <?php echo Config::get('autotag') ? "true" : "false"; ?>
<?php if (isset($_GET['list'])) echo ",openList: " . (int)$_GET['list']; ?>
<?php if (isset($_GET['pda'])) echo ", touchDevice: true"; ?>
}).loadLists(1);
});
</script>
<div id="wrapper">
<div id="container">
<div id="mtt_body">
<h2><?php mttinfo('title'); ?></h2>
<div id="loading"></div>
<div id="bar">
<div id="msg"><span class="msg-text"></span>
<div class="msg-details"></div>
</div>
<div class="bar-menu">
<span class="menu-owner" style="display:none">
<a href="#settings" id="settings"><?php _e('a_settings'); ?></a>
</span>
<span class="bar-delim" style="display:none"> | </span>
<span id="bar_auth">
<span id="bar_public" style="display:none"><?php _e('public_tasks'); ?> |</span>
<a href="#login" id="bar_login" class="nodecor"><u><?php _e('a_login'); ?></u> <span class="arrdown"></span></a>
<a href="#logout" id="bar_logout"><?php _e('a_logout'); ?></a>
</span>
</div>
</div>
<br clear="all"/>
<div id="page_tasks" style="display:none">
<div id="lists">
<ul class="mtt-tabs"></ul>
<div class="mtt-tabs-add-button" title="<?php _e('list_new'); ?>"><span></span>
</div>
<div id="tabs_buttons">
<div class="mtt-tabs-select-button mtt-tabs-button" title="<?php _e('list_select'); ?>">
<span></span></div>
</div>
<div id="list_all" class="mtt-tab mtt-tabs-alltasks mtt-tabs-hidden">
<a href="#alltasks"><span><?php _e('alltasks'); ?></span>
<div class="list-action"></div>
</a></div>
</div>
<div id="toolbar" class="mtt-htabs">
<div id="htab_search">
<table class="mtt-searchbox">
<tr>
<td>
<div class="mtt-searchbox-c">
<input type="text" name="search" value="" maxlength="250" id="search" autocomplete="off"/>
<div class="mtt-searchbox-icon mtt-icon-search"></div>
<div id="search_close" class="mtt-searchbox-icon mtt-icon-cancelsearch"></div>
</div>
</td>
</tr>
</table>
</div>
<div id="htab_newtask">
<table class="mtt-taskbox">
<tr>
<td class="mtt-tb-cell">
<div class="mtt-tb-c">
<form id="newtask_form" method="post">
<label id="task_placeholder" class="placeholding" for="task">
<input type="text" name="task" value="" maxlength="250" id="task" autocomplete="off"/>
<span><?php _e('htab_newtask'); ?></span>
</label>
<div id="newtask_submit" class="mtt-taskbox-icon mtt-icon-submittask" title="<?php _e('btn_add'); ?>"></div>
</form>
</div>
</td>
<td>
<a href="#" id="newtask_adv" class="mtt-img-button" title="<?php _e('advanced_add'); ?>"><span></span></a>
</td>
</tr>
</table>
</div>
<div id="searchbar" style="display:none"><?php _e('searching'); ?>
<span id="searchbarkeyword"></span></div>
<div style="clear:both"></div>
</div>
<h3>
<span id="taskview" class="mtt-menu-button"><span class="btnstr"><?php _e('tasks'); ?></span> (<span id="total">0</span>) <span class="arrdown"></span></span>
<span class="mtt-notes-showhide"><?php _e('notes'); ?>
<a href="#" id="mtt-notes-show"><?php _e('notes_show'); ?></a> / <a href="#" id="mtt-notes-hide"><?php _e('notes_hide'); ?></a></span>
<span id="mtt_filters"></span>
<span id="tagcloudbtn" class="mtt-menu-button"><?php _e('tagcloud'); ?>
<span class="arrdown2"></span></span>
</h3>
<div id="taskcontainer">
<ol id="tasklist" class="sortable"></ol>
</div>
</div> <!-- end of page_tasks -->
<div id="page_taskedit" style="display:none">
<div><a href="#" class="mtt-back-button"><?php _e('go_back'); ?></a></div>
<h3 class="mtt-inadd"><?php _e('add_task'); ?></h3>
<h3 class="mtt-inedit"><?php _e('edit_task'); ?>
<div id="taskedit-date" class="mtt-inedit">
(<span class="date-created" title="<?php _e('taskdate_created'); ?>"><span></span></span><span class="date-completed" title="<?php _e('taskdate_completed'); ?>"> &mdash; <span></span></span>)
</div>
</h3>
<form id="taskedit_form" name="edittask" method="post">
<input type="hidden" name="isadd" value="0"/>
<input type="hidden" name="id" value=""/>
<div class="form-row form-row-short">
<span class="h"><?php _e('priority'); ?></span>
<select name="prio">
<option value="2">+2</option>
<option value="1">+1</option>
<option value="0" selected="selected">&plusmn;0</option>
<option value="-1">&minus;1</option>
</select>
</div>
<div class="form-row form-row-short">
<span class="h"><?php _e('due'); ?> </span>
<input name="duedate" id="duedate" value="" class="in100" title="Y-M-D, M/D/Y, D.M.Y, M/D, D.M" autocomplete="off"/>
</div>
<div class="form-row-short-end"></div>
<div class="form-row">
<div class="h"><?php _e('task'); ?></div>
<input type="text" name="task" value="" class="in500" maxlength="250"/>
</div>
<div class="form-row">
<div class="h"><?php _e('note'); ?></div>
<textarea name="note" class="in500"></textarea></div>
<div class="form-row">
<div class="h"><?php _e('tags'); ?></div>
<table cellspacing="0" cellpadding="0" width="100%">
<tr>
<td>
<input type="text" name="tags" id="edittags" value="" class="in500" maxlength="250"/>
</td>
<td class="alltags-cell">
<a href="#" id="alltags_show"><?php _e('alltags_show'); ?></a>
<a href="#" id="alltags_hide" style="display:none"><?php _e('alltags_hide'); ?></a>
</td>
</tr>
</table>
</div>
<div class="form-row" id="alltags" style="display:none;"><?php _e('alltags'); ?>
<span class="tags-list"></span></div>
<div class="form-row form-bottom-buttons">
<input type="submit" value="<?php _e('save'); ?>"/>
<input type="button" id="mtt_edit_cancel" class="mtt-back-button" value="<?php _e('cancel'); ?>"/>
</div>
</form>
</div> <!-- end of page_taskedit -->
<div id="authform" style="display:none">
<form id="login_form">
<div class="h"><?php _e('password'); ?></div>
<div><input type="password" name="password" id="password"/></div>
<div><input type="submit" value="<?php _e('btn_login'); ?>"/></div>
</form>
</div>
<div id="priopopup" style="display:none">
<span class="prio-neg prio-neg-1">&minus;1</span>
<span class="prio-zero">&plusmn;0</span>
<span class="prio-pos prio-pos-1">+1</span>
<span class="prio-pos prio-pos-2">+2</span>
</div>
<div id="taskviewcontainer" class="mtt-menu-container" style="display:none">
<ul>
<li id="view_tasks"><?php _e('tasks'); ?> (<span id="cnt_total">0</span>)</li>
<li id="view_past"><?php _e('f_past'); ?> (<span id="cnt_past">0</span>)</li>
<li id="view_today"><?php _e('f_today'); ?> (<span id="cnt_today">0</span>)</li>
<li id="view_soon"><?php _e('f_soon'); ?> (<span id="cnt_soon">0</span>)</li>
</ul>
</div>
<div id="tagcloud" style="display:none">
<a id="tagcloudcancel" class="mtt-img-button"><span></span></a>
<div id="tagcloudload"></div>
<div id="tagcloudcontent"></div>
</div>
<div id="listmenucontainer" class="mtt-menu-container" style="display:none">
<ul>
<li class="mtt-need-list mtt-need-real-list" id="btnRenameList"><?php _e('list_rename'); ?></li>
<li class="mtt-need-list mtt-need-real-list" id="btnDeleteList"><?php _e('list_delete'); ?></li>
<li class="mtt-need-list mtt-need-real-list" id="btnClearCompleted"><?php _e('list_clearcompleted'); ?></li>
<li class="mtt-need-list mtt-need-real-list mtt-menu-indicator" submenu="listexportmenucontainer">
<div class="submenu-icon"></div><?php _e('list_export'); ?></li>
<li class="mtt-menu-delimiter mtt-need-real-list"></li>
<li class="mtt-need-list mtt-need-real-list" id="btnPublish">
<div class="menu-icon"></div><?php _e('list_publish'); ?></li>
<li class="mtt-need-list mtt-need-real-list" id="btnRssFeed">
<div class="menu-icon"></div><?php _e('list_rssfeed'); ?></li>
<li class="mtt-menu-delimiter mtt-need-real-list"></li>
<li class="mtt-need-list mtt-need-real-list sort-item" id="sortByHand">
<div class="menu-icon"></div><?php _e('sortByHand'); ?>
<span class="mtt-sort-direction"></span></li>
<li class="mtt-need-list sort-item" id="sortByDateCreated">
<div class="menu-icon"></div><?php _e('sortByDateCreated'); ?>
<span class="mtt-sort-direction"></span></li>
<li class="mtt-need-list sort-item" id="sortByPrio">
<div class="menu-icon"></div><?php _e('sortByPriority'); ?>
<span class="mtt-sort-direction"></span></li>
<li class="mtt-need-list sort-item" id="sortByDueDate">
<div class="menu-icon"></div><?php _e('sortByDueDate'); ?>
<span class="mtt-sort-direction"></span></li>
<li class="mtt-need-list sort-item" id="sortByDateModified">
<div class="menu-icon"></div><?php _e('sortByDateModified'); ?>
<span class="mtt-sort-direction"></span></li>
<li class="mtt-menu-delimiter"></li>
<li class="mtt-need-list" id="btnShowCompleted">
<div class="menu-icon"></div><?php _e('list_showcompleted'); ?></li>
</ul>
</div>
<div id="listexportmenucontainer" class="mtt-menu-container" style="display:none">
<ul>
<li class="mtt-need-list mtt-need-real-list" id="btnExportCSV"><?php _e('list_export_csv'); ?></li>
<li class="mtt-need-list mtt-need-real-list" id="btnExportICAL"><?php _e('list_export_ical'); ?></li>
</ul>
</div>
<div id="taskcontextcontainer" class="mtt-menu-container" style="display:none">
<ul>
<li id="cmenu_edit"><b><?php _e('action_edit'); ?></b></li>
<li id="cmenu_note"><?php _e('action_note'); ?></li>
<li id="cmenu_prio" class="mtt-menu-indicator" submenu="cmenupriocontainer">
<div class="submenu-icon"></div><?php _e('action_priority'); ?></li>
<li id="cmenu_move" class="mtt-menu-indicator" submenu="cmenulistscontainer">
<div class="submenu-icon"></div><?php _e('action_move'); ?></li>
<li id="cmenu_delete"><?php _e('action_delete'); ?></li>
</ul>
</div>
<div id="cmenupriocontainer" class="mtt-menu-container" style="display:none">
<ul>
<li id="cmenu_prio:2">
<div class="menu-icon"></div>
+2
</li>
<li id="cmenu_prio:1">
<div class="menu-icon"></div>
+1
</li>
<li id="cmenu_prio:0">
<div class="menu-icon"></div>&plusmn;0
</li>
<li id="cmenu_prio:-1">
<div class="menu-icon"></div>&minus;1
</li>
</ul>
</div>
<div id="cmenulistscontainer" class="mtt-menu-container" style="display:none">
<ul>
</ul>
</div>
<div id="slmenucontainer" class="mtt-menu-container" style="display:none">
<ul>
<li id="slmenu_list:-1" class="list-id--1 mtt-need-list" <?php if (is_readonly()) echo 'style="display:none"' ?>>
<div class="menu-icon"></div>
<a href="#alltasks"><?php _e('alltasks'); ?></a></li>
<li class="mtt-menu-delimiter slmenu-lists-begin mtt-need-list" <?php if (is_readonly()) echo 'style="display:none"' ?>></li>
</ul>
</div>
<div id="page_ajax" style="display:none"></div>
</div>
<div id="space"></div>
</div>
<div id="footer">
<div id="footer_content">Powered by
<strong><a href="http://www.mytinytodo.net/">myTinyTodo</a></strong> 1.4.3
</div>
</div>
</div>
</body>
</html>
<!-- r390 -->

190
public/pda.css Executable file
View File

@ -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;
}

116
public/print.css Executable file
View File

@ -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;
}

1358
public/style.css Executable file

File diff suppressed because it is too large Load Diff

137
public/style_rtl.css Executable file
View File

@ -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;
}

73
server/ajax/ajax.go Normal file
View File

@ -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)
}
}

20
server/ajax/ajax_test.go Normal file
View File

@ -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
}

60
server/ajax/form/form.go Normal file
View File

@ -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
}

View File

@ -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"
}`))
}

52
server/ajax/list.go Normal file
View File

@ -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")
}

56
server/ajax/list/list.go Normal file
View File

@ -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")
}

115
server/ajax/storage.go Normal file
View File

@ -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)
}

14
server/ajax/tag.go Normal file
View File

@ -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")
}

128
server/ajax/task.go Normal file
View File

@ -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")
}

100
server/ajax/task/task.go Normal file
View File

@ -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
}

View File

@ -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))
}

47
server/ajax/task_test.go Normal file
View File

@ -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)
}
}()
}

57
server/routes.go Normal file
View File

@ -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)
}
}

22
server/server.go Normal file
View File

@ -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(),
}
}

34
testdata/tables vendored Normal file
View File

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