Initial Commit
This commit is contained in:
+146
@@ -0,0 +1,146 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"git.aiterp.net/AiteRP/aitestory/formparser"
|
||||
"git.aiterp.net/gisle/wrouter/generate"
|
||||
"github.com/microcosm-cc/bluemonday"
|
||||
"github.com/russross/blackfriday"
|
||||
)
|
||||
|
||||
// PageCategories are used by the view model and page to enforce
|
||||
// a limited selection of categories. I may move it to a configuration
|
||||
var PageCategories = []string{
|
||||
"OoC",
|
||||
"Story",
|
||||
"Background",
|
||||
"Document",
|
||||
"News",
|
||||
"Item",
|
||||
"Info",
|
||||
}
|
||||
|
||||
// PageTypes describes how the source is rendered. For now it's only markdown,
|
||||
// but who knows what the future holds.
|
||||
var PageTypes = []string{
|
||||
"Markdown",
|
||||
}
|
||||
|
||||
// PageMinDate is the earliest date possible. Stories from Matriarch Eriana's childhood
|
||||
// are thus not going to happen.
|
||||
var PageMinDate, _ = time.Parse(time.RFC3339, "1753-01-01T00:00:00Z")
|
||||
|
||||
// Page is the model describing the individual articles posted
|
||||
// by users.
|
||||
type Page struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Author string `json:"author"`
|
||||
Category string `json:"category"`
|
||||
FictionalDate time.Time `json:"fictionalDate"`
|
||||
PublishDate time.Time `json:"publishDate"`
|
||||
EditDate time.Time `json:"editDate"`
|
||||
Dated bool `json:"dated"`
|
||||
Published bool `json:"published"`
|
||||
Unlisted bool `json:"unlisted"`
|
||||
Specific bool `json:"specific"`
|
||||
Indexed bool `json:"indexed"`
|
||||
BackgroundURL string `json:"backgroundUrl"`
|
||||
Type string `json:"type"`
|
||||
Source string `json:"source"`
|
||||
|
||||
cachedOutput string
|
||||
}
|
||||
|
||||
// Defaults fills in the default details for a page, suited for populating a form
|
||||
func (page *Page) Defaults() {
|
||||
page.Category = PageCategories[0]
|
||||
|
||||
page.Dated = true
|
||||
page.Published = true
|
||||
page.Unlisted = false
|
||||
page.Specific = false
|
||||
page.Indexed = true
|
||||
|
||||
page.BackgroundURL = ""
|
||||
page.Type = PageTypes[0]
|
||||
page.Source = ""
|
||||
}
|
||||
|
||||
// Insert adds the page to the database
|
||||
func (page *Page) Insert() error {
|
||||
page.generateID()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Content parses the content of the page
|
||||
func (page *Page) Content() (string, error) {
|
||||
if page.cachedOutput != "" {
|
||||
return page.cachedOutput, nil
|
||||
}
|
||||
|
||||
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))
|
||||
|
||||
return page.cachedOutput, nil
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("Page type '%s' is not supported", page.Type)
|
||||
}
|
||||
|
||||
// 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.Select(form.Get("category"), &page.Category, PageCategories, page.Category != "")
|
||||
if err != nil {
|
||||
errors = append(errors, fmt.Errorf("Category: %s", err))
|
||||
}
|
||||
|
||||
err = formparser.Date(form.Get("fictionalDate"), &page.FictionalDate, !page.FictionalDate.IsZero())
|
||||
if err != nil {
|
||||
errors = append(errors, fmt.Errorf("Fictonal Date: %s", err))
|
||||
}
|
||||
|
||||
page.Dated = form.Get("dated") != ""
|
||||
page.Published = form.Get("published") != ""
|
||||
page.Unlisted = form.Get("unlisted") != ""
|
||||
page.Specific = form.Get("specific") != ""
|
||||
page.Indexed = form.Get("indexed") != ""
|
||||
|
||||
err = formparser.String(form.Get("backgroundUrl"), &page.BackgroundURL, 0, 255)
|
||||
if err != nil {
|
||||
errors = append(errors, fmt.Errorf("Background URL: %s", err))
|
||||
}
|
||||
|
||||
err = formparser.Select(form.Get("type"), &page.Type, PageTypes, page.Type != "")
|
||||
if err != nil {
|
||||
errors = append(errors, fmt.Errorf("Category: %s", err))
|
||||
}
|
||||
|
||||
err = formparser.String(form.Get("source"), &page.Source, 0, 102400)
|
||||
if err != nil {
|
||||
errors = append(errors, fmt.Errorf("Content is too long, max: 100 KB (~17,000 words)"))
|
||||
}
|
||||
|
||||
if len(errors) > 0 {
|
||||
errors = nil
|
||||
}
|
||||
|
||||
return errors
|
||||
}
|
||||
|
||||
// Standardize page ID generation
|
||||
func (page *Page) generateID() {
|
||||
page.ID = generate.FriendlyID(16)
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package model
|
||||
|
||||
import "testing"
|
||||
import "time"
|
||||
|
||||
func TestPage(t *testing.T) {
|
||||
t.Run("BasicConstants", func(t *testing.T) {
|
||||
if PageMinDate.Format(time.RFC3339) != "1753-01-01T00:00:00Z" {
|
||||
t.Error("Invalid date:", PageMinDate.Format(time.RFC3339))
|
||||
t.Fail()
|
||||
}
|
||||
|
||||
page := Page{}
|
||||
page.generateID()
|
||||
if len(page.ID) != 16 {
|
||||
t.Errorf("len(page.ID): %d != 16", len(page.ID))
|
||||
t.Fail()
|
||||
}
|
||||
|
||||
id1 := page.ID
|
||||
page.generateID()
|
||||
id2 := page.ID
|
||||
|
||||
t.Logf("Page IDs: %s, %s (should not be the same)", id1, id2)
|
||||
if id1 == id2 {
|
||||
t.Fail()
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"git.aiterp.net/AiteRP/aitestory/server"
|
||||
"git.aiterp.net/gisle/wrouter/generate"
|
||||
)
|
||||
|
||||
// TagTypes are the allowed values for Tag.Type
|
||||
var TagTypes = []string{
|
||||
"Location",
|
||||
"Character",
|
||||
"Event",
|
||||
"Organization",
|
||||
"Source",
|
||||
}
|
||||
|
||||
// Tag describes a tag
|
||||
type Tag struct {
|
||||
ID string
|
||||
Type string
|
||||
Name string
|
||||
}
|
||||
|
||||
// Insert adds the tag to the database, giving it a new unique ID
|
||||
func (tag *Tag) Insert() error {
|
||||
db := server.Main.DB
|
||||
|
||||
// Validate tag type
|
||||
validType := false
|
||||
for _, tagType := range TagTypes {
|
||||
if tagType == tag.Type {
|
||||
validType = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !validType {
|
||||
return fmt.Errorf("\"%s\" is not a valid tag type", tag.Type)
|
||||
}
|
||||
|
||||
// Validate tag name
|
||||
if len(tag.Name) == 0 {
|
||||
return errors.New("Tag name is empty")
|
||||
}
|
||||
|
||||
// Generate an ID if none exists
|
||||
if tag.ID == "" {
|
||||
tag.ID = generate.ID()
|
||||
}
|
||||
|
||||
// Do the thing
|
||||
_, err := db.Exec("INSERT INTO `tag` (id,type,name,disabled) VALUES (?,?,?,false)", tag.ID, tag.Type, tag.Name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListTags finds all the tags, without filter. If it hits
|
||||
// the tag cache, it will copy it making it safe to modify
|
||||
func ListTags() ([]Tag, error) {
|
||||
db := server.Main.DB
|
||||
|
||||
// Read from the database
|
||||
rows, err := db.Query("SELECT id,type,name FROM `tag` WHERE disabled=false")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
results := make([]Tag, 0, 64)
|
||||
for rows.Next() {
|
||||
tag := Tag{}
|
||||
rows.Scan(&tag.ID, &tag.Type, &tag.Name)
|
||||
results = append(results, tag)
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"git.aiterp.net/AiteRP/aitestory/server"
|
||||
)
|
||||
|
||||
func TestTag(t *testing.T) {
|
||||
if server.Main.Config.DB.Password == "" {
|
||||
t.Skip("No database password")
|
||||
return
|
||||
}
|
||||
|
||||
t.Run("Insert", func(t *testing.T) {
|
||||
tag := Tag{}
|
||||
tag.Name = "Te'Eryvi"
|
||||
tag.Type = "Organization"
|
||||
|
||||
err := tag.Insert()
|
||||
if err != nil {
|
||||
t.Log("Failed to insert:", err)
|
||||
t.Fail()
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Insert_BadType", func(t *testing.T) {
|
||||
tag := Tag{}
|
||||
tag.Name = "Ehanis Tioran"
|
||||
tag.Type = "Karakter"
|
||||
|
||||
err := tag.Insert()
|
||||
if err == nil {
|
||||
t.Log("Oops, it was inserted.")
|
||||
t.Fail()
|
||||
return
|
||||
}
|
||||
|
||||
if err.Error() != `"Karakter" is not a valid tag type` {
|
||||
t.Log("Wrong error:", err)
|
||||
t.Fail()
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Insert_BadName", func(t *testing.T) {
|
||||
tag := Tag{}
|
||||
tag.Name = ""
|
||||
tag.Type = "Character"
|
||||
|
||||
err := tag.Insert()
|
||||
if err == nil {
|
||||
t.Log("Oops, it was inserted.")
|
||||
t.Fail()
|
||||
return
|
||||
}
|
||||
|
||||
if err.Error() != `Tag name is empty` {
|
||||
t.Log("Wrong error:", err)
|
||||
t.Fail()
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("List", func(t *testing.T) {
|
||||
tags, err := ListTags()
|
||||
if err != nil {
|
||||
t.Log("Failed to get tags:", err)
|
||||
t.Fail()
|
||||
}
|
||||
if len(tags) == 0 {
|
||||
t.Log("No tags found")
|
||||
t.Fail()
|
||||
}
|
||||
|
||||
t.Logf("%d tags found", len(tags))
|
||||
|
||||
found := false
|
||||
for _, tag := range tags {
|
||||
if tag.Name == "Te'Eryvi" {
|
||||
t.Logf("Tag found: %+v", tag)
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Log("The tag inserted in last test wasn't found")
|
||||
t.Fail()
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user