package lists import ( "database/sql" "encoding/json" "errors" "fagott.pw/charakterin" "fagott.pw/grilist/frontend" "fagott.pw/grilist/grilist" "fagott.pw/grilist/cache" "fagott.pw/grilist/modules/grils" "fmt" "github.com/julienschmidt/httprouter" "github.com/lib/pq" "io/ioutil" "log" "net/http" "net/url" "sort" "strconv" ) // Module und so. type Module struct { g *grilist.Grilist c *cache.Cache grils *grils.GrilsModule } // List ist eine Liste an DINGEN. type List struct { ID int Name string Description string Owner *charakterin.User ForkOf sql.NullInt64 UpdatedAt pq.NullTime Grils []*ListGril } // ListGril ist ein geranktes Gril type ListGril struct { Gril *grils.Gril Order int } // ListGrils ist die Sort-Interface Implementation für Grils einer Liste. type ListGrils []*ListGril func (l ListGrils) Len() int { return len(l) } func (l ListGrils) Swap(i, j int) { l[i], l[j] = l[j], l[i] } func (l ListGrils) Less(i, j int) bool { return l[i].Order < l[j].Order } // Name gibt den Namen des Moduls zurück func (m *Module) Name() string { return "Lists" } // Init initialisiert das Modul func (m *Module) Init(g *grilist.Grilist) { m.g = g gm, ok := g.Modules["Grils"] if !ok { log.Fatal("lists: grils module not found") } grilsModule, ok := gm.(*grils.GrilsModule) if !ok { log.Fatal("lists: error with grils module") } m.grils = grilsModule m.g.Router.GET("/list/:id", m.viewList) m.g.Router.POST("/list/:id/order", m.updateGrilOrder) m.g.Router.POST("/list/:id", m.addGrilToList) m.g.Router.DELETE("/list/:id/order", m.removeGrilFromList) m.g.Router.GET("/new/list", m.displayCreateList) m.g.Router.POST("/new/list", m.createList) m.g.Router.GET("/api/lists/user", m.APIgetUserLists) m.c = cache.New() } func (m *Module) getListGrils(list *List) error { rows, err := m.g.DB.Query(`SELECT gril_id, "order" FROM grilist.lists_grils WHERE list_id = $1 ORDER BY "order" ASC`, list.ID) if err != nil { return err } defer rows.Close() list.Grils = list.Grils[:0] for rows.Next() { var grilID int lg := &ListGril{} if err := rows.Scan(&grilID, &lg.Order); err != nil { log.Println("error scanning row in getListGrils:", err) continue } gril, err := m.grils.FromID(grilID) if err != nil { log.Println("error getting listGril:", err) continue } lg.Gril = gril list.Grils = append(list.Grils, lg) } sort.Sort(ListGrils(list.Grils)) return nil } func (m *Module) getLists(whereClause string, params ...interface{}) ([]*List, error) { var lists []*List rows, err := m.g.DB.Query(fmt.Sprintf(`SELECT id, name, description, fork_of, updated_at, user_id FROM grilist.lists WHERE %s`, whereClause), params...) if err != nil { return nil, err } defer rows.Close() for rows.Next() { list := &List{} var ownerID int if err := rows.Scan(&list.ID, &list.Name, &list.Description, &list.ForkOf, &list.UpdatedAt, &ownerID); err != nil { log.Println("error scanning row in getLists:", err) continue } if clist, ok := m.c.Get(list.ID); ok { // weiteres parsen abbrechen lists = append(lists, clist.(*List)) continue } // Owner kriegn owner, err := m.g.Charakterin.GetUserByID(ownerID) if err != nil { log.Println("error retreiving owner of list", err) continue } list.Owner = owner m.c.Insert(list.ID, list) lists = append(lists, list) } return lists, nil } // GetUserLists gibt die Listen eines Benutzers zurück. func (m *Module) GetUserLists(u *charakterin.User, withGrils bool) []*List { lists, err := m.getLists(`user_id = $1`, u.ID) if err != nil { log.Println(err) } if withGrils { for _, list := range lists { if err := m.getListGrils(list); err != nil { log.Println(err) } } } return lists } // FromID sucht nach der Liste mit der gegebenen ID und gibt sie, falls sie existiert, zurück. func (m *Module) FromID(id int, withGrils bool) (*List, error) { if lst, ok := m.c.Get(id); ok { l := lst.(*List) // Potenzieller Optimierungsbedarf: neue query, wenn die Liste leer ist. updateGrilOrder nutzt derzeit aber genau dieses verhalten. if withGrils && len(l.Grils) == 0 { if err := m.getListGrils(l); err != nil { return nil, err } } return l, nil } lists, err := m.getLists(`id = $1`, id) if err != nil { return nil, err } if withGrils { for _, list := range lists { if err := m.getListGrils(list); err != nil { return nil, err } } } if len(lists) == 0 { return nil, errors.New("no list found") } return lists[0], nil } func ListsToCards(lists []*List) []frontend.Card { var cards []frontend.Card for _, list := range lists { cards = append(cards, frontend.Card{ Title: list.Name, Description: list.Description, Size: "medium", Actions: []frontend.Action{ frontend.Action{ Name: "anguckieren", Link: fmt.Sprintf("/list/%d", list.ID), }, frontend.Action{ Name: fmt.Sprintf(`von %s`, list.Owner.ID, list.Owner.GetName(), list.Owner.GetName()), Link: "#", Disabled: true, }, }, }) } return cards } // ProvideDashboardData gibt Daten für das Dashboard bezogen auf den Benutzer zurück func (m *Module) ProvideDashboardData(user *charakterin.User) []grilist.DashboardCategory { var categories []grilist.DashboardCategory // Neue Listen lists, err := m.getLists(`1=1 ORDER BY id DESC LIMIT 5`) if err != nil { log.Println(err) return categories } for _, list := range lists { if err := m.getListGrils(list); err != nil { log.Println(err) } } categories = append(categories, grilist.DashboardCategory{ Title: "Neueste Listen", Cards: ListsToCards(lists), }) if user == nil { return categories } // Listen des Benutzers lists = m.GetUserLists(user, false) categories = append(categories, grilist.DashboardCategory{ Title: "Meine Listen", Cards: ListsToCards(lists), }) return categories } func (m *Module) viewList(w http.ResponseWriter, r *http.Request, p httprouter.Params) { user, _ := m.g.Charakterin.GetUserFromRequest(r) sid := p.ByName("id") id, err := strconv.Atoi(sid) if err != nil { log.Println("redir") http.Redirect(w, r, "/", 302) return } list, err := m.FromID(id, true) if err != nil { log.Println("redir") http.Redirect(w, r, "/", 302) return } data := m.g.Renderer.DefaultData() data["user"] = user data["list"] = list m.g.Renderer.RenderPage("list", w, data) } func (m *Module) addGrilToList(w http.ResponseWriter, r *http.Request, p httprouter.Params) { slistID := p.ByName("id") user, err := m.g.Charakterin.GetUserFromRequest(r) if err != nil { http.Error(w, "403", http.StatusForbidden) return } listID, err := strconv.Atoi(slistID) if err != nil { log.Println("invalid list id") return } values, err := readBody(r) if err != nil { log.Println("invalid POST data") return } grilID, err := strconv.Atoi(values.Get("id")) if err != nil { log.Println("invalid gril id") return } list, err := m.FromID(listID, true) if err != nil { http.Error(w, "invalid list", 404) return } if list.Owner.ID != user.ID { http.Error(w, "403", http.StatusForbidden) return } rank := 0 if len(list.Grils) > 0 { rank = list.Grils[len(list.Grils)-1].Order + 1 } // rein in die DB damit _, err = m.g.DB.Query(`INSERT INTO grilist.lists_grils(list_id, gril_id, "order") VALUES($1, $2, $3)`, listID, grilID, rank) if err != nil { log.Println("error inserting gril into list:", err) http.Error(w, "could not insert gril", 500) return } gril, err := m.grils.FromID(grilID) if err != nil { log.Println("inserted gril into list but couldnt get gril afterwards:", err) http.Error(w, "error after insert", 500) return } data := m.g.Renderer.DefaultData() data["Index"] = len(list.Grils) value := make(map[string]interface{}) value["IsListOwner"] = true value["Gril"] = ListGril{gril, rank} data["Value"] = value m.g.Renderer.RenderPage("list_gril", w, data) return } func (m *Module) displayCreateList(w http.ResponseWriter, r *http.Request, p httprouter.Params) { user, err := m.g.Charakterin.GetUserFromRequest(r) if err != nil { http.Redirect(w, r, "/", 302) return } data := m.g.Renderer.DefaultData() data["user"] = user m.g.Renderer.RenderPage("create_list", w, data) } func (m *Module) createList(w http.ResponseWriter, r *http.Request, p httprouter.Params) { user, err := m.g.Charakterin.GetUserFromRequest(r) if err != nil { log.Println(err) http.Error(w, "500", http.StatusInternalServerError) return } values, err := readBody(r) if err != nil { log.Println(err) http.Error(w, "500", http.StatusInternalServerError) return } var id int err = m.g.DB.QueryRow(`INSERT INTO grilist.lists(user_id, name, description) VALUES($1, $2, $3) RETURNING id`, user.ID, values.Get("name"), values.Get("description")).Scan(&id) if err != nil { log.Println(err) http.Error(w, "500", http.StatusInternalServerError) return } http.Redirect(w, r, fmt.Sprintf("/list/%d", id), 302) } func (m *Module) updateGrilOrder(w http.ResponseWriter, r *http.Request, p httprouter.Params) { slistID := p.ByName("id") user, err := m.g.Charakterin.GetUserFromRequest(r) if err != nil { http.Error(w, "403", http.StatusForbidden) return } listID, err := strconv.Atoi(slistID) if err != nil { http.Error(w, "invalid list ID (type mismatch)", 400) return } values, err := readBody(r) if err != nil { http.Error(w, "invalid POST data", 400) return } grilID, err := strconv.Atoi(values.Get("gril")) if err != nil { http.Error(w, "invalid gril ID", 404) return } pos, err := strconv.Atoi(values.Get("pos")) if err != nil { http.Error(w, "invalid position", 400) return } // rein in die DB damit _, err = m.g.DB.Exec(`SELECT grilist.set_gril_order($1, $2, $3, $4)`, user.ID, listID, grilID, pos) if err != nil { log.Println("error reordering gril:", err) http.Error(w, "could not update gril order", 500) return } // wenn die liste im cache ist, die Grils clearen, damit beim naechsten aufruf die Gril-Liste neu geholt wird. if l, ok := m.c.Get(listID); ok { ls := l.(*List) ls.Grils = ls.Grils[:0] } w.WriteHeader(200) w.Write([]byte("ok")) return } func (m *Module) removeGrilFromList(w http.ResponseWriter, r *http.Request, p httprouter.Params) { slistID := p.ByName("id") listID, err := strconv.Atoi(slistID) if err != nil { http.Error(w, "invalid list ID (type mismatch)", 400) return } values, err := readBody(r) if err != nil { http.Error(w, "invalid POST data", 400) return } grilID, err := strconv.Atoi(values.Get("gril")) if err != nil { http.Error(w, "invalid gril ID", 404) return } res, err := m.g.DB.Exec(`DELETE FROM grilist.lists_grils WHERE list_id=$1 AND gril_id=$2`, listID, grilID) if err != nil { log.Println("error removing gril:", err) http.Error(w, "could not remove gril", 500) return } if r, err := res.RowsAffected(); err != nil || r == 0 { log.Println("invalid rows affected in removing gril") http.Error(w, "gril not in list / other error", 500) return } w.WriteHeader(200) w.Write([]byte("ok")) return } func (m *Module) APIgetUserLists(w http.ResponseWriter, r *http.Request, p httprouter.Params) { user, err := m.g.Charakterin.GetUserFromRequest(r) if err != nil { http.Error(w, "403", http.StatusForbidden) return } lists := m.GetUserLists(user, false) for _, list := range lists { // owner wegen SICHERHEIT rausfiltern (nodumppassword2k16) list.Owner = nil } data, err := json.Marshal(&lists) if err != nil { log.Println(err) http.Error(w, "500", http.StatusInternalServerError) return } w.WriteHeader(200) w.Write(data) } // New erstellt eine neue Instanz des Modules func New() *Module { return &Module{} } func readBody(r *http.Request) (url.Values, error) { defer r.Body.Close() data, err := ioutil.ReadAll(r.Body) if err != nil { return nil, err } values, err := url.ParseQuery(string(data)) if err != nil { return nil, err } return values, nil }