diff --git a/controllers/listcontroller.go b/controllers/listcontroller.go index 3f54061..cc67b94 100644 --- a/controllers/listcontroller.go +++ b/controllers/listcontroller.go @@ -58,6 +58,7 @@ func listFiltered(category model.PageCategory) wrouter.FunctionHandlerFunc { return true } + vm.ActiveTag = *tag vm.Headers, err = model.ListHeadersByTag(category.Key, tag) } else { vm.Headers, err = model.ListHeadersByCategory(category.Key) diff --git a/controllers/pagecontroller.go b/controllers/pagecontroller.go index a5285c7..efb5c6f 100644 --- a/controllers/pagecontroller.go +++ b/controllers/pagecontroller.go @@ -1,10 +1,13 @@ package controllers import ( + "fmt" "net/http" "strings" "time" + "git.aiterp.net/gisle/wrouter/response" + "git.aiterp.net/AiteRP/aitestory/model" "git.aiterp.net/AiteRP/aitestory/viewmodel" @@ -99,22 +102,188 @@ func pageCreate(path string, w http.ResponseWriter, req *http.Request, user *aut return true } } + } else { + // Losing input makes the user sad, let's see to it + // that it doesn't happen. + req.ParseForm() + pc.Page.ParseForm(req.Form) + pc.TagInput = req.Form.Get("tags") } - view.Render(w, "create", 200, pc) + view.Render(w, "page/create", 200, pc) return true } func pageView(path string, w http.ResponseWriter, req *http.Request, user *auth.User) bool { + if req.Method != "GET" || strings.LastIndex(req.URL.Path, "/") > len(path) { + return false + } + + page, err := model.FindPage(req.URL.Path[len(path):]) + if err != nil { + response.Text(w, 500, err.Error()) + return true + } else if page == nil { + return false + } + + pv := viewmodel.PageView{} + pv.Page = page + pv.Setup(user) + + view.Render(w, "page/view", 200, pv) + + return true +} + +func pageDelete(path string, w http.ResponseWriter, req *http.Request, user *auth.User) bool { if (req.Method != "GET" && req.Method != "POST") || strings.LastIndex(req.URL.Path, "/") > len(path) { return false } + page, err := model.FindPage(req.URL.Path[len(path):]) + if err != nil { + response.Text(w, 500, err.Error()) + return true + } else if page == nil { + return false + } + + pv := viewmodel.PageView{} + pv.Page = page + pv.Setup(user) + + if user == nil || user.FullID() != page.Author { + view.Render(w, "message/error-access", http.StatusForbidden, path) + return true + } + + if req.Method == "POST" { + req.ParseForm() + + // Catch sneaky shenanigans + if req.Form.Get("aft") != user.Session.ID { + view.Render(w, "message/error-forgery", http.StatusForbidden, path) + return true + } + + // Thy will be done + err := page.Delete() + if err != nil { + // It wasn't done D: + view.Render(w, "message/error-internal", http.StatusInternalServerError, err) + return true + } + + // It has been done + view.Render(w, "message/page-deleted", 200, pv) + return true + } + + view.Render(w, "page/delete", 200, pv) + return true +} + +func pageEdit(path string, w http.ResponseWriter, req *http.Request, user *auth.User) bool { + if (req.Method != "GET" && req.Method != "POST") || strings.LastIndex(req.URL.Path, "/") > len(path) { + return false + } + + page, err := model.FindPage(req.URL.Path[len(path):]) + if err != nil { + response.Text(w, 500, err.Error()) + return true + } else if page == nil { + return false + } + + pf := viewmodel.PageForm{} + pf.Setup(user) + + pf.Operation = "Edit" + pf.Page = *page + + if user == nil { + pf.Error = "You are not logged in" + } + + if user != nil && user.FullID() != page.Author { + pf.Error = "Permission denied" + } + + if req.Method == "POST" { + req.ParseForm() + + errs := pf.Page.ParseForm(req.Form) + if len(errs) > 0 { + pf.Error = "Validation failed: " + errs[0].Error() + } + + if len(errs) == 0 && pf.Error == "" { + pf.TagInput = req.Form.Get("tags") + pf.Page.Tags = make([]model.Tag, 0, strings.Count(pf.TagInput, "\n")) + tagLines := strings.Split(pf.TagInput, "\n") + for _, line := range tagLines { + var tagType, tagName string + + // Skip empty lines, and allow some accidental letters + if len(line) < 2 { + continue + } + + // Parse tokens + tokens := strings.SplitN(line, ":", 2) + if len(tokens) == 2 { + tagType = strings.Trim(tokens[0], "  \t\r") + tagName = strings.Trim(tokens[1], "  \t\r") + } else { + tagType = "*" // Permit untyped tags if it exists. + tagName = strings.Trim(tokens[0], "  \t\r") + } + + // Grab the tag + tag, err := model.EnsureTag(tagType, tagName) + if err != nil { + pf.Error = "Check your tags: " + err.Error() + break + } + + // Take a copy of it + pf.Page.Tags = append(pf.Page.Tags, *tag) + } + + if pf.Error == "" { + pf.Page.EditDate = time.Now() + + err := pf.Page.Update() + if err != nil { + pf.Error = "Edit failed: " + err.Error() + } else { + http.Redirect(w, req, "/page/"+pf.Page.ID, 302) + return true + } + } + } + } else { + for _, tag := range page.Tags { + line := fmt.Sprintf("%s: %s\n", tag.Type, tag.Name) + + if page.PrimaryTag().ID == tag.ID { + pf.TagInput = line + pf.TagInput + } else { + pf.TagInput += line + } + } + } + + view.Render(w, "page/edit", 200, pf) return true } func init() { PageController.Function("/create", pageCreate) + PageController.Function("/edit/", pageEdit) + PageController.Function("/delete/", pageDelete) PageController.Function("/", pageView) } diff --git a/controllers/usercontroller.go b/controllers/usercontroller.go index d5cdb4e..216851b 100644 --- a/controllers/usercontroller.go +++ b/controllers/usercontroller.go @@ -42,7 +42,7 @@ func userLogin(path string, w http.ResponseWriter, req *http.Request, user *auth } ul.Setup(user) - view.Render(w, "login", 200, ul) + view.Render(w, "user/login", 200, ul) return true } diff --git a/model/category.go b/model/category.go index ba9cc5b..22382c3 100644 --- a/model/category.go +++ b/model/category.go @@ -21,13 +21,13 @@ func (category *PageCategory) URLRoot() string { // a limited selection of categories. I may move it to a configuration // or the database, but for now I think this list is pretty fixed var PageCategories = []PageCategory{ - {"OoC", "OoC", "ooc", "OoC content is for announcements, scheduling, general information, or anything that is not in-universe"}, - {"Info", "Info", "info", "Information gained during and between RP sessions"}, - {"News", "News", "news", "News stories that might be pertinent to ongoing plots"}, - {"Item", "Items", "item", "Items relevant to plots, that is more than just a document saved on a character's own omni-tool"}, - {"Document", "Documents", "document", "Data files, shadow broker dossiers, and other data that is not inside an item"}, - {"Background", "Background", "background", "Rumors, suspicious persons, or inter-RP occurences that may be noticed"}, - {"Story", "Stories", "story", "Background stories and inter-RP character intearactions"}, + {"OoC", "OoC", "O", "OoC content is for announcements, scheduling, general information, or anything that is not in-universe"}, + {"Info", "Info", "i", "Information gained during and between RP sessions"}, + {"News", "News", "N", "News stories that might be pertinent to ongoing plots"}, + {"Item", "Items", "I", "Items relevant to plots, that is more than just a document saved on a character's own omni-tool"}, + {"Document", "Document", "D", "Data files, shadow broker dossiers, and other data that is not inside an item"}, + {"Background", "Background", "B", "Rumors, suspicious persons, or inter-RP occurences that may be noticed"}, + {"Story", "Stories", "S", "Background stories and inter-RP character intearactions"}, } var pageCategories []string diff --git a/model/header.go b/model/header.go index 029a0eb..1832118 100644 --- a/model/header.go +++ b/model/header.go @@ -23,6 +23,17 @@ type Header struct { PrimaryTag *Tag `json:"primaryTag"` } +// CategoryInfo gets information about the category +func (header *Header) CategoryInfo() PageCategory { + for _, category := range PageCategories { + if category.Key == header.Category { + return category + } + } + + return PageCategory{"Unknown", "Unknown", "?", ""} +} + // ListHeaders grabs all the general pages from // the database to list them func ListHeaders() ([]Header, error) { @@ -31,7 +42,8 @@ func ListHeaders() ([]Header, error) { FROM page LEFT JOIN page_tag ON (page.id = page_tag.page_id AND page_tag.primary = true) LEFT JOIN tag ON (tag.id = page_tag.tag_id) - WHERE page.specific=false AND page.published=true AND page.unlisted=false; + WHERE page.specific=false AND page.published=true AND page.unlisted=false + ORDER BY page.fictional_date DESC ` db := server.Main.DB @@ -63,7 +75,8 @@ func ListHeadersByCategory(category string) ([]Header, error) { FROM page LEFT JOIN page_tag ON (page.id = page_tag.page_id AND page_tag.primary = true) LEFT JOIN tag ON (tag.id = page_tag.tag_id) - WHERE page.specific=false AND page.published=true AND page.unlisted=false AND page.category = ?; + WHERE page.specific=false AND page.published=true AND page.unlisted=false AND page.category = ? + ORDER BY page.fictional_date DESC ` db := server.Main.DB @@ -97,7 +110,8 @@ func ListHeadersByTag(category string, tag *Tag) ([]Header, error) { RIGHT JOIN page ON page.id = page_tag.page_id LEFT JOIN (page_tag AS pt2) ON (page.id = pt2.page_id AND pt2.primary = true) LEFT JOIN (tag AS tag) ON (tag.id = pt2.tag_id) - WHERE page_tag.tag_id=? + WHERE page_tag.tag_id=? AND page.unlisted=false AND page.published=true + ORDER BY page.fictional_date DESC ` const query2 = ` SELECT page.id,page.name,page.author,page.category,page.fictional_date,page.publish_date,page.edit_date,page.dated,tag.id,tag.type,tag.name @@ -105,7 +119,8 @@ func ListHeadersByTag(category string, tag *Tag) ([]Header, error) { RIGHT JOIN page ON page.id = page_tag.page_id LEFT JOIN (page_tag AS pt2) ON (page.id = pt2.page_id AND pt2.primary = true) LEFT JOIN (tag AS tag) ON (tag.id = pt2.tag_id) - WHERE page_tag.tag_id=? AND page.category=? + WHERE page_tag.tag_id=? AND page.category=? AND page.unlisted=false AND page.published=true + ORDER BY page.fictional_date DESC ` if tag == nil { @@ -190,7 +205,7 @@ func parseHeader(header *Header, rows *sql.Rows) error { rows.Scan(&header.ID, &header.Name, &header.Author, &header.Category, &fictionalDate, &publishDate, &editDate, &header.Dated, &tagID, &tagType, &tagName) if tagID != "" { - header.PrimaryTag = &Tag{tagID, tagName, tagType} + header.PrimaryTag = &Tag{tagID, tagType, tagName} } header.FictionalDate, err = time.Parse("2006-01-02 15:04:05", fictionalDate) diff --git a/model/page.go b/model/page.go index 6128197..dd426a1 100644 --- a/model/page.go +++ b/model/page.go @@ -4,6 +4,7 @@ import ( "database/sql" "errors" "fmt" + "html/template" "net/url" "time" @@ -46,6 +47,7 @@ type Page struct { prevTags []Tag cachedOutput string + primaryTag *Tag } // Defaults fills in the default details for a page, suited for populating a form @@ -99,6 +101,10 @@ func (page *Page) Insert() error { } } + if len(page.Tags) > 0 { + page.primaryTag = &page.Tags[0] + } + return nil } @@ -157,6 +163,9 @@ func (page *Page) Update() error { return err } } + if len(page.Tags) > 0 { + page.primaryTag = &page.Tags[0] + } return nil } @@ -184,29 +193,33 @@ func (page *Page) Delete() error { } // Content parses the content of the page -func (page *Page) Content() (string, error) { - if page.cachedOutput != "" { - return page.cachedOutput, nil - } - +func (page *Page) Content() (template.HTML, error) { if page.Type == "Markdown" { // TODO: Convert [[Ehanis Tioran]] to [Ehanis Tioran](https://wiki.aiterp.net/index.php?title=Ehanis%20Tioran) unsafe := blackfriday.MarkdownCommon([]byte(page.Source)) - page.cachedOutput = string(bluemonday.UGCPolicy().SanitizeBytes(unsafe)) + output := string(bluemonday.UGCPolicy().SanitizeBytes(unsafe)) - return page.cachedOutput, nil + return template.HTML(output), nil } return "", fmt.Errorf("Page type '%s' is not supported", page.Type) } +// PrimaryTag gets the page's primary tag +func (page *Page) PrimaryTag() Tag { + if page.primaryTag == nil { + return Tag{} + } + + return *page.primaryTag +} + // ParseForm validates the values in a form and sets the page's values whenever possible regardless // so that it can be pushed to the viewmodel to allow the user to correct their mistakes without fear // of losing their hard work func (page *Page) ParseForm(form url.Values) []error { errors := make([]error, 0, 4) - page.cachedOutput = "" err := formparser.String(form.Get("name"), &page.Name, 2, 192) if err != nil { @@ -266,10 +279,11 @@ func FindPage(id string) (*Page, error) { WHERE id=? ` const selectPageTags = ` - SELECT tag.id,tag.type,tag.name + SELECT tag.id,tag.type,tag.name,page_tag.primary FROM page_tag RIGHT JOIN tag ON (tag.id = page_tag.tag_id) WHERE page_tag.page_id = ? + ORDER BY tag.type DESC, tag.name ` db := server.Main.DB @@ -297,9 +311,15 @@ func FindPage(id string) (*Page, error) { page.Tags = make([]Tag, 0, 64) for rows.Next() { + primary := false + tag := Tag{} - rows.Scan(&tag.ID, &tag.Type, &tag.Name) + rows.Scan(&tag.ID, &tag.Type, &tag.Name, &primary) page.Tags = append(page.Tags, tag) + + if primary { + page.primaryTag = &tag + } } return page, nil diff --git a/model/page_test.go b/model/page_test.go index 8a7076b..a20b2f1 100644 --- a/model/page_test.go +++ b/model/page_test.go @@ -259,7 +259,7 @@ func TestPage(t *testing.T) { t.Errorf("page.Content: %s", err) } - assertEquals(t, "page.Content()", content, "

Returning Va’ynna’s Omni-Tool

\n\n

Additional Content is additional

\n") + assertEquals(t, "page.Content()", string(content), "

Returning Va’ynna’s Omni-Tool

\n\n

Additional Content is additional

\n") }) t.Run("WikiURL", func(t *testing.T) { diff --git a/model/tag.go b/model/tag.go index e6f2d43..8b436f8 100644 --- a/model/tag.go +++ b/model/tag.go @@ -2,6 +2,7 @@ package model import ( "errors" + "fmt" "strings" "git.aiterp.net/AiteRP/aitestory/server" @@ -24,6 +25,17 @@ type Tag struct { Name string } +// Icon is the API used to get the icon for the tag. +func (tag Tag) Icon() string { + return tag.Type[0:1] +} + +// CSSCLass gets the CSS class that colors the tag on the +// page list +func (tag Tag) CSSCLass() string { + return fmt.Sprintf("ttype-%s", strings.ToLower(tag.Type)) +} + // Insert adds the tag to the database, giving it a new unique ID func (tag *Tag) Insert() error { db := server.Main.DB diff --git a/view/funcs.go b/view/funcs.go new file mode 100644 index 0000000..204eea6 --- /dev/null +++ b/view/funcs.go @@ -0,0 +1,28 @@ +package view + +import ( + "html/template" + "strings" + "time" +) + +func formatDate(date time.Time) string { + return date.Format("Jan _2, 2006") +} + +func formatDateLong(date time.Time) string { + return date.Format("Jan _2, 2006 15:04 MST") +} + +func formatUserID(userid string) string { + split := strings.SplitN(userid, ":", 2) + return split[len(split)-1] +} + +var funcMap = template.FuncMap{ + "formatDate": formatDate, + "formatDateLong": formatDateLong, + "formatUserID": formatUserID, + + "tolower": strings.ToLower, +} diff --git a/view/renderer.go b/view/renderer.go index 21185eb..558002a 100644 --- a/view/renderer.go +++ b/view/renderer.go @@ -27,7 +27,7 @@ func Register(name string, base string, fragments ...string) { } args := append([]string{path.Join(rootPath, name+".tmpl"), path.Join(rootPath, base+".tmpl")}, fragments...) - tmpl, err := template.New(name).ParseFiles(args...) + tmpl, err := template.New(name).Funcs(funcMap).ParseFiles(args...) if err != nil { log.Fatalf("Failed to register %s: %s", name, err) } @@ -44,7 +44,7 @@ func Render(w http.ResponseWriter, name string, status int, viewModel interface{ var err error if server.Main.Config.Server.Debug { - tmpl, err = template.New(name).ParseFiles(argsCache[name]...) + tmpl, err = template.New(name).Funcs(funcMap).ParseFiles(argsCache[name]...) if err != nil { response.Text(w, 500, "Failed to run template "+name+": "+err.Error()) return @@ -78,6 +78,9 @@ func Run(w io.Writer, name string, viewModel interface{}) error { func init() { Register("index", "base/default") - Register("login", "base/default") - Register("create", "base/default") + Register("user/login", "base/default") + Register("page/create", "base/default") + Register("page/edit", "base/default") + Register("page/view", "base/default") + Register("page/delete", "base/default") } diff --git a/view/templates/index.tmpl b/view/templates/index.tmpl index 7885fe7..85af6f1 100644 --- a/view/templates/index.tmpl +++ b/view/templates/index.tmpl @@ -1,6 +1,28 @@ {{ define "content" }}
- + + {{ range .Headers }} + + + + + + + {{ end }} +
{{.CategoryInfo.Icon}} + +
+ {{ if .Dated }} +
{{.FictionalDate | formatDate}}
+ {{ else }} +
{{.PublishDate | formatDate}}
+ {{ end }} + {{ if .PrimaryTag }} +
{{.PrimaryTag.Name}}
+ {{ end }} +
{{.Author | formatUserID}}
+
+
{{ end }} @@ -9,10 +31,16 @@ + {{ if $.ActiveTag.ID }} + + {{ end }} + {{ if $.User.LoggedIn }}