photo post type

This commit is contained in:
Niko Abeler 2023-01-14 11:28:13 +01:00
parent c7834b08d5
commit 1e5ea053cc
7 changed files with 251 additions and 32 deletions

View File

@ -3,7 +3,11 @@ package web
import ( import (
"fmt" "fmt"
"h4kor/owl-blogs" "h4kor/owl-blogs"
"io"
"mime/multipart"
"net/http" "net/http"
"os"
"path"
"strings" "strings"
"sync" "sync"
"time" "time"
@ -174,7 +178,12 @@ func userEditorPostHandler(repo *owl.Repository) func(http.ResponseWriter, *http
return return
} }
err = r.ParseForm() if strings.Split(r.Header.Get("Content-Type"), ";")[0] == "multipart/form-data" {
err = r.ParseMultipartForm(32 << 20)
} else {
err = r.ParseForm()
}
if err != nil { if err != nil {
println("Error parsing form: ", err.Error()) println("Error parsing form: ", err.Error())
w.WriteHeader(http.StatusInternalServerError) w.WriteHeader(http.StatusInternalServerError)
@ -213,6 +222,23 @@ func userEditorPostHandler(repo *owl.Repository) func(http.ResponseWriter, *http
reply_url := r.Form.Get("reply_url") reply_url := r.Form.Get("reply_url")
bookmark_url := r.Form.Get("bookmark_url") bookmark_url := r.Form.Get("bookmark_url")
// photo values
var photo_file multipart.File
var photo_header *multipart.FileHeader
if strings.Split(r.Header.Get("Content-Type"), ";")[0] == "multipart/form-data" {
photo_file, photo_header, err = r.FormFile("photo")
if err != nil && err != http.ErrMissingFile {
println("Error getting photo file: ", err.Error())
w.WriteHeader(http.StatusInternalServerError)
html, _ := owl.RenderUserError(user, owl.ErrorMessage{
Error: "Internal server error",
Message: "Internal server error",
})
w.Write([]byte(html))
return
}
}
// validate form values // validate form values
if post_type == "" { if post_type == "" {
html, _ := owl.RenderUserError(user, owl.ErrorMessage{ html, _ := owl.RenderUserError(user, owl.ErrorMessage{
@ -246,12 +272,20 @@ func userEditorPostHandler(repo *owl.Repository) func(http.ResponseWriter, *http
w.Write([]byte(html)) w.Write([]byte(html))
return return
} }
if post_type == "photo" && photo_file == nil {
html, _ := owl.RenderUserError(user, owl.ErrorMessage{
Error: "Missing Photo",
Message: "You must provide a photo to upload",
})
w.Write([]byte(html))
return
}
// TODO: scrape reply_url for title and description // TODO: scrape reply_url for title and description
// TODO: scrape bookmark_url for title and description // TODO: scrape bookmark_url for title and description
// create post // create post
post, err := user.CreateNewPost(owl.PostMeta{ meta := owl.PostMeta{
Type: post_type, Type: post_type,
Title: title, Title: title,
Description: description, Description: description,
@ -268,7 +302,32 @@ func userEditorPostHandler(repo *owl.Repository) func(http.ResponseWriter, *http
Ingredients: strings.Split(recipe_ingredients, "\n"), Ingredients: strings.Split(recipe_ingredients, "\n"),
Duration: recipe_duration, Duration: recipe_duration,
}, },
}, content) }
if photo_file != nil {
meta.PhotoPath = photo_header.Filename
}
post, err := user.CreateNewPost(meta, content)
// save photo
if photo_file != nil {
println("Saving photo: ", photo_header.Filename)
photo_path := path.Join(post.MediaDir(), photo_header.Filename)
media_file, err := os.Create(photo_path)
if err != nil {
println("Error creating photo file: ", err.Error())
w.WriteHeader(http.StatusInternalServerError)
html, _ := owl.RenderUserError(user, owl.ErrorMessage{
Error: "Internal server error",
Message: "Internal server error",
})
w.Write([]byte(html))
return
}
defer media_file.Close()
io.Copy(media_file, photo_file)
}
if err != nil { if err != nil {
println("Error creating post: ", err.Error()) println("Error creating post: ", err.Error())

View File

@ -1,14 +1,18 @@
package web_test package web_test
import ( import (
"bytes"
"h4kor/owl-blogs" "h4kor/owl-blogs"
main "h4kor/owl-blogs/cmd/owl/web" main "h4kor/owl-blogs/cmd/owl/web"
"h4kor/owl-blogs/test/assertions" "h4kor/owl-blogs/test/assertions"
"h4kor/owl-blogs/test/mocks" "h4kor/owl-blogs/test/mocks"
"io" "io"
"io/ioutil"
"mime/multipart"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"net/url" "net/url"
"path"
"strconv" "strconv"
"strings" "strings"
"testing" "testing"
@ -280,3 +284,63 @@ func TestEditorPostWithSessionRecipe(t *testing.T) {
assertions.AssertStatus(t, rr, http.StatusFound) assertions.AssertStatus(t, rr, http.StatusFound)
assertions.AssertEqual(t, rr.Header().Get("Location"), post.FullUrl()) assertions.AssertEqual(t, rr.Header().Get("Location"), post.FullUrl())
} }
func TestEditorPostWithSessionPhoto(t *testing.T) {
repo, user := getSingleUserTestRepo()
user.ResetPassword("testpassword")
sessionId := user.CreateNewSession()
csrfToken := "test_csrf_token"
// read photo from file
photo_data, err := ioutil.ReadFile("../../../fixtures/image.png")
assertions.AssertNoError(t, err, "Error reading photo")
// Create Request and Response
bodyBuf := &bytes.Buffer{}
bodyWriter := multipart.NewWriter(bodyBuf)
// write photo
fileWriter, err := bodyWriter.CreateFormFile("photo", "../../../fixtures/image.png")
assertions.AssertNoError(t, err, "Error creating form file")
_, err = fileWriter.Write(photo_data)
assertions.AssertNoError(t, err, "Error writing photo")
// write other fields
bodyWriter.WriteField("type", "photo")
bodyWriter.WriteField("title", "testtitle")
bodyWriter.WriteField("content", "testcontent")
bodyWriter.WriteField("csrf_token", csrfToken)
// close body writer
err = bodyWriter.Close()
assertions.AssertNoError(t, err, "Error closing body writer")
req, err := http.NewRequest("POST", user.EditorUrl(), bodyBuf)
req.Header.Add("Content-Type", "multipart/form-data; boundary="+bodyWriter.Boundary())
req.Header.Add("Content-Length", strconv.Itoa(len(bodyBuf.Bytes())))
req.AddCookie(&http.Cookie{Name: "csrf_token", Value: csrfToken})
req.AddCookie(&http.Cookie{Name: "session", Value: sessionId})
assertions.AssertNoError(t, err, "Error creating request")
rr := httptest.NewRecorder()
router := main.SingleUserRouter(&repo)
router.ServeHTTP(rr, req)
assertions.AssertStatus(t, rr, http.StatusFound)
posts, _ := user.AllPosts()
assertions.AssertEqual(t, len(posts), 1)
post := posts[0]
assertions.AssertEqual(t, rr.Header().Get("Location"), post.FullUrl())
assertions.AssertNotEqual(t, post.Meta().PhotoPath, "")
ret_photo_data, err := ioutil.ReadFile(path.Join(post.MediaDir(), post.Meta().PhotoPath))
assertions.AssertNoError(t, err, "Error reading photo")
assertions.AssertEqual(t, len(photo_data), len(ret_photo_data))
if len(photo_data) == len(ret_photo_data) {
for i := range photo_data {
assertions.AssertEqual(t, photo_data[i], ret_photo_data[i])
}
}
}

View File

@ -20,6 +20,57 @@
</form> </form>
</details> </details>
<details>
<summary>Upload Photo</summary>
<form action="" method="post" enctype="multipart/form-data">
<h2>Upload Photo</h2>
<input type="hidden" name="csrf_token" value="{{.CsrfToken}}">
<input type="hidden" name="type" value="photo">
<label for="title">Title</label>
<input type="text" name="title" placeholder="Title" />
<label for="description">Description</label>
<input type="text" name="description" placeholder="Description" />
<label for="content">Content</label>
<textarea name="content" placeholder="Content" rows="24"></textarea>
<label for="photo">Photo</label>
<input type="file" name="photo" placeholder="Photo" />
<br><br>
<input type="submit" value="Create Article" />
</form>
</details>
<details>
<summary>Write Recipe</summary>
<form action="" method="post">
<h2>Create new Recipe</h2>
<input type="hidden" name="csrf_token" value="{{.CsrfToken}}">
<input type="hidden" name="type" value="recipe">
<label for="title">Title</label>
<input type="text" name="title" placeholder="Title" />
<label for="yield">Yield</label>
<input type="text" name="yield" placeholder="Yield" />
<label for="duration">Duration</label>
<input type="text" name="duration" placeholder="Duration" />
<label for="description">Description</label>
<input type="text" name="description" placeholder="Description" />
<label for="ingredients">Ingredients (1 per line)</label>
<textarea name="ingredients" placeholder="Ingredients" rows="8"></textarea>
<label for="content">Instructions</label>
<textarea name="content" placeholder="Ingredients" rows="24"></textarea>
<br><br>
<input type="submit" value="Create Reply" />
</form>
</details>
<details> <details>
<summary>Write Note</summary> <summary>Write Note</summary>
<form action="" method="post"> <form action="" method="post">
@ -54,35 +105,6 @@
</form> </form>
</details> </details>
<details>
<summary>Write Recipe</summary>
<form action="" method="post">
<h2>Create new Recipe</h2>
<input type="hidden" name="csrf_token" value="{{.CsrfToken}}">
<input type="hidden" name="type" value="recipe">
<label for="title">Title</label>
<input type="text" name="title" placeholder="Title" />
<label for="yield">Yield</label>
<input type="text" name="yield" placeholder="Yield" />
<label for="duration">Duration</label>
<input type="text" name="duration" placeholder="Duration" />
<label for="description">Description</label>
<input type="text" name="description" placeholder="Description" />
<label for="ingredients">Ingredients (1 per line)</label>
<textarea name="ingredients" placeholder="Ingredients" rows="8"></textarea>
<label for="content">Instructions</label>
<textarea name="content" placeholder="Ingredients" rows="24"></textarea>
<br><br>
<input type="submit" value="Create Reply" />
</form>
</details>
<details> <details>
<summary>Bookmark</summary> <summary>Bookmark</summary>
<form action="" method="post"> <form action="" method="post">

52
embed/photo/detail.html Normal file
View File

@ -0,0 +1,52 @@
<div class="h-entry">
<hgroup>
<h1 class="p-name">{{.Title}}</h1>
<small>
<a class="u-url" href="{{.Post.FullUrl}}">#</a>
Published:
<time class="dt-published" datetime="{{.Post.Meta.Date}}">
{{.Post.Meta.FormattedDate}}
</time>
{{ if .Post.User.Config.AuthorName }}
by
<a class="p-author h-card" href="{{.Post.User.FullUrl}}">
{{ if .Post.User.AvatarUrl }}
<img class="u-photo u-logo" style="height: 1em;" src="{{ .Post.User.AvatarUrl }}"
alt="{{ .Post.User.Config.Title }}" />
{{ end }}
{{.Post.User.Config.AuthorName}}
</a>
{{ end }}
</small>
</hgroup>
<hr>
<br>
{{ if .Post.Meta.PhotoPath }}
<img class="u-photo" src="media/{{.Post.Meta.PhotoPath}}" alt="{{.Post.Meta.Description}}" />
{{ end }}
<div class="e-content">
{{.Content}}
</div>
<hr>
{{if .Post.ApprovedIncomingWebmentions}}
<h3>
Webmentions
</h3>
<ul>
{{range .Post.ApprovedIncomingWebmentions}}
<li>
<a href="{{.Source}}">
{{if .Title}}
{{.Title}}
{{else}}
{{.Source}}
{{end}}
</a>
</li>
{{end}}
</ul>
{{end}}
</div>

View File

@ -56,4 +56,23 @@
</div> </div>
</div> </div>
<hr>
{{if .Post.ApprovedIncomingWebmentions}}
<h3>
Webmentions
</h3>
<ul>
{{range .Post.ApprovedIncomingWebmentions}}
<li>
<a href="{{.Source}}">
{{if .Title}}
{{.Title}}
{{else}}
{{.Source}}
{{end}}
</a>
</li>
{{end}}
</ul>
{{end}}
</div> </div>

BIN
fixtures/image.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

@ -88,6 +88,7 @@ type PostMeta struct {
Reply ReplyData `yaml:"reply"` Reply ReplyData `yaml:"reply"`
Bookmark BookmarkData `yaml:"bookmark"` Bookmark BookmarkData `yaml:"bookmark"`
Recipe RecipeData `yaml:"recipe"` Recipe RecipeData `yaml:"recipe"`
PhotoPath string `yaml:"photo"`
} }
func (pm PostMeta) FormattedDate() string { func (pm PostMeta) FormattedDate() string {
@ -104,6 +105,7 @@ func (pm *PostMeta) UnmarshalYAML(unmarshal func(interface{}) error) error {
Reply ReplyData `yaml:"reply"` Reply ReplyData `yaml:"reply"`
Bookmark BookmarkData `yaml:"bookmark"` Bookmark BookmarkData `yaml:"bookmark"`
Recipe RecipeData `yaml:"recipe"` Recipe RecipeData `yaml:"recipe"`
PhotoPath string `yaml:"photo"`
} }
type S struct { type S struct {
Date string `yaml:"date"` Date string `yaml:"date"`
@ -129,6 +131,7 @@ func (pm *PostMeta) UnmarshalYAML(unmarshal func(interface{}) error) error {
pm.Reply = t.Reply pm.Reply = t.Reply
pm.Bookmark = t.Bookmark pm.Bookmark = t.Bookmark
pm.Recipe = t.Recipe pm.Recipe = t.Recipe
pm.PhotoPath = t.PhotoPath
possibleFormats := []string{ possibleFormats := []string{
"2006-01-02", "2006-01-02",