Page is done
This commit is contained in:
@@ -58,6 +58,7 @@ func listFiltered(category model.PageCategory) wrouter.FunctionHandlerFunc {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
vm.ActiveTag = *tag
|
||||||
vm.Headers, err = model.ListHeadersByTag(category.Key, tag)
|
vm.Headers, err = model.ListHeadersByTag(category.Key, tag)
|
||||||
} else {
|
} else {
|
||||||
vm.Headers, err = model.ListHeadersByCategory(category.Key)
|
vm.Headers, err = model.ListHeadersByCategory(category.Key)
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
package controllers
|
package controllers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"git.aiterp.net/gisle/wrouter/response"
|
||||||
|
|
||||||
"git.aiterp.net/AiteRP/aitestory/model"
|
"git.aiterp.net/AiteRP/aitestory/model"
|
||||||
"git.aiterp.net/AiteRP/aitestory/viewmodel"
|
"git.aiterp.net/AiteRP/aitestory/viewmodel"
|
||||||
|
|
||||||
@@ -99,22 +102,188 @@ func pageCreate(path string, w http.ResponseWriter, req *http.Request, user *aut
|
|||||||
return true
|
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
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func pageView(path string, w http.ResponseWriter, req *http.Request, user *auth.User) bool {
|
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) {
|
if (req.Method != "GET" && req.Method != "POST") || strings.LastIndex(req.URL.Path, "/") > len(path) {
|
||||||
return false
|
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
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
PageController.Function("/create", pageCreate)
|
PageController.Function("/create", pageCreate)
|
||||||
|
PageController.Function("/edit/", pageEdit)
|
||||||
|
PageController.Function("/delete/", pageDelete)
|
||||||
PageController.Function("/", pageView)
|
PageController.Function("/", pageView)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ func userLogin(path string, w http.ResponseWriter, req *http.Request, user *auth
|
|||||||
}
|
}
|
||||||
|
|
||||||
ul.Setup(user)
|
ul.Setup(user)
|
||||||
view.Render(w, "login", 200, ul)
|
view.Render(w, "user/login", 200, ul)
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|||||||
+7
-7
@@ -21,13 +21,13 @@ func (category *PageCategory) URLRoot() string {
|
|||||||
// a limited selection of categories. I may move it to a configuration
|
// 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
|
// or the database, but for now I think this list is pretty fixed
|
||||||
var PageCategories = []PageCategory{
|
var PageCategories = []PageCategory{
|
||||||
{"OoC", "OoC", "ooc", "OoC content is for announcements, scheduling, general information, or anything that is not in-universe"},
|
{"OoC", "OoC", "O", "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"},
|
{"Info", "Info", "i", "Information gained during and between RP sessions"},
|
||||||
{"News", "News", "news", "News stories that might be pertinent to ongoing plots"},
|
{"News", "News", "N", "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"},
|
{"Item", "Items", "I", "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"},
|
{"Document", "Document", "D", "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"},
|
{"Background", "Background", "B", "Rumors, suspicious persons, or inter-RP occurences that may be noticed"},
|
||||||
{"Story", "Stories", "story", "Background stories and inter-RP character intearactions"},
|
{"Story", "Stories", "S", "Background stories and inter-RP character intearactions"},
|
||||||
}
|
}
|
||||||
var pageCategories []string
|
var pageCategories []string
|
||||||
|
|
||||||
|
|||||||
+20
-5
@@ -23,6 +23,17 @@ type Header struct {
|
|||||||
PrimaryTag *Tag `json:"primaryTag"`
|
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
|
// ListHeaders grabs all the general pages from
|
||||||
// the database to list them
|
// the database to list them
|
||||||
func ListHeaders() ([]Header, error) {
|
func ListHeaders() ([]Header, error) {
|
||||||
@@ -31,7 +42,8 @@ func ListHeaders() ([]Header, error) {
|
|||||||
FROM page
|
FROM page
|
||||||
LEFT JOIN page_tag ON (page.id = page_tag.page_id AND page_tag.primary = true)
|
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)
|
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
|
db := server.Main.DB
|
||||||
@@ -63,7 +75,8 @@ func ListHeadersByCategory(category string) ([]Header, error) {
|
|||||||
FROM page
|
FROM page
|
||||||
LEFT JOIN page_tag ON (page.id = page_tag.page_id AND page_tag.primary = true)
|
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)
|
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
|
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
|
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 (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)
|
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 = `
|
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
|
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
|
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 (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)
|
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 {
|
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)
|
rows.Scan(&header.ID, &header.Name, &header.Author, &header.Category, &fictionalDate, &publishDate, &editDate, &header.Dated, &tagID, &tagType, &tagName)
|
||||||
if tagID != "" {
|
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)
|
header.FictionalDate, err = time.Parse("2006-01-02 15:04:05", fictionalDate)
|
||||||
|
|||||||
+30
-10
@@ -4,6 +4,7 @@ import (
|
|||||||
"database/sql"
|
"database/sql"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"html/template"
|
||||||
"net/url"
|
"net/url"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -46,6 +47,7 @@ type Page struct {
|
|||||||
|
|
||||||
prevTags []Tag
|
prevTags []Tag
|
||||||
cachedOutput string
|
cachedOutput string
|
||||||
|
primaryTag *Tag
|
||||||
}
|
}
|
||||||
|
|
||||||
// Defaults fills in the default details for a page, suited for populating a form
|
// 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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -157,6 +163,9 @@ func (page *Page) Update() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if len(page.Tags) > 0 {
|
||||||
|
page.primaryTag = &page.Tags[0]
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -184,29 +193,33 @@ func (page *Page) Delete() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Content parses the content of the page
|
// Content parses the content of the page
|
||||||
func (page *Page) Content() (string, error) {
|
func (page *Page) Content() (template.HTML, error) {
|
||||||
if page.cachedOutput != "" {
|
|
||||||
return page.cachedOutput, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if page.Type == "Markdown" {
|
if page.Type == "Markdown" {
|
||||||
// TODO: Convert [[Ehanis Tioran]] to [Ehanis Tioran](https://wiki.aiterp.net/index.php?title=Ehanis%20Tioran)
|
// TODO: Convert [[Ehanis Tioran]] to [Ehanis Tioran](https://wiki.aiterp.net/index.php?title=Ehanis%20Tioran)
|
||||||
|
|
||||||
unsafe := blackfriday.MarkdownCommon([]byte(page.Source))
|
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)
|
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
|
// 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
|
// so that it can be pushed to the viewmodel to allow the user to correct their mistakes without fear
|
||||||
// of losing their hard work
|
// of losing their hard work
|
||||||
func (page *Page) ParseForm(form url.Values) []error {
|
func (page *Page) ParseForm(form url.Values) []error {
|
||||||
errors := make([]error, 0, 4)
|
errors := make([]error, 0, 4)
|
||||||
page.cachedOutput = ""
|
|
||||||
|
|
||||||
err := formparser.String(form.Get("name"), &page.Name, 2, 192)
|
err := formparser.String(form.Get("name"), &page.Name, 2, 192)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -266,10 +279,11 @@ func FindPage(id string) (*Page, error) {
|
|||||||
WHERE id=?
|
WHERE id=?
|
||||||
`
|
`
|
||||||
const selectPageTags = `
|
const selectPageTags = `
|
||||||
SELECT tag.id,tag.type,tag.name
|
SELECT tag.id,tag.type,tag.name,page_tag.primary
|
||||||
FROM page_tag
|
FROM page_tag
|
||||||
RIGHT JOIN tag ON (tag.id = page_tag.tag_id)
|
RIGHT JOIN tag ON (tag.id = page_tag.tag_id)
|
||||||
WHERE page_tag.page_id = ?
|
WHERE page_tag.page_id = ?
|
||||||
|
ORDER BY tag.type DESC, tag.name
|
||||||
`
|
`
|
||||||
|
|
||||||
db := server.Main.DB
|
db := server.Main.DB
|
||||||
@@ -297,9 +311,15 @@ func FindPage(id string) (*Page, error) {
|
|||||||
|
|
||||||
page.Tags = make([]Tag, 0, 64)
|
page.Tags = make([]Tag, 0, 64)
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
|
primary := false
|
||||||
|
|
||||||
tag := Tag{}
|
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)
|
page.Tags = append(page.Tags, tag)
|
||||||
|
|
||||||
|
if primary {
|
||||||
|
page.primaryTag = &tag
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return page, nil
|
return page, nil
|
||||||
|
|||||||
+1
-1
@@ -259,7 +259,7 @@ func TestPage(t *testing.T) {
|
|||||||
t.Errorf("page.Content: %s", err)
|
t.Errorf("page.Content: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
assertEquals(t, "page.Content()", content, "<h1>Returning Va’ynna’s Omni-Tool</h1>\n\n<p>Additional Content is additional</p>\n")
|
assertEquals(t, "page.Content()", string(content), "<h1>Returning Va’ynna’s Omni-Tool</h1>\n\n<p>Additional Content is additional</p>\n")
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("WikiURL", func(t *testing.T) {
|
t.Run("WikiURL", func(t *testing.T) {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package model
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"git.aiterp.net/AiteRP/aitestory/server"
|
"git.aiterp.net/AiteRP/aitestory/server"
|
||||||
@@ -24,6 +25,17 @@ type Tag struct {
|
|||||||
Name string
|
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
|
// Insert adds the tag to the database, giving it a new unique ID
|
||||||
func (tag *Tag) Insert() error {
|
func (tag *Tag) Insert() error {
|
||||||
db := server.Main.DB
|
db := server.Main.DB
|
||||||
|
|||||||
@@ -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,
|
||||||
|
}
|
||||||
+7
-4
@@ -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...)
|
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 {
|
if err != nil {
|
||||||
log.Fatalf("Failed to register %s: %s", name, err)
|
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
|
var err error
|
||||||
|
|
||||||
if server.Main.Config.Server.Debug {
|
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 {
|
if err != nil {
|
||||||
response.Text(w, 500, "Failed to run template "+name+": "+err.Error())
|
response.Text(w, 500, "Failed to run template "+name+": "+err.Error())
|
||||||
return
|
return
|
||||||
@@ -78,6 +78,9 @@ func Run(w io.Writer, name string, viewModel interface{}) error {
|
|||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
Register("index", "base/default")
|
Register("index", "base/default")
|
||||||
Register("login", "base/default")
|
Register("user/login", "base/default")
|
||||||
Register("create", "base/default")
|
Register("page/create", "base/default")
|
||||||
|
Register("page/edit", "base/default")
|
||||||
|
Register("page/view", "base/default")
|
||||||
|
Register("page/delete", "base/default")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,28 @@
|
|||||||
{{ define "content" }}
|
{{ define "content" }}
|
||||||
<article>
|
<article>
|
||||||
|
<table class="page-list">
|
||||||
|
{{ range .Headers }}
|
||||||
|
<tbody>
|
||||||
|
<tr class="">
|
||||||
|
<td class="pl-icon">{{.CategoryInfo.Icon}}</td>
|
||||||
|
<td class="pl-content">
|
||||||
|
<div class="plc-title"><a href="/page/{{.ID}}">{{.Name}}</a></div>
|
||||||
|
<div class="plc-meta">
|
||||||
|
{{ if .Dated }}
|
||||||
|
<div class="plcm-date">{{.FictionalDate | formatDate}}</div>
|
||||||
|
{{ else }}
|
||||||
|
<div class="plcm-date">{{.PublishDate | formatDate}}</div>
|
||||||
|
{{ end }}
|
||||||
|
{{ if .PrimaryTag }}
|
||||||
|
<div class="plcm-tag {{.PrimaryTag.CSSCLass}}">{{.PrimaryTag.Name}}</div>
|
||||||
|
{{ end }}
|
||||||
|
<div class="plcm-author">{{.Author | formatUserID}}</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="spacer"><td></td></tr>
|
||||||
|
{{ end }}
|
||||||
|
</table>
|
||||||
</article>
|
</article>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|
||||||
@@ -9,10 +31,16 @@
|
|||||||
|
|
||||||
<ul>
|
<ul>
|
||||||
{{ range .Categories }}
|
{{ range .Categories }}
|
||||||
<li class="{{ if eq .Key $.ActiveCategory.Key }}selected{{end}}"><a href="/{{.URLRoot}}/{{$.ActiveTag.Hook}}"><div class="mg-icon">Π</div><div class="mg-label">{{.Plural}}</div></a></li>
|
<li class="{{ if eq .Key $.ActiveCategory.Key }}selected{{end}}"><a href="/{{.URLRoot}}/"><div class="mg-icon">{{.Icon}}</div><div class="mg-label">{{.Plural}}</div></a></li>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
|
{{ if $.ActiveTag.ID }}
|
||||||
|
<ul>
|
||||||
|
<li class="selected"><a href="/{{$.ActiveTag.Hook}}"><div class="mg-icon">{{$.ActiveTag.Icon}}</div><div class="mg-label">{{$.ActiveTag.Name}}</div></a></li>
|
||||||
|
</ul>
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
{{ if $.User.LoggedIn }}
|
{{ if $.User.LoggedIn }}
|
||||||
<ul>
|
<ul>
|
||||||
<li><a href="/page/create"><div class="mg-icon">+</div><div class="mg-label">Create</div></a></li>
|
<li><a href="/page/create"><div class="mg-icon">+</div><div class="mg-label">Create</div></a></li>
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
{{ define "content" }}
|
{{ define "content" }}
|
||||||
<article>
|
<article>
|
||||||
<h1>Create</h1>
|
<h1>{{$.Operation}}</h1>
|
||||||
<form action="/page/create", method="POST">
|
<form action="/page/create", method="POST">
|
||||||
<p class="red">{{$.Error}}</p>
|
<p class="danger">{{$.Error}}</p>
|
||||||
<input class="big" placeholder="Page Name" name="name" type="text" value="{{$.Page.Name}}" autofocus />
|
<input class="big" placeholder="Page Name" name="name" type="text" value="{{$.Page.Name}}" />
|
||||||
<textarea class="tall" placeholder="Content" name="source">{{$.Page.Source}}</textarea>
|
<textarea class="tall" placeholder="Content" name="source">{{$.Page.Source}}</textarea>
|
||||||
<input placeholder="IC Date" name="fictionalDate" type="text" value="{{if $.Page.FictionalDate.IsZero}}{{else}}{{$.Page.FictionalDate}}{{end}}" autofocus />
|
<input placeholder="IC Date" name="fictionalDate" type="text" value="{{if $.Page.FictionalDate.IsZero}}{{else}}{{$.Page.FictionalDate}}{{end}}" />
|
||||||
<div class="group">
|
<div class="group">
|
||||||
{{ range $.Categories }}
|
{{ range $.Categories }}
|
||||||
<div class="radio-wrapper">
|
<div class="radio-wrapper">
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
</div>
|
</div>
|
||||||
<textarea name="tags" placeholder="Tags, e.g. 'Location: Miner's Respite'. One per line, topmost is primary tag"></textarea>
|
<textarea name="tags" placeholder="Tags, e.g. 'Location: Miner's Respite'. One per line, topmost is primary tag">{{$.TagInput}}</textarea>
|
||||||
<div class="group">
|
<div class="group">
|
||||||
<div class="radio-wrapper">
|
<div class="radio-wrapper">
|
||||||
<input type="checkbox" name="dated" value="true" {{if $.Page.Dated}}checked{{end}}><b> Dated</b>: The IC date is shown on the page list. It will still be used for sorting if this option is disabled.</input>
|
<input type="checkbox" name="dated" value="true" {{if $.Page.Dated}}checked{{end}}><b> Dated</b>: The IC date is shown on the page list. It will still be used for sorting if this option is disabled.</input>
|
||||||
@@ -30,7 +30,7 @@
|
|||||||
<input type="hidden" name="type" value="Markdown" />
|
<input type="hidden" name="type" value="Markdown" />
|
||||||
<input type="hidden" name="published" value="True" />
|
<input type="hidden" name="published" value="True" />
|
||||||
|
|
||||||
<button type="submit">Submit</button>
|
<button type="submit">{{$.Operation}}</button>
|
||||||
|
|
||||||
</form>
|
</form>
|
||||||
</article>
|
</article>
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
{{ define "content" }}
|
||||||
|
<article class="narrow">
|
||||||
|
<h1 class="danger">Delete Page</h1>
|
||||||
|
<form action="/page/delete/{{$.Page.ID}}", method="POST">
|
||||||
|
<p class="danger">
|
||||||
|
This is an irreversible action, so make sure that this is the correct page!
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li><b>ID:</b> {{$.Page.ID}}</li>
|
||||||
|
<li><b>Name:</b> {{$.Page.Name}}</li>
|
||||||
|
<li><b>Category:</b> {{$.Page.Category}}</li>
|
||||||
|
<li><b>Published:</b> {{$.Page.PublishDate | formatDateLong}}</li>
|
||||||
|
</ul>
|
||||||
|
<input type="hidden" name="aft" value="{{$.User.SessionID}}" />
|
||||||
|
|
||||||
|
<button type="submit">Delete Page</button>
|
||||||
|
</article>
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
{{ define "menu" }}
|
||||||
|
<a href="/"><h1>Page</h1></a>
|
||||||
|
|
||||||
|
{{ if eq $.User.ID $.Page.Author}}
|
||||||
|
<ul>
|
||||||
|
<li><a href="/page/edit/{{$.Page.ID}}"><div class="mg-icon">E</div><div class="mg-label">Edit</div></a></li>
|
||||||
|
<li class="selected"><a href="/page/delete/{{$.Page.ID}}"><div class="mg-icon">X</div><div class="mg-label">Delete</div></a></li>
|
||||||
|
</ul>
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<li><a href="/page/{{$.Page.ID}}"><div class="mg-icon"><</div><div class="mg-label">Back</div></a></li>
|
||||||
|
</ul>
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
{{ define "head" }}
|
||||||
|
<link rel="stylesheet" href="/ui/css/form.css" />
|
||||||
|
{{ end }}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
{{ define "content" }}
|
||||||
|
<article>
|
||||||
|
<h1>{{$.Operation}}</h1>
|
||||||
|
<form action="/page/edit/{{$.Page.ID}}", method="POST">
|
||||||
|
<p class="danger">{{$.Error}}</p>
|
||||||
|
<input class="big" placeholder="Page Name" name="name" type="text" value="{{$.Page.Name}}" />
|
||||||
|
<textarea class="tall" placeholder="Content" name="source">{{$.Page.Source}}</textarea>
|
||||||
|
<input placeholder="IC Date" name="fictionalDate" type="text" value="{{if $.Page.FictionalDate.IsZero}}{{else}}{{$.Page.FictionalDate}}{{end}}" />
|
||||||
|
<div class="group">
|
||||||
|
{{ range $.Categories }}
|
||||||
|
<div class="radio-wrapper">
|
||||||
|
<input name="category" type="radio" name="category" value="{{.Key}}" {{if eq $.Page.Category .Key}}checked{{end}}><b> {{.Key}}</b>: {{.Info}}</input>
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
||||||
|
</div>
|
||||||
|
<textarea name="tags" placeholder="Tags, e.g. 'Location: Miner's Respite'. One per line, topmost is primary tag">{{$.TagInput}}</textarea>
|
||||||
|
<div class="group">
|
||||||
|
<div class="radio-wrapper">
|
||||||
|
<input type="checkbox" name="dated" value="true" {{if $.Page.Dated}}checked{{end}}><b> Dated</b>: The IC date is shown on the page list. It will still be used for sorting if this option is disabled.</input>
|
||||||
|
</div>
|
||||||
|
<div class="radio-wrapper">
|
||||||
|
<input type="checkbox" name="unlisted" value="true" {{if $.Page.Unlisted}}checked{{end}}><b> Unlisted</b>: This page will not show up on page lists, but anyone with a link can view it.</input>
|
||||||
|
</div>
|
||||||
|
<div class="radio-wrapper">
|
||||||
|
<input type="checkbox" name="specific" value="true" {{if $.Page.Specific}}checked{{end}}><b> Specific</b>: This page will only show up on page lists when one of its tags is searched for.</input>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Future option -->
|
||||||
|
<input type="hidden" name="type" value="Markdown" />
|
||||||
|
<input type="hidden" name="published" value="True" />
|
||||||
|
|
||||||
|
<button type="submit">{{$.Operation}}</button>
|
||||||
|
|
||||||
|
</form>
|
||||||
|
</article>
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
{{ define "menu" }}
|
||||||
|
<a href="/"><h1>Aite RP</h1></a>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<li><a href="/"><div class="mg-icon"><</div><div class="mg-label">Back</div></a></li>
|
||||||
|
</ul>
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
{{ define "head" }}
|
||||||
|
<link rel="stylesheet" href="/ui/css/form.css" />
|
||||||
|
<script type="text/javascript" src="/ui/js/form-page.js"></script>
|
||||||
|
{{ end }}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
{{ define "content" }}
|
||||||
|
<article class="narrow">
|
||||||
|
{{$.Page.Content}}
|
||||||
|
</article>
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
{{ define "menu" }}
|
||||||
|
<a href="/page/{{$.Page.ID}}"><h1>Page</h1></a>
|
||||||
|
{{ if $.Page.Dated}}
|
||||||
|
<div class="page-property">{{$.Page.FictionalDate | formatDate}}</div>
|
||||||
|
{{ else }}
|
||||||
|
<div class="page-property">{{$.Page.PublishDate | formatDate}}</div>
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
{{ range $.Page.Tags }}
|
||||||
|
<li><a href="/{{.Hook}}"><div class="mg-icon">{{.Icon}}</div><div class="mg-label {{.CSSCLass}}">{{.Name}}</div></a></li>
|
||||||
|
{{ end }}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
{{ if eq $.User.ID $.Page.Author}}
|
||||||
|
<ul>
|
||||||
|
<li><a href="/page/edit/{{$.Page.ID}}"><div class="mg-icon">E</div><div class="mg-label">Edit</div></a></li>
|
||||||
|
<li><a href="/page/delete/{{$.Page.ID}}"><div class="mg-icon">X</div><div class="mg-label">Delete</div></a></li>
|
||||||
|
</ul>
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<li><a href="/"><div class="mg-icon"><</div><div class="mg-label">Back</div></a></li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
{{ if $.User.LoggedIn }}
|
||||||
|
<ul>
|
||||||
|
<li><a href="/user/logout"><div class="mg-icon">A</div><div class="mg-label">Logout</div></a></li>
|
||||||
|
</ul>
|
||||||
|
{{ else }}
|
||||||
|
<ul>
|
||||||
|
<li><a href="/user/login"><div class="mg-icon">A</div><div class="mg-label">Login</div></a></li>
|
||||||
|
</ul>
|
||||||
|
{{ end }}
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
{{ define "head" }}
|
||||||
|
{{ end }}
|
||||||
@@ -2,7 +2,8 @@
|
|||||||
<article>
|
<article>
|
||||||
<h1>Login</h1>
|
<h1>Login</h1>
|
||||||
<form action="/user/login", method="POST">
|
<form action="/user/login", method="POST">
|
||||||
<p class="red">{{$.Error}}</p>
|
<p>Use your wiki.aiterp.net account.</p>
|
||||||
|
<p class="danger">{{$.Error}}</p>
|
||||||
<input placeholder="Username" name="username" type="text" value="{{$.UserName}}" {{if ne $.UserName ""}}autofocus{{end}} />
|
<input placeholder="Username" name="username" type="text" value="{{$.UserName}}" {{if ne $.UserName ""}}autofocus{{end}} />
|
||||||
<input placeholder="Password" name="password" type="password" {{if $.UserName}}autofocus{{end}} />
|
<input placeholder="Password" name="password" type="password" {{if $.UserName}}autofocus{{end}} />
|
||||||
<button type="submit">Submit</button>
|
<button type="submit">Submit</button>
|
||||||
+6
-4
@@ -11,10 +11,11 @@ import (
|
|||||||
// Base is the basic information used to render the page
|
// Base is the basic information used to render the page
|
||||||
type Base struct {
|
type Base struct {
|
||||||
User struct {
|
User struct {
|
||||||
ID string
|
ID string
|
||||||
Name string
|
Name string
|
||||||
Role string
|
Role string
|
||||||
LoggedIn bool
|
LoggedIn bool
|
||||||
|
SessionID string
|
||||||
}
|
}
|
||||||
ViewTitle string
|
ViewTitle string
|
||||||
}
|
}
|
||||||
@@ -28,6 +29,7 @@ func (base *Base) setupBase(user *auth.User, viewTitle string) {
|
|||||||
base.User.Name = user.ID
|
base.User.Name = user.ID
|
||||||
base.User.Role = user.Data["role"]
|
base.User.Role = user.Data["role"]
|
||||||
base.User.LoggedIn = true
|
base.User.LoggedIn = true
|
||||||
|
base.User.SessionID = user.Session.ID
|
||||||
}
|
}
|
||||||
|
|
||||||
base.ViewTitle = fmt.Sprintf("%s - %s", viewTitle, server.Main.Config.View.Title)
|
base.ViewTitle = fmt.Sprintf("%s - %s", viewTitle, server.Main.Config.View.Title)
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
|
|
||||||
type PageView struct {
|
type PageView struct {
|
||||||
Base
|
Base
|
||||||
Page model.Page
|
Page *model.Page
|
||||||
}
|
}
|
||||||
|
|
||||||
func (pv *PageView) Setup(user *auth.User) {
|
func (pv *PageView) Setup(user *auth.User) {
|
||||||
|
|||||||
Reference in New Issue
Block a user