changeset 28:be37dbe1c05d

Finish todo list implementation
author Lewin Bormann <lbo@spheniscida.de>
date Sat, 10 Dec 2016 12:54:29 +0100
parents 4a84e6d53800
children 70822d7d17c5
files handler_todo.go handlers.go sql/todo.go
diffstat 3 files changed, 138 insertions(+), 32 deletions(-) [+]
line wrap: on
line diff
--- a/handler_todo.go	Sat Dec 10 12:07:29 2016 +0100
+++ b/handler_todo.go	Sat Dec 10 12:54:29 2016 +0100
@@ -5,6 +5,7 @@
 	"errors"
 	"fmt"
 	"log"
+	"strconv"
 
 	_ "bitbucket.org/dermesser/goe_bot/sql"
 )
@@ -21,29 +22,70 @@
 		return replyContent{text: "_Zugriff fehlgeschlagen:_ " + err.Error()}, err
 	}
 
-	id, err := todo.AddTodo(msg.Text, msg.From.First_Name+" "+msg.From.Last_Name)
+	// Add new item
+	if msg.Text != "" {
+		id, err := todo.AddTodo(msg.Text, msg.From.First_Name+" "+msg.From.Last_Name)
+
+		if err != nil {
+			return replyContent{text: "_Erstellung fehlgeschlagen:_ " + err.Error()}, err
+		}
+
+		return replyContent{text: fmt.Sprintf("Aufgabe #%d erstellt", id)}, nil
+	} else { // Query open items
+		todos, err := todo.GetOpenTodos("") // default list
+
+		if err != nil {
+			return replyContent{text: "_Konnte keine Aufgaben abfragen:_ " + err.Error()}, err
+		}
 
-	if err != nil {
-		return replyContent{text: "_Erstellung fehlgeschlagen:_ " + err.Error()}, err
+		// create buttons
+		buttons := make([][]inlineKeyboardButton, len(todos))
+
+		for i := range todos {
+			text := fmt.Sprintf("%s (%s)", todos[i].Text, todos[i].Owner)
+			token := fmt.Sprintf("%s:%d", todoDoneCallback, todos[i].ID)
+
+			row := []inlineKeyboardButton{
+				inlineKeyboardButton{Callback_Data: token, Text: text},
+			}
+
+			buttons[i] = row
+		}
+
+		return replyContent{text: "Aufgabe anklicken, um sie als erledigt zu markieren;\n_/todo aufgabe_ um Aufgabe anzulegen",
+			buttons: inlineKeyboardMarkup{Inline_Keyboard: buttons}}, nil
 	}
-
-	return replyContent{text: fmt.Sprintf("Aufgabe #%d erstellt", id)}, nil
 }
 
-func todoButtonTest(ctx context.Context, msg message) (replyContent, error) {
-	buttonRows := [][]inlineKeyboardButton{}
+// Callback handler; called when a button is pressed.
+func todoDoneHandler(ctx context.Context, token string, cbq callbackQuery) (replyContent, error) {
+	log.Println("Received callback for", todoDoneCallback, token)
 
-	for _, b := range []string{"1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15"} {
-		buttonRows = append(buttonRows, []inlineKeyboardButton{inlineKeyboardButton{Callback_Data: "markdone:" + b, Text: b}})
+	todo, err := backend.Todo(cbq.Message.Chat.ID)
+
+	if err != nil {
+		return replyContent{text: "_Zugriff fehlgeschlagen:_ " + err.Error()}, err
 	}
 
-	return replyContent{text: "Please select", buttons: inlineKeyboardMarkup{buttonRows}}, nil
-}
+	id, err := strconv.ParseUint(token, 10, 64)
+
+	if err != nil {
+		return replyContent{text: "_Falsches Format; Zahl erwartet_ (" + token + ")"}, err
+	}
+
+	affected, err := todo.MarkTodoDone(id)
 
-func todoDoneHandler(ctx context.Context, token string, cbq callbackQuery) (replyContent, error) {
-	log.Println("Received callback for", token)
+	if err != nil {
+		return replyContent{text: "_Aktion fehlgeschlagen:_ " + err.Error()}, err
+	}
+
+	reply := replyContent{}
 
-	reply := replyContent{text: "Verarbeitet!"}
+	if affected > 0 {
+		reply.text = "*✓*"
+	} else {
+		reply.text = "Aufgabe bereits erledigt!"
+	}
 
 	return reply, nil
 }
--- a/handlers.go	Sat Dec 10 12:07:29 2016 +0100
+++ b/handlers.go	Sat Dec 10 12:54:29 2016 +0100
@@ -10,14 +10,13 @@
 )
 
 const (
-	echoCmd     = "echo"
-	fortuneCmd  = "fortune"
-	helpCmd     = "help"
-	quoteCmd    = "quote"
-	remindCmd   = "remind"
-	statusCmd   = "status"
-	todoCmd     = "todo"
-	todoTestCmd = "todo-"
+	echoCmd    = "echo"
+	fortuneCmd = "fortune"
+	helpCmd    = "help"
+	quoteCmd   = "quote"
+	remindCmd  = "remind"
+	statusCmd  = "status"
+	todoCmd    = "todo"
 
 	todoDoneCallback = "markdone"
 )
@@ -32,13 +31,12 @@
 
 var (
 	handlers = map[string]handler{
-		echoCmd:     {echoHandler, "Anfrage zurücksenden"},
-		fortuneCmd:  {missingHandler, "Glückskeks"},
-		quoteCmd:    {missingHandler, "Zitat speichern/abfragen"},
-		remindCmd:   {missingHandler, "Wecker"},
-		statusCmd:   {statusHandler, "Status anfragen"},
-		todoCmd:     {todoHandler, "Aufgabenliste"},
-		todoTestCmd: {todoButtonTest, "TODO test (internal)"},
+		echoCmd:    {echoHandler, "Anfrage zurücksenden"},
+		fortuneCmd: {missingHandler, "Glückskeks"},
+		quoteCmd:   {missingHandler, "Zitat speichern/abfragen"},
+		remindCmd:  {missingHandler, "Wecker"},
+		statusCmd:  {statusHandler, "Status anfragen"},
+		todoCmd:    {todoHandler, "Aufgabenliste"},
 	}
 
 	callbackHandlers = map[string]callbackHandler{
@@ -126,12 +124,17 @@
 }
 
 // Dispatches an incoming callbackQuery. The data field has the format callback_type:token.
+// The token can't contain any colons.
 func dispatchCallback(ctx context.Context, cbq callbackQuery) (replyContent, error) {
 	parts := strings.Split(cbq.Data, ":") // callback:token
 
+	if len(parts) != 2 {
+		log.Println("Bad callback data format:", cbq.Data)
+		return replyContent{text: "_Tut mir leid, ein interner Fehler ist aufgetreten_"}, errors.New("bad callback data format")
+	}
+
 	if handler, ok := callbackHandlers[parts[0]]; ok {
 		rp, err := handler(ctx, parts[1], cbq)
-
 		return rp, err
 	} else {
 		log.Println("Didn't find callback handler for type", parts[0])
--- a/sql/todo.go	Sat Dec 10 12:07:29 2016 +0100
+++ b/sql/todo.go	Sat Dec 10 12:54:29 2016 +0100
@@ -1,4 +1,4 @@
-// This file contains logic for retrieving and manipulating todo lists.
+// Thifile contains logic for retrieving and manipulating todo lists.
 package sql
 
 import (
@@ -15,7 +15,7 @@
 	selectOpenTodos = `SELECT id, text, owner FROM todo WHERE NOT done AND chat_id = $1 AND list = $2 ORDER BY ts ASC`
 	// parameters: 1 = todo ID
 	// returns: n/a
-	markTodoDone = `UPDATE todo SET done = true WHERE id = $1`
+	markTodoDone = `UPDATE todo SET done = true WHERE id = $1 AND NOT done`
 )
 
 // Data access object for todo lists
@@ -82,3 +82,64 @@
 		return 0, errors.New("prepared statement not found")
 	}
 }
+
+type OpenTodo struct {
+	// to be used as callback data
+	ID    uint64
+	Text  string
+	Owner string
+}
+
+func (td Todo) GetOpenTodos(list string) ([]OpenTodo, error) {
+	if stmt, ok := td.db.prepared[selectOpenTodos]; ok {
+		rows, err := stmt.Query(td.chatID, "")
+
+		if err != nil {
+			log.Println("Couldn't query open todos:", err)
+			return nil, err
+		}
+
+		defer rows.Close()
+
+		items := make([]OpenTodo, 0, 16)
+
+		for rows.Next() {
+			todo := OpenTodo{}
+			err = rows.Scan(&todo.ID, &todo.Text, &todo.Owner)
+
+			if err != nil {
+				log.Println("Error during scan (todo):", err)
+				return nil, err
+			}
+
+			items = append(items, todo)
+		}
+
+		return items, nil
+	} else {
+		log.Println("Couldn't find prepared statement for", selectOpenTodos)
+		return nil, errors.New("prepared statement not found")
+	}
+}
+
+// Mark todo item as done. Returns number of affected items (0 if the item was already marked as done)
+func (td Todo) MarkTodoDone(id uint64) (int, error) {
+	if stmt, ok := td.db.prepared[markTodoDone]; ok {
+		result, err := stmt.Exec(id)
+
+		if err != nil {
+			log.Println("Couldn't mark", id, "as done:", err)
+			return 0, err
+		}
+
+		// if 0 rows were affected, it's a duplicate action (ok). More rows can't be affected
+		// because of the primary key constraint.
+
+		aff, err := result.RowsAffected()
+
+		return int(aff), err
+	} else {
+		log.Println("Couldn't find prepared statement for", markTodoDone)
+		return 0, errors.New("prepared statement not found")
+	}
+}