edit feature
This commit is contained in:
parent
a610515d5e
commit
e628735ab2
|
@ -38,7 +38,7 @@ func TestEditorFormGet(t *testing.T) {
|
|||
app := owlApp.FiberApp
|
||||
token := getUserToken(owlApp.AuthorService)
|
||||
|
||||
req := httptest.NewRequest("GET", "/editor/Image", nil)
|
||||
req := httptest.NewRequest("GET", "/editor/new/Image", nil)
|
||||
req.AddCookie(&http.Cookie{Name: "token", Value: token})
|
||||
resp, err := app.Test(req)
|
||||
require.NoError(t, err)
|
||||
|
@ -50,7 +50,7 @@ func TestEditorFormGetNoAuth(t *testing.T) {
|
|||
owlApp := App(db)
|
||||
app := owlApp.FiberApp
|
||||
|
||||
req := httptest.NewRequest("GET", "/editor/Image", nil)
|
||||
req := httptest.NewRequest("GET", "/editor/new/Image", nil)
|
||||
req.AddCookie(&http.Cookie{Name: "token", Value: "invalid"})
|
||||
resp, err := app.Test(req)
|
||||
require.NoError(t, err)
|
||||
|
@ -83,7 +83,7 @@ func TestEditorFormPost(t *testing.T) {
|
|||
io.WriteString(part, "test content")
|
||||
writer.Close()
|
||||
|
||||
req := httptest.NewRequest("POST", "/editor/Image", body)
|
||||
req := httptest.NewRequest("POST", "/editor/new/Image", body)
|
||||
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||||
req.AddCookie(&http.Cookie{Name: "token", Value: token})
|
||||
resp, err := app.Test(req)
|
||||
|
@ -125,7 +125,7 @@ func TestEditorFormPostNoAuth(t *testing.T) {
|
|||
io.WriteString(part, "test content")
|
||||
writer.Close()
|
||||
|
||||
req := httptest.NewRequest("POST", "/editor/Image", body)
|
||||
req := httptest.NewRequest("POST", "/editor/new/Image", body)
|
||||
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||||
req.AddCookie(&http.Cookie{Name: "token", Value: "invalid"})
|
||||
resp, err := app.Test(req)
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
|
||||
<ul>
|
||||
{{range .Types}}
|
||||
<li><a href="/editor/{{.}}">{{.}}</a></li>
|
||||
<li><a href="/editor/new/{{.}}/">{{.}}</a></li>
|
||||
{{end}}
|
||||
|
||||
{{end}}
|
|
@ -29,6 +29,13 @@
|
|||
|
||||
</div>
|
||||
|
||||
{{ if .LoggedIn }}
|
||||
<br>
|
||||
<br>
|
||||
<br>
|
||||
<a href="/editor/edit/{{.Entry.ID}}/" role="button" class="secondary outline">Edit</a>
|
||||
{{ end }}
|
||||
|
||||
|
||||
{{end}}
|
||||
|
||||
|
|
|
@ -36,6 +36,7 @@ func NewWebApp(
|
|||
configRegister *app.ConfigRegister,
|
||||
) *WebApp {
|
||||
app := fiber.New()
|
||||
app.Use(middleware.NewUserMiddleware(authorService).Handle)
|
||||
|
||||
indexHandler := NewIndexHandler(entryService, configRepo)
|
||||
listHandler := NewListHandler(entryService, configRepo)
|
||||
|
@ -62,8 +63,10 @@ func NewWebApp(
|
|||
editor := app.Group("/editor")
|
||||
editor.Use(middleware.NewAuthMiddleware(authorService).Handle)
|
||||
editor.Get("/", editorListHandler.Handle)
|
||||
editor.Get("/:editor/", editorHandler.HandleGet)
|
||||
editor.Post("/:editor/", editorHandler.HandlePost)
|
||||
editor.Get("/new/:editor/", editorHandler.HandleGetNew)
|
||||
editor.Post("/new/:editor/", editorHandler.HandlePostNew)
|
||||
editor.Get("/edit/:id/", editorHandler.HandleGetEdit)
|
||||
editor.Post("/edit/:id/", editorHandler.HandlePostEdit)
|
||||
|
||||
// SiteConfig
|
||||
siteConfig := app.Group("/site-config")
|
||||
|
|
|
@ -1,171 +0,0 @@
|
|||
package editor
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"mime/multipart"
|
||||
"owl-blogs/app"
|
||||
"owl-blogs/domain/model"
|
||||
"reflect"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type HttpFormData interface {
|
||||
// FormFile returns the first file by key from a MultipartForm.
|
||||
FormFile(key string) (*multipart.FileHeader, error)
|
||||
// FormValue returns the first value by key from a MultipartForm.
|
||||
// Search is performed in QueryArgs, PostArgs, MultipartForm and FormFile in this particular order.
|
||||
// Defaults to the empty string "" if the form value doesn't exist.
|
||||
// If a default value is given, it will return that value if the form value does not exist.
|
||||
// Returned value is only valid within the handler. Do not store any references.
|
||||
// Make copies or use the Immutable setting instead.
|
||||
FormValue(key string, defaultValue ...string) string
|
||||
}
|
||||
|
||||
type EditorEntryForm struct {
|
||||
entry model.Entry
|
||||
binSvc *app.BinaryService
|
||||
}
|
||||
|
||||
type EntryFormFieldParams struct {
|
||||
InputType string
|
||||
Widget string
|
||||
}
|
||||
|
||||
type EntryFormField struct {
|
||||
Name string
|
||||
Params EntryFormFieldParams
|
||||
}
|
||||
|
||||
func NewEntryForm(entry model.Entry, binaryService *app.BinaryService) *EditorEntryForm {
|
||||
return &EditorEntryForm{
|
||||
entry: entry,
|
||||
binSvc: binaryService,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *EntryFormFieldParams) ApplyTag(tagKey string, tagValue string) error {
|
||||
switch tagKey {
|
||||
case "inputType":
|
||||
s.InputType = tagValue
|
||||
case "widget":
|
||||
s.Widget = tagValue
|
||||
default:
|
||||
return fmt.Errorf("unknown tag key: %v", tagKey)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *EntryFormField) Html() string {
|
||||
html := ""
|
||||
html += fmt.Sprintf("<label for=\"%v\">%v</label>\n", s.Name, s.Name)
|
||||
if s.Params.InputType == "text" && s.Params.Widget == "textarea" {
|
||||
html += fmt.Sprintf("<textarea name=\"%v\" id=\"%v\" rows=\"20\"></textarea>\n", s.Name, s.Name)
|
||||
} else {
|
||||
html += fmt.Sprintf("<input type=\"%v\" name=\"%v\" id=\"%v\" />\n", s.Params.InputType, s.Name, s.Name)
|
||||
}
|
||||
return html
|
||||
}
|
||||
|
||||
func FieldToFormField(field reflect.StructField) (EntryFormField, error) {
|
||||
formField := EntryFormField{
|
||||
Name: field.Name,
|
||||
Params: EntryFormFieldParams{},
|
||||
}
|
||||
tag := field.Tag.Get("owl")
|
||||
for _, param := range strings.Split(tag, " ") {
|
||||
parts := strings.Split(param, "=")
|
||||
if len(parts) != 2 {
|
||||
continue
|
||||
}
|
||||
err := formField.Params.ApplyTag(parts[0], parts[1])
|
||||
if err != nil {
|
||||
return EntryFormField{}, err
|
||||
}
|
||||
}
|
||||
return formField, nil
|
||||
}
|
||||
|
||||
func StructToFormFields(meta interface{}) ([]EntryFormField, error) {
|
||||
entryType := reflect.TypeOf(meta).Elem()
|
||||
numFields := entryType.NumField()
|
||||
fields := []EntryFormField{}
|
||||
for i := 0; i < numFields; i++ {
|
||||
field, err := FieldToFormField(entryType.Field(i))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fields = append(fields, field)
|
||||
}
|
||||
return fields, nil
|
||||
}
|
||||
|
||||
func (s *EditorEntryForm) HtmlForm() (string, error) {
|
||||
meta := s.entry.MetaData()
|
||||
fields, err := StructToFormFields(meta)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
html := "<form method=\"POST\" enctype=\"multipart/form-data\">\n"
|
||||
for _, field := range fields {
|
||||
html += field.Html()
|
||||
}
|
||||
html += "<input type=\"submit\" value=\"Submit\" />\n"
|
||||
html += "</form>\n"
|
||||
|
||||
return html, nil
|
||||
}
|
||||
|
||||
func (s *EditorEntryForm) Parse(ctx HttpFormData) (model.Entry, error) {
|
||||
if ctx == nil {
|
||||
return nil, fmt.Errorf("nil context")
|
||||
}
|
||||
meta := s.entry.MetaData()
|
||||
metaVal := reflect.ValueOf(meta)
|
||||
if metaVal.Kind() != reflect.Ptr {
|
||||
return nil, fmt.Errorf("meta data is not a pointer")
|
||||
}
|
||||
fields, err := StructToFormFields(meta)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, field := range fields {
|
||||
fieldName := field.Name
|
||||
|
||||
if field.Params.InputType == "file" {
|
||||
file, err := ctx.FormFile(fieldName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fileData, err := file.Open()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer fileData.Close()
|
||||
fileBytes := make([]byte, file.Size)
|
||||
_, err = fileData.Read(fileBytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
binaryFile, err := s.binSvc.Create(file.Filename, fileBytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
metaField := metaVal.Elem().FieldByName(fieldName)
|
||||
if metaField.IsValid() {
|
||||
metaField.SetString(binaryFile.Id)
|
||||
}
|
||||
} else {
|
||||
formValue := ctx.FormValue(fieldName)
|
||||
metaField := metaVal.Elem().FieldByName(fieldName)
|
||||
if metaField.IsValid() {
|
||||
metaField.SetString(formValue)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return s.entry, nil
|
||||
}
|
|
@ -5,7 +5,7 @@ import (
|
|||
"owl-blogs/app/repository"
|
||||
"owl-blogs/domain/model"
|
||||
"owl-blogs/render"
|
||||
"owl-blogs/web/editor"
|
||||
"owl-blogs/web/forms"
|
||||
"time"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
|
@ -41,7 +41,7 @@ func (h *EditorHandler) paramToEntry(c *fiber.Ctx) (model.Entry, error) {
|
|||
return entryType, nil
|
||||
}
|
||||
|
||||
func (h *EditorHandler) HandleGet(c *fiber.Ctx) error {
|
||||
func (h *EditorHandler) HandleGetNew(c *fiber.Ctx) error {
|
||||
c.Set(fiber.HeaderContentType, fiber.MIMETextHTML)
|
||||
|
||||
entryType, err := h.paramToEntry(c)
|
||||
|
@ -49,7 +49,7 @@ func (h *EditorHandler) HandleGet(c *fiber.Ctx) error {
|
|||
return err
|
||||
}
|
||||
|
||||
form := editor.NewEntryForm(entryType, h.binSvc)
|
||||
form := forms.NewForm(entryType.MetaData(), h.binSvc)
|
||||
htmlForm, err := form.HtmlForm()
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -57,23 +57,24 @@ func (h *EditorHandler) HandleGet(c *fiber.Ctx) error {
|
|||
return render.RenderTemplateWithBase(c, getSiteConfig(h.configRepo), "views/editor", htmlForm)
|
||||
}
|
||||
|
||||
func (h *EditorHandler) HandlePost(c *fiber.Ctx) error {
|
||||
func (h *EditorHandler) HandlePostNew(c *fiber.Ctx) error {
|
||||
c.Set(fiber.HeaderContentType, fiber.MIMETextHTML)
|
||||
|
||||
entryType, err := h.paramToEntry(c)
|
||||
entry, err := h.paramToEntry(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
form := editor.NewEntryForm(entryType, h.binSvc)
|
||||
form := forms.NewForm(entry.MetaData(), h.binSvc)
|
||||
// get form data
|
||||
entry, err := form.Parse(c)
|
||||
entryMeta, err := form.Parse(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// create entry
|
||||
now := time.Now()
|
||||
entry.SetMetaData(entryMeta)
|
||||
entry.SetPublishedAt(&now)
|
||||
entry.SetAuthorId(c.Locals("author").(string))
|
||||
|
||||
|
@ -84,3 +85,45 @@ func (h *EditorHandler) HandlePost(c *fiber.Ctx) error {
|
|||
return c.Redirect("/posts/" + entry.ID() + "/")
|
||||
|
||||
}
|
||||
|
||||
func (h *EditorHandler) HandleGetEdit(c *fiber.Ctx) error {
|
||||
c.Set(fiber.HeaderContentType, fiber.MIMETextHTML)
|
||||
|
||||
id := c.Params("id")
|
||||
entry, err := h.entrySvc.FindById(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
form := forms.NewForm(entry.MetaData(), h.binSvc)
|
||||
htmlForm, err := form.HtmlForm()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return render.RenderTemplateWithBase(c, getSiteConfig(h.configRepo), "views/editor", htmlForm)
|
||||
}
|
||||
|
||||
func (h *EditorHandler) HandlePostEdit(c *fiber.Ctx) error {
|
||||
c.Set(fiber.HeaderContentType, fiber.MIMETextHTML)
|
||||
|
||||
id := c.Params("id")
|
||||
entry, err := h.entrySvc.FindById(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
form := forms.NewForm(entry.MetaData(), h.binSvc)
|
||||
// get form data
|
||||
meta, err := form.Parse(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// update entry
|
||||
entry.SetMetaData(meta)
|
||||
err = h.entrySvc.Update(entry)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.Redirect("/posts/" + entry.ID() + "/")
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@ type EntryHandler struct {
|
|||
type entryData struct {
|
||||
Entry model.Entry
|
||||
Author *model.Author
|
||||
LoggedIn bool
|
||||
}
|
||||
|
||||
func NewEntryHandler(
|
||||
|
@ -49,5 +50,14 @@ func (h *EntryHandler) Handle(c *fiber.Ctx) error {
|
|||
author = &model.Author{}
|
||||
}
|
||||
|
||||
return render.RenderTemplateWithBase(c, getSiteConfig(h.configRepo), "views/entry", entryData{Entry: entry, Author: author})
|
||||
return render.RenderTemplateWithBase(
|
||||
c,
|
||||
getSiteConfig(h.configRepo),
|
||||
"views/entry",
|
||||
entryData{
|
||||
Entry: entry,
|
||||
Author: author,
|
||||
LoggedIn: c.Locals("author") != nil,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
|
@ -135,6 +135,14 @@ func (s *Form) Parse(ctx HttpFormData) (interface{}, error) {
|
|||
if field.Params.InputType == "file" {
|
||||
file, err := ctx.FormFile(fieldName)
|
||||
if err != nil {
|
||||
// If field already has a value, we can ignore the error
|
||||
if field.Value != "" {
|
||||
metaField := dataVal.Elem().FieldByName(fieldName)
|
||||
if metaField.IsValid() {
|
||||
metaField.SetString(field.Value)
|
||||
}
|
||||
continue
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
fileData, err := file.Open()
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
package editor_test
|
||||
package forms_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
|
@ -6,10 +6,9 @@ import (
|
|||
"mime/multipart"
|
||||
"os"
|
||||
"owl-blogs/app"
|
||||
"owl-blogs/domain/model"
|
||||
"owl-blogs/infra"
|
||||
"owl-blogs/test"
|
||||
"owl-blogs/web/editor"
|
||||
"owl-blogs/web/forms"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
|
@ -18,7 +17,7 @@ import (
|
|||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type MockEntryMetaData struct {
|
||||
type MockData struct {
|
||||
Image string `owl:"inputType=file"`
|
||||
Content string `owl:"inputType=text"`
|
||||
}
|
||||
|
@ -65,37 +64,16 @@ func (f *MockFormData) FormValue(key string, defaultValue ...string) string {
|
|||
return key
|
||||
}
|
||||
|
||||
type MockEntry struct {
|
||||
model.EntryBase
|
||||
metaData MockEntryMetaData
|
||||
}
|
||||
|
||||
func (e *MockEntry) Content() model.EntryContent {
|
||||
return model.EntryContent(e.metaData.Content)
|
||||
}
|
||||
|
||||
func (e *MockEntry) MetaData() interface{} {
|
||||
return &e.metaData
|
||||
}
|
||||
|
||||
func (e *MockEntry) SetMetaData(metaData interface{}) {
|
||||
e.metaData = *metaData.(*MockEntryMetaData)
|
||||
}
|
||||
|
||||
func (e *MockEntry) Title() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func TestFieldToFormField(t *testing.T) {
|
||||
field := reflect.TypeOf(&MockEntryMetaData{}).Elem().Field(0)
|
||||
formField, err := editor.FieldToFormField(field)
|
||||
field := reflect.TypeOf(&MockData{}).Elem().Field(0)
|
||||
formField, err := forms.FieldToFormField(field, "")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "Image", formField.Name)
|
||||
require.Equal(t, "file", formField.Params.InputType)
|
||||
}
|
||||
|
||||
func TestStructToFields(t *testing.T) {
|
||||
fields, err := editor.StructToFormFields(&MockEntryMetaData{})
|
||||
fields, err := forms.StructToFormFields(&MockData{})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, fields, 2)
|
||||
require.Equal(t, "Image", fields[0].Name)
|
||||
|
@ -104,8 +82,8 @@ func TestStructToFields(t *testing.T) {
|
|||
require.Equal(t, "text", fields[1].Params.InputType)
|
||||
}
|
||||
|
||||
func TestEditorEntryForm_HtmlForm(t *testing.T) {
|
||||
form := editor.NewEntryForm(&MockEntry{}, nil)
|
||||
func TestForm_HtmlForm(t *testing.T) {
|
||||
form := forms.NewForm(&MockData{}, nil)
|
||||
html, err := form.HtmlForm()
|
||||
require.NoError(t, err)
|
||||
require.Contains(t, html, "<form")
|
||||
|
@ -117,7 +95,7 @@ func TestEditorEntryForm_HtmlForm(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestFormParseNil(t *testing.T) {
|
||||
form := editor.NewEntryForm(&MockEntry{}, nil)
|
||||
form := forms.NewForm(&MockData{}, nil)
|
||||
_, err := form.Parse(nil)
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
@ -125,9 +103,9 @@ func TestFormParseNil(t *testing.T) {
|
|||
func TestFormParse(t *testing.T) {
|
||||
binRepo := infra.NewBinaryFileRepo(test.NewMockDb())
|
||||
binService := app.NewBinaryFileService(binRepo)
|
||||
form := editor.NewEntryForm(&MockEntry{}, binService)
|
||||
entry, err := form.Parse(NewMockFormData())
|
||||
form := forms.NewForm(&MockData{}, binService)
|
||||
data, err := form.Parse(NewMockFormData())
|
||||
require.NoError(t, err)
|
||||
require.NotZero(t, entry.MetaData().(*MockEntryMetaData).Image)
|
||||
require.Equal(t, "Content", entry.MetaData().(*MockEntryMetaData).Content)
|
||||
require.NotZero(t, data.(*MockData).Image)
|
||||
require.Equal(t, "Content", data.(*MockData).Content)
|
||||
}
|
|
@ -10,21 +10,37 @@ type AuthMiddleware struct {
|
|||
authorService *app.AuthorService
|
||||
}
|
||||
|
||||
type UserMiddleware struct {
|
||||
authorService *app.AuthorService
|
||||
}
|
||||
|
||||
func NewAuthMiddleware(authorService *app.AuthorService) *AuthMiddleware {
|
||||
return &AuthMiddleware{authorService: authorService}
|
||||
}
|
||||
|
||||
func NewUserMiddleware(authorService *app.AuthorService) *UserMiddleware {
|
||||
return &UserMiddleware{authorService: authorService}
|
||||
}
|
||||
|
||||
func (m *AuthMiddleware) Handle(c *fiber.Ctx) error {
|
||||
if c.Locals("author") == nil {
|
||||
return c.Redirect("/auth/login")
|
||||
}
|
||||
|
||||
return c.Next()
|
||||
}
|
||||
|
||||
func (m *UserMiddleware) Handle(c *fiber.Ctx) error {
|
||||
// get token from cookie
|
||||
token := c.Cookies("token")
|
||||
if token == "" {
|
||||
return c.Redirect("/auth/login")
|
||||
return c.Next()
|
||||
}
|
||||
|
||||
// check token
|
||||
valid, name := m.authorService.ValidateToken(token)
|
||||
if !valid {
|
||||
return c.Redirect("/auth/login")
|
||||
return c.Next()
|
||||
}
|
||||
|
||||
// set author name to context
|
||||
|
|
Loading…
Reference in New Issue