Compare commits

...

42 Commits

Author SHA1 Message Date
5e586aa49a Merge branch 'devel' 2024-09-01 18:51:33 +02:00
6cc0fb40d7 Add version number to index.html 2024-09-01 18:51:14 +02:00
66b2743d3d Merge branch 'devel' 2024-09-01 18:48:26 +02:00
18617f1dbc Give autogenerated articles tags as well 2024-09-01 18:48:18 +02:00
3723b2b5e6 Merge branch 'devel' 2024-09-01 18:18:18 +02:00
79ee20a50e Allow articles to be independent of an issue and allow a title for an issue 2024-09-01 18:18:07 +02:00
e05521591b Add route for PDF uploads 2024-09-01 18:16:26 +02:00
0c9a79e24a Allow for PDF uploads from hub 2024-09-01 18:16:09 +02:00
ce788bfd50 Merge branch 'devel' 2024-09-01 12:54:12 +02:00
8147d9bef6 Small bug fix in build process 2024-09-01 12:54:05 +02:00
230a6278cc Merge branch 'devel' 2024-09-01 12:49:30 +02:00
da0d65d40b Fixed bug where no RSS feed is being generated 2024-09-01 12:27:30 +02:00
42d6e0c198 Merge branch 'devel' 2024-08-31 12:17:41 +02:00
a2d219b2c0 Don't show autogenerated articles in list for deletion 2024-08-31 12:17:30 +02:00
e1af2979af Merge branch 'devel' 2024-08-31 11:27:15 +02:00
b02a882ed7 Fixed small bug that used article.Link instead of correct path for article deletion 2024-08-31 11:27:03 +02:00
f6dedc6f10 Merge branch 'devel' 2024-08-31 01:43:53 +02:00
edb448413b Remove deleted article file when deleting article 2024-08-31 01:42:37 +02:00
cdf0a49550 Merge branch 'devel' 2024-08-31 01:38:28 +02:00
222b791e90 Delete Ports from article links in articles.go and issues.go 2024-08-31 01:35:39 +02:00
c3c0650210 Merge branch 'devel' 2024-08-31 01:00:55 +02:00
bc58b1be44 Fixed small bug in ServeArticle where it was looking at the link to read the file 2024-08-31 01:00:39 +02:00
ab6b9b9a4f Add port to url of UploadArticleImage() 2024-08-31 00:52:10 +02:00
91ef195a56 Check if article has been published before adding it to RSS feed 2024-08-31 00:49:35 +02:00
d077f700d8 Merge branch 'devel' 2024-08-31 00:36:48 +02:00
e4589f3b84 Remove unnecessary mv from update.sh 2024-08-31 00:35:07 +02:00
cd67fe6df3 Delete unnecessary http:// from articles.go and issues.go 2024-08-31 00:22:32 +02:00
ec752b1c66 Merge branch 'devel' 2024-08-30 23:43:12 +02:00
4663cedec5 Store article content outside of DB and convert and serve on request from respective file 2024-08-30 23:43:01 +02:00
46aef4f12f Merge branch 'devel' 2024-08-25 10:51:35 +02:00
1b29e328cf Merge branch 'devel' 2024-08-25 06:38:55 +02:00
e50cb819f3 Merge branch 'devel' 2024-08-23 21:45:30 +02:00
c32e38ca10 Merge branch 'devel' 2024-08-23 20:57:11 +02:00
d7c8c7a43a Merge branch 'devel' 2024-08-18 17:31:00 +02:00
1cd3edc90c Merge branch 'devel' 2024-08-18 12:06:29 +02:00
0e768c9f61 Merge branch 'devel' 2024-08-08 21:27:07 +02:00
1fcd775cc5 Merge branch 'devel' 2024-08-08 21:14:24 +02:00
203a1ed147 Implemented EasyMDE 2024-08-08 21:13:25 +02:00
ef1914ee5c Implemented article preview 2024-08-08 21:13:25 +02:00
084b101e31 Register f.ArticlePreviewHtmlData in init() 2024-08-08 21:13:25 +02:00
b2db128aa9 Shorten lines by referencing frontend as f and backend as b 2024-08-08 21:13:25 +02:00
081e880fb6 Change structure of code tor frontend and backend one 2024-08-08 21:13:25 +02:00
17 changed files with 320 additions and 166 deletions

View File

@ -9,16 +9,20 @@ import (
) )
type Article struct { type Article struct {
Title string Title string
Created time.Time Created time.Time
Description string Description string
Content string Link string
Link string EncURL string
Published bool EncLength int
Rejected bool EncType string
ID int64 Published bool
AuthorID int64 Rejected bool
IssueID int64 ID int64
AuthorID int64
IssueID int64
IsInIssue bool
AutoGenerated bool
} }
func (db *DB) AddArticle(a *Article) (int64, error) { func (db *DB) AddArticle(a *Article) (int64, error) {
@ -27,8 +31,8 @@ func (db *DB) AddArticle(a *Article) (int64, error) {
selectQuery := "SELECT id FROM issues WHERE published = false" selectQuery := "SELECT id FROM issues WHERE published = false"
insertQuery := ` insertQuery := `
INSERT INTO articles INSERT INTO articles
(title, description, content, link, published, rejected, author_id, issue_id) (title, description, link, enc_url, enc_length, enc_type, published, rejected, author_id, issue_id, is_in_issue, auto_generated)
VALUES (?, ?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
` `
for i := 0; i < TxMaxRetries; i++ { for i := 0; i < TxMaxRetries; i++ {
@ -45,8 +49,10 @@ func (db *DB) AddArticle(a *Article) (int64, error) {
return 0, fmt.Errorf("error getting issue ID when adding article to DB: %v", err) return 0, fmt.Errorf("error getting issue ID when adding article to DB: %v", err)
} }
result, err := tx.Exec(insertQuery, a.Title, a.Description, fmt.Println(a)
a.Content, a.Link, a.Published, a.Rejected, a.AuthorID, id) result, err := tx.Exec(insertQuery, a.Title, a.Description, a.Link,
a.EncURL, a.EncLength, a.EncType, a.Published, a.Rejected, a.AuthorID, id,
a.IsInIssue, a.AutoGenerated)
if err != nil { if err != nil {
if rollbackErr := tx.Rollback(); rollbackErr != nil { if rollbackErr := tx.Rollback(); rollbackErr != nil {
log.Fatalf("transaction error: %v, rollback error: %v", err, rollbackErr) log.Fatalf("transaction error: %v, rollback error: %v", err, rollbackErr)
@ -80,7 +86,7 @@ func (db *DB) AddArticle(a *Article) (int64, error) {
func (db *DB) GetArticle(id int64) (*Article, error) { func (db *DB) GetArticle(id int64) (*Article, error) {
query := ` query := `
SELECT title, created, description, content, link, published, author_id SELECT title, created, description, link, enc_url, enc_length, enc_type, published, author_id, issue_id, is_in_issue, auto_generated
FROM articles FROM articles
WHERE id = ? WHERE id = ?
` `
@ -91,7 +97,9 @@ func (db *DB) GetArticle(id int64) (*Article, error) {
var err error var err error
if err := row.Scan(&article.Title, &created, &article.Description, if err := row.Scan(&article.Title, &created, &article.Description,
&article.Content, &article.Link, &article.Published, &article.AuthorID); err != nil { &article.Link, &article.EncURL, &article.EncLength, &article.EncType,
&article.Published, &article.AuthorID, &article.IssueID,
&article.IsInIssue, &article.AutoGenerated); err != nil {
return nil, fmt.Errorf("error scanning article row: %v", err) return nil, fmt.Errorf("error scanning article row: %v", err)
} }
@ -106,7 +114,7 @@ func (db *DB) GetArticle(id int64) (*Article, error) {
func (db *DB) GetCertainArticles(published, rejected bool) ([]*Article, error) { func (db *DB) GetCertainArticles(published, rejected bool) ([]*Article, error) {
query := ` query := `
SELECT id, title, created, description, content, link, author_id, issue_id SELECT id, title, created, description, link, enc_url, enc_length, enc_type, author_id, issue_id, is_in_issue, auto_generated
FROM articles FROM articles
WHERE published = ? WHERE published = ?
AND rejected = ? AND rejected = ?
@ -122,8 +130,9 @@ func (db *DB) GetCertainArticles(published, rejected bool) ([]*Article, error) {
var created []byte var created []byte
if err = rows.Scan(&article.ID, &article.Title, &created, if err = rows.Scan(&article.ID, &article.Title, &created,
&article.Description, &article.Content, &article.Link, &article.Description, &article.Link, &article.EncURL, &article.EncLength,
&article.AuthorID, &article.IssueID); err != nil { &article.EncType, &article.AuthorID, &article.IssueID,
&article.IsInIssue, &article.AutoGenerated); err != nil {
return nil, fmt.Errorf("error scanning article row: %v", err) return nil, fmt.Errorf("error scanning article row: %v", err)
} }
@ -144,9 +153,9 @@ func (db *DB) GetCurrentIssueArticles() ([]*Article, error) {
txOptions := &sql.TxOptions{Isolation: sql.LevelSerializable} txOptions := &sql.TxOptions{Isolation: sql.LevelSerializable}
issueQuery := "SELECT id FROM issues WHERE published = false" issueQuery := "SELECT id FROM issues WHERE published = false"
articlesQuery := ` articlesQuery := `
SELECT id, title, created, description, content, link, author_id SELECT id, title, created, description, link, enc_url, enc_length, enc_type, author_id, auto_generated
FROM articles FROM articles
WHERE issue_id = ? AND published = true WHERE issue_id = ? AND published = true AND is_in_issue = true
` `
for i := 0; i < TxMaxRetries; i++ { for i := 0; i < TxMaxRetries; i++ {
@ -178,7 +187,8 @@ func (db *DB) GetCurrentIssueArticles() ([]*Article, error) {
var created []byte var created []byte
if err = rows.Scan(&article.ID, &article.Title, &created, if err = rows.Scan(&article.ID, &article.Title, &created,
&article.Description, &article.Content, &article.Link, &article.AuthorID); err != nil { &article.Description, &article.Link, &article.EncURL, &article.EncLength,
&article.EncType, &article.AuthorID, &article.AutoGenerated); err != nil {
if rollbackErr := tx.Rollback(); rollbackErr != nil { if rollbackErr := tx.Rollback(); rollbackErr != nil {
log.Fatalf("transaction error: %v, rollback error: %v", err, rollbackErr) log.Fatalf("transaction error: %v, rollback error: %v", err, rollbackErr)
} }

View File

@ -9,47 +9,6 @@ import (
"git.streifling.com/jason/rss" "git.streifling.com/jason/rss"
) )
func GetChannel(db *DB, title, link, description string) (*rss.Channel, error) {
channel := &rss.Channel{
Title: title,
Link: link,
Description: description,
Items: make([]*rss.Item, 0),
}
articles, err := db.GetCertainArticles(true, false)
if err != nil {
return nil, fmt.Errorf("error fetching published articles: %v", err)
}
for _, article := range articles {
tags, err := db.GetArticleTags(article.ID)
if err != nil {
return nil, fmt.Errorf("error fetching tags for article %v: %v", article.Title, err)
}
tagNames := make([]string, 0)
for _, tag := range tags {
tagNames = append(tagNames, tag.Name)
}
user, err := db.GetUser(article.AuthorID)
if err != nil {
return nil, fmt.Errorf("error finding user %v: %v", article.AuthorID, err)
}
channel.Items = append(channel.Items, &rss.Item{
Title: article.Title,
Author: fmt.Sprint(user.FirstName, " ", user.LastName),
PubDate: article.Created.Format(time.RFC1123Z),
Description: article.Description,
Content: &rss.Content{Value: article.Content},
Categories: tagNames,
})
}
return channel, nil
}
func GenerateRSS(c *Config, db *DB) (*string, error) { func GenerateRSS(c *Config, db *DB) (*string, error) {
channel := &rss.Channel{ channel := &rss.Channel{
Title: c.Title, Title: c.Title,
@ -72,7 +31,13 @@ func GenerateRSS(c *Config, db *DB) (*string, error) {
for _, tag := range tags { for _, tag := range tags {
tagNames = append(tagNames, tag.Name) tagNames = append(tagNames, tag.Name)
} }
tagNames = append(tagNames, fmt.Sprint("Orient Express ", article.IssueID))
if article.IsInIssue || article.AutoGenerated {
tagNames = append(tagNames, fmt.Sprint("Orient Express ", article.IssueID))
}
if article.AutoGenerated {
tagNames = append(tagNames, "autogenerated")
}
user, err := db.GetUser(article.AuthorID) user, err := db.GetUser(article.AuthorID)
if err != nil { if err != nil {
@ -93,15 +58,18 @@ func GenerateRSS(c *Config, db *DB) (*string, error) {
Author: fmt.Sprint(user.FirstName, " ", user.LastName), Author: fmt.Sprint(user.FirstName, " ", user.LastName),
Categories: tagNames, Categories: tagNames,
Description: articleDescription, Description: articleDescription,
Guid: string(article.ID),
Link: article.Link,
PubDate: article.Created.Format(time.RFC1123Z), PubDate: article.Created.Format(time.RFC1123Z),
Title: articleTitle, Title: articleTitle,
} }
fmt.Println(article.Link, ": ", len(article.Link))
if article.Link == "" { if article.AutoGenerated {
item.Link = fmt.Sprint("http://", c.Domain, "/article/serve/", article.ID, ".html") item.Enclosure = &rss.Enclosure{
} else { Url: article.EncURL,
item.Link = article.Link Lenght: article.EncLength,
Type: article.EncType,
}
} }
channel.Items = append(channel.Items, item) channel.Items = append(channel.Items, item)

View File

@ -4,6 +4,7 @@ import (
"fmt" "fmt"
"log" "log"
"net/http" "net/http"
"os"
"strconv" "strconv"
b "streifling.com/jason/cpolis/cmd/backend" b "streifling.com/jason/cpolis/cmd/backend"
@ -30,6 +31,25 @@ func ServeArticle(c *b.Config, db *b.DB) http.HandlerFunc {
return return
} }
fmt.Fprint(w, article.Content) if !article.Published {
return
}
articleAbsName := fmt.Sprint(c.ArticleDir, "/", article.ID, ".md")
contentBytes, err := os.ReadFile(articleAbsName)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
content, err := b.ConvertToHTML(string(contentBytes))
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
fmt.Fprint(w, content)
} }
} }

View File

@ -73,12 +73,13 @@ func SubmitArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
} }
article := &b.Article{ article := &b.Article{
Title: r.PostFormValue("article-title"), Title: r.PostFormValue("article-title"),
Description: r.PostFormValue("article-description"), Description: r.PostFormValue("article-description"),
Content: r.PostFormValue("article-content"), Published: false,
Published: false, Rejected: false,
Rejected: false, AuthorID: session.Values["id"].(int64),
AuthorID: session.Values["id"].(int64), IsInIssue: r.PostFormValue("issue") == "on",
AutoGenerated: false,
} }
article.ID, err = db.AddArticle(article) article.ID, err = db.AddArticle(article)
@ -88,6 +89,20 @@ func SubmitArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
return return
} }
articleAbsName := fmt.Sprint(c.ArticleDir, "/", article.ID, ".md")
if err = os.WriteFile(articleAbsName, []byte(r.PostFormValue("article-content")), 0644); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
article.Link = fmt.Sprint(c.Domain, "/article/serve/", article.ID)
if err = db.UpdateAttributes(&b.Attribute{Table: "articles", ID: article.ID, AttName: "link", Value: article.Link}); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
r.ParseForm() r.ParseForm()
tags := make([]int64, 0) tags := make([]int64, 0)
for _, tag := range r.Form["tags"] { for _, tag := range r.Form["tags"] {
@ -129,11 +144,18 @@ func ResubmitArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
description := r.PostFormValue("article-description") description := r.PostFormValue("article-description")
content := r.PostFormValue("article-content") content := r.PostFormValue("article-content")
link := fmt.Sprint(c.ArticleDir, "/", id, ".md")
if err = os.WriteFile(link, []byte(content), 0644); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if err = db.UpdateAttributes( if err = db.UpdateAttributes(
&b.Attribute{Table: "articles", ID: id, AttName: "title", Value: title}, &b.Attribute{Table: "articles", ID: id, AttName: "title", Value: title},
&b.Attribute{Table: "articles", ID: id, AttName: "description", Value: description}, &b.Attribute{Table: "articles", ID: id, AttName: "description", Value: description},
&b.Attribute{Table: "articles", ID: id, AttName: "content", Value: content},
&b.Attribute{Table: "articles", ID: id, AttName: "rejected", Value: false}, &b.Attribute{Table: "articles", ID: id, AttName: "rejected", Value: false},
&b.Attribute{Table: "articles", ID: id, AttName: "is_in_issue", Value: r.PostFormValue("issue") == "on"},
); err != nil { ); err != nil {
log.Println(err) log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
@ -221,46 +243,35 @@ func ReviewUnpublishedArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.Hand
return return
} }
type htmlData struct { data := new(struct {
Title string Article *b.Article
Description string Content template.HTML
Content template.HTML Tags []*b.Tag
Tags []*b.Tag })
ID int64
}
var err error id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
data := new(htmlData)
data.ID, err = strconv.ParseInt(r.PathValue("id"), 10, 64)
if err != nil { if err != nil {
log.Println(err) log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
} }
article, err := db.GetArticle(data.ID) data.Article, err = db.GetArticle(id)
if err != nil { if err != nil {
log.Println(err) log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
} }
data.Title, err = b.ConvertToPlain(article.Title) articleAbsName := fmt.Sprint(c.ArticleDir, "/", data.Article.ID, ".md")
contentBytes, err := os.ReadFile(articleAbsName)
if err != nil { if err != nil {
log.Println(err) log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
} }
data.Description, err = b.ConvertToPlain(article.Description) content, err := b.ConvertToHTML(string(contentBytes))
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
content, err := b.ConvertToHTML(article.Content)
if err != nil { if err != nil {
log.Println(err) log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
@ -268,7 +279,7 @@ func ReviewUnpublishedArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.Hand
} }
data.Content = template.HTML(content) data.Content = template.HTML(content)
data.Tags, err = db.GetArticleTags(data.ID) data.Tags, err = db.GetArticleTags(data.Article.ID)
if err != nil { if err != nil {
log.Println(err) log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
@ -287,12 +298,12 @@ func ReviewRejectedArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.Handler
return return
} }
type htmlData struct { data := new(struct {
Selected map[int64]bool Selected map[int64]bool
Article *b.Article Article *b.Article
Content string
Tags []*b.Tag Tags []*b.Tag
} })
data := new(htmlData)
id, err := strconv.ParseInt(r.PathValue("id"), 10, 64) id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
if err != nil { if err != nil {
@ -308,6 +319,16 @@ func ReviewRejectedArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.Handler
return return
} }
articleAbsName := fmt.Sprint(c.ArticleDir, "/", data.Article.ID, ".md")
contentBytes, err := os.ReadFile(articleAbsName)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
data.Content = string(contentBytes)
data.Tags, err = db.GetTagList() data.Tags, err = db.GetTagList()
if err != nil { if err != nil {
log.Println(err) log.Println(err)
@ -342,7 +363,7 @@ func PublishArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
id, err := strconv.ParseInt(r.PathValue("id"), 10, 64) id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
if err != nil { if err != nil {
log.Println(err) log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusBadRequest)
return return
} }
@ -353,21 +374,7 @@ func PublishArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
return return
} }
content, err := b.ConvertToHTML(article.Content) if err = db.AddArticleToCurrentIssue(article.ID); err != nil {
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
err = os.WriteFile(fmt.Sprint(c.ArticleDir, "/", article.ID, ".html"), []byte(content), 0444)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if err = db.AddArticleToCurrentIssue(id); err != nil {
log.Println(err) log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
@ -504,9 +511,16 @@ func ShowPublishedArticles(c *b.Config, db *b.DB, s *b.CookieStore) http.Handler
return return
} }
filteredArticles := make([]*b.Article, 0)
for _, article := range publishedArticles {
if !article.AutoGenerated {
filteredArticles = append(filteredArticles, article)
}
}
tmpl, err := template.ParseFiles(c.WebDir + "/templates/published-articles.html") tmpl, err := template.ParseFiles(c.WebDir + "/templates/published-articles.html")
tmpl = template.Must(tmpl, err) tmpl = template.Must(tmpl, err)
tmpl.ExecuteTemplate(w, "page-content", publishedArticles) tmpl.ExecuteTemplate(w, "page-content", filteredArticles)
} }
} }
@ -555,7 +569,15 @@ func ReviewArticleForDeletion(c *b.Config, db *b.DB, s *b.CookieStore) http.Hand
return return
} }
content, err := b.ConvertToHTML(article.Content) articleAbsName := fmt.Sprint(c.ArticleDir, "/", article.ID, ".md")
contentBytes, err := os.ReadFile(articleAbsName)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
content, err := b.ConvertToHTML(string(contentBytes))
if err != nil { if err != nil {
log.Println(err) log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
@ -596,6 +618,12 @@ func DeleteArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
return return
} }
if err = os.Remove(fmt.Sprint(c.ArticleDir, "/", id, ".md")); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
feed, err := b.GenerateRSS(c, db) feed, err := b.GenerateRSS(c, db)
if err != nil { if err != nil {
log.Println(err) log.Println(err)

View File

@ -5,6 +5,7 @@ import (
"html/template" "html/template"
"io" "io"
"log" "log"
"mime"
"net/http" "net/http"
"os" "os"
"path/filepath" "path/filepath"
@ -36,29 +37,37 @@ func PublishLatestIssue(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFun
return return
} }
imgFileName := session.Values["issue-image"].(string)
imgAbsName := fmt.Sprint(c.PicsDir, "/", imgFileName)
imgFile, err := os.Open(imgAbsName)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer imgFile.Close()
imgInfo, err := imgFile.Stat()
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
imgSize := imgInfo.Size()
mimeType := mime.TypeByExtension(filepath.Ext(imgAbsName))
article := &b.Article{ article := &b.Article{
Title: "Autogenerated Issue Article", Title: r.PostFormValue("issue-title"),
Content: r.PostFormValue("issue-content"), EncURL: fmt.Sprint(c.Domain, "/image/serve/", imgFileName),
Link: session.Values["issue-image"].(string), EncLength: int(imgSize),
Published: true, EncType: mimeType,
Rejected: false, Published: true,
Created: time.Now(), Rejected: false,
AuthorID: session.Values["id"].(int64), Created: time.Now(),
} AuthorID: session.Values["id"].(int64),
fmt.Println(article.Link) AutoGenerated: true,
content, err := b.ConvertToHTML(article.Content)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
err = os.WriteFile(fmt.Sprint(c.ArticleDir, "/", article.ID, ".html"), []byte(content), 0444)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
} }
article.ID, err = db.AddArticle(article) article.ID, err = db.AddArticle(article)
@ -68,6 +77,26 @@ func PublishLatestIssue(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFun
return return
} }
articleAbsName := fmt.Sprint(c.ArticleDir, "/", article.ID, ".md")
if err = os.WriteFile(articleAbsName, []byte(r.PostFormValue("article-content")), 0644); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
article.Link = fmt.Sprint(c.Domain, "/article/serve/", article.ID)
if err = db.UpdateAttributes(&b.Attribute{Table: "articles", ID: article.ID, AttName: "link", Value: article.Link}); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if err = db.UpdateAttributes(&b.Attribute{Table: "articles", ID: article.ID, AttName: "link", Value: article.Link}); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if err = db.AddArticleToCurrentIssue(article.ID); err != nil { if err = db.AddArticleToCurrentIssue(article.ID); err != nil {
log.Println(err) log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)

59
cmd/frontend/pdf.go Normal file
View File

@ -0,0 +1,59 @@
package frontend
import (
"fmt"
"io"
"log"
"net/http"
"os"
"path/filepath"
"github.com/google/uuid"
b "streifling.com/jason/cpolis/cmd/backend"
)
func UploadPDF(c *b.Config, s *b.CookieStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if _, err := getSession(w, r, c, s); err != nil {
return
}
if err := r.ParseMultipartForm(10 << 20); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
file, _, err := r.FormFile("pdf-upload")
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer file.Close()
filename := fmt.Sprint(uuid.New(), ".pdf")
absFilepath, err := filepath.Abs(fmt.Sprint(c.PDFDir, "/", filename))
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
pdf, err := os.Create(absFilepath)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer pdf.Close()
if _, err = io.Copy(pdf, file); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
}
}

View File

@ -81,6 +81,7 @@ func main() {
mux.HandleFunc("POST /issue/publish", f.PublishLatestIssue(config, db, store)) mux.HandleFunc("POST /issue/publish", f.PublishLatestIssue(config, db, store))
mux.HandleFunc("POST /issue/upload-image", f.UploadIssueImage(config, store)) mux.HandleFunc("POST /issue/upload-image", f.UploadIssueImage(config, store))
mux.HandleFunc("POST /login", f.Login(config, db, store)) mux.HandleFunc("POST /login", f.Login(config, db, store))
mux.HandleFunc("POST /pdf/upload", f.UploadPDF(config, store))
mux.HandleFunc("POST /tag/add", f.AddTag(config, db, store)) mux.HandleFunc("POST /tag/add", f.AddTag(config, db, store))
mux.HandleFunc("POST /user/add", f.AddUser(config, db, store)) mux.HandleFunc("POST /user/add", f.AddUser(config, db, store))
mux.HandleFunc("POST /user/add-first", f.AddFirstUser(config, db, store)) mux.HandleFunc("POST /user/add-first", f.AddFirstUser(config, db, store))

View File

@ -21,15 +21,20 @@ CREATE TABLE issues (
); );
CREATE TABLE articles ( CREATE TABLE articles (
id INT AUTO_INCREMENT, id INT AUTO_INCREMENT,
title VARCHAR(255) NOT NULL, title VARCHAR(255) NOT NULL,
created TIMESTAMP DEFAULT CURRENT_TIMESTAMP, created TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
description TEXT NOT NULL, description TEXT NOT NULL,
link VARCHAR(255), link VARCHAR(255),
published BOOL NOT NULL, enc_url VARCHAR(255),
rejected BOOL NOT NULL, enc_length INT,
author_id INT NOT NULL, enc_type VARCHAR(255),
issue_id INT NOT NULL, published BOOL NOT NULL,
rejected BOOL NOT NULL,
author_id INT NOT NULL,
issue_id INT NOT NULL,
is_in_issue BOOL NOT NULL,
auto_generated BOOL NOT NULL,
PRIMARY KEY (id), PRIMARY KEY (id),
FOREIGN KEY (author_id) REFERENCES users (id), FOREIGN KEY (author_id) REFERENCES users (id),
FOREIGN KEY (issue_id) REFERENCES issues (id) FOREIGN KEY (issue_id) REFERENCES issues (id)

2
go.mod
View File

@ -4,7 +4,7 @@ go 1.22.0
require ( require (
firebase.google.com/go/v4 v4.14.1 firebase.google.com/go/v4 v4.14.1
git.streifling.com/jason/rss v0.1.2 git.streifling.com/jason/rss v0.1.3
github.com/BurntSushi/toml v1.3.2 github.com/BurntSushi/toml v1.3.2
github.com/go-sql-driver/mysql v1.7.1 github.com/go-sql-driver/mysql v1.7.1
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0

4
go.sum
View File

@ -15,8 +15,8 @@ cloud.google.com/go/storage v1.40.0 h1:VEpDQV5CJxFmJ6ueWNsKxcr1QAYOXEgxDa+sBbJah
cloud.google.com/go/storage v1.40.0/go.mod h1:Rrj7/hKlG87BLqDJYtwR0fbPld8uJPbQ2ucUMY7Ir0g= cloud.google.com/go/storage v1.40.0/go.mod h1:Rrj7/hKlG87BLqDJYtwR0fbPld8uJPbQ2ucUMY7Ir0g=
firebase.google.com/go/v4 v4.14.1 h1:4qiUETaFRWoFGE1XP5VbcEdtPX93Qs+8B/7KvP2825g= firebase.google.com/go/v4 v4.14.1 h1:4qiUETaFRWoFGE1XP5VbcEdtPX93Qs+8B/7KvP2825g=
firebase.google.com/go/v4 v4.14.1/go.mod h1:fgk2XshgNDEKaioKco+AouiegSI9oTWVqRaBdTTGBoM= firebase.google.com/go/v4 v4.14.1/go.mod h1:fgk2XshgNDEKaioKco+AouiegSI9oTWVqRaBdTTGBoM=
git.streifling.com/jason/rss v0.1.2 h1:UB3UHJXMt5WDDh9y8n0Z6nS1XortbPXjEr7QZTdovY4= git.streifling.com/jason/rss v0.1.3 h1:fd3j4ZtcLehapcmmroo3AP3X34gRHC4xzpfV6bDV1ZU=
git.streifling.com/jason/rss v0.1.2/go.mod h1:gpZF0nZbQSstMpyHD9DTAvlQEG7v4pjO5c7aIMWM4Jg= git.streifling.com/jason/rss v0.1.3/go.mod h1:gpZF0nZbQSstMpyHD9DTAvlQEG7v4pjO5c7aIMWM4Jg=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8= github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8=
github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=

View File

@ -43,13 +43,15 @@ chmod +x $CPOLIS_DIR/tailwindcss
$CPOLIS_DIR/tailwindcss -i $CPOLIS_DIR/web/static/css/input.css -o $CPOLIS_DIR/web/static/css/style.css $CPOLIS_DIR/tailwindcss -i $CPOLIS_DIR/web/static/css/input.css -o $CPOLIS_DIR/web/static/css/style.css
echo '\nBuilding cpolis...' >&2 echo '\nBuilding cpolis...' >&2
go build -o $TMP_DIR/cpolis $CPOLIS_DIR/cmd/main.go cd $CPOLIS_DIR
go build -o $BIN_DIR/cpolis cmd/main.go
cd
echo '\nSetting system files up...' >&2 echo '\nSetting up system files...' >&2
sudo mv $TMP_DIR/cpolis $BIN_DIR/cpolis
sudo chown root:root $BIN_DIR/cpolis sudo chown root:root $BIN_DIR/cpolis
chmod +x $BIN_DIR/cpolis chmod +x $BIN_DIR/cpolis
echo '\nSetting up service...' >&2
sudo mv $CPOLIS_DIR/cpolis.service $SYSTEMD_DIR sudo mv $CPOLIS_DIR/cpolis.service $SYSTEMD_DIR
sudo chown root:root $SYSTEMD_DIR/cpolis.service sudo chown root:root $SYSTEMD_DIR/cpolis.service
sudo systemctl daemon-reload sudo systemctl daemon-reload

View File

@ -17,7 +17,18 @@
<div> <div>
<h3>Cover</h3> <h3>Cover</h3>
<input id="image-upload" name="issue-image" type="file" required hx-post="/issue/upload-image" /> <div class="flex">
<label class="btn text-center" for="image-upload">Bild hochladen</label>
<input class="hidden" id="image-upload" name="issue-image" type="file" required
hx-post="/issue/upload-image" />
</div>
</div>
<div>
<h3>Titel</h3>
<div class="flex flex-col gap-y-1">
<input name="issue-title" type="text" />
</div>
</div> </div>
<div> <div>

View File

@ -21,6 +21,11 @@
<div> <div>
<span>Tags</span> <span>Tags</span>
<div class="flex flex-wrap gap-x-4"> <div class="flex flex-wrap gap-x-4">
<div>
<input id="issue" name="issue" type="checkbox" />
<label for="issue">Orient Express</label>
</div>
{{range .Tags}} {{range .Tags}}
<div> <div>
<input id="{{.Name}}" name="tags" type="checkbox" value="{{.ID}}" /> <input id="{{.Name}}" name="tags" type="checkbox" value="{{.ID}}" />

View File

@ -16,7 +16,7 @@
{{if lt . 3}} {{if lt . 3}}
<div class="mb-3"> <div class="mb-3">
<h2>Redakteur</h2> <h2>Redakteur</h2>
<div class="grid grid-cols-2 gap-4"> <div class="grid grid-cols-2 gap-x-4 gap-y-2">
<button class="btn" hx-get="/article/all-unpublished" hx-target="#page-content"> <button class="btn" hx-get="/article/all-unpublished" hx-target="#page-content">
Unveröffentlichte Artikel Unveröffentlichte Artikel
</button> </button>
@ -28,9 +28,14 @@
{{if lt . 2}} {{if lt . 2}}
<div class="mb-3"> <div class="mb-3">
<h2>Herausgeber</h2> <h2>Herausgeber</h2>
<div class="grid grid-cols-2 gap-4"> <div class="grid grid-cols-2 gap-x-4 gap-y-2">
<button class="btn" hx-get="/issue/this" hx-target="#page-content">Diese Ausgabe</button> <button class="btn" hx-get="/issue/this" hx-target="#page-content">Diese Ausgabe</button>
<button class="btn" hx-get="/article/all-published" hx-target="#page-content">Artikel löschen</button> <button class="btn" hx-get="/article/all-published" hx-target="#page-content">Artikel löschen</button>
<form class="flex" hx-encoding="multipart/form-data">
<label class="btn text-center" for="pdf-upload">PDF hochladen</label>
<input accept=".pdf" class="hidden" id="pdf-upload" name="pdf-upload" type="file"
hx-post="/pdf/upload" />
</form>
</div> </div>
</div> </div>
{{end}} {{end}}

View File

@ -24,6 +24,7 @@
<p class="text-center text-gray-500 dark:text-gray-400"> <p class="text-center text-gray-500 dark:text-gray-400">
&copy; 2024 Jason Streifling. Alle Rechte vorbehalten. &copy; 2024 Jason Streifling. Alle Rechte vorbehalten.
</p> </p>
<p class="text-center text-gray-500 dark:text-gray-400">v0.8.2</p>
<p class="text-center text-gray-500 dark:text-gray-400"> <p class="text-center text-gray-500 dark:text-gray-400">
<strong>Hinweis:</strong> Diese Software befindet sich noch in der Entwicklung und kann Fehler enthalten. <strong>Hinweis:</strong> Diese Software befindet sich noch in der Entwicklung und kann Fehler enthalten.
</p> </p>

View File

@ -14,13 +14,18 @@
<div class="flex flex-col gap-y-1"> <div class="flex flex-col gap-y-1">
<label for="easyMDE">Artikel</label> <label for="easyMDE">Artikel</label>
<textarea id="easyMDE">{{.Article.Content}}</textarea> <textarea id="easyMDE">{{.Content}}</textarea>
<input id="article-content" name="article-content" type="hidden" /> <input id="article-content" name="article-content" type="hidden" />
</div> </div>
<div> <div>
<span>Tags</span> <span>Tags</span>
<div class="flex flex-wrap gap-x-4"> <div class="flex flex-wrap gap-x-4">
<div>
<input id="issue" name="issue" type="checkbox" {{if .Article.IsInIssue}}checked{{end}} />
<label for="issue">Orient Express</label>
</div>
{{range .Tags}} {{range .Tags}}
<div> <div>
<input id="tag-{{.Name}}" name="tags" type="checkbox" value="{{.ID}}" {{if index $.Selected <input id="tag-{{.Name}}" name="tags" type="checkbox" value="{{.ID}}" {{if index $.Selected

View File

@ -4,12 +4,12 @@
<div> <div>
<span>Titel</span> <span>Titel</span>
<div class="bg-white border mb-3 px-2 py-2 rounded-md w-full"> <div class="bg-white border mb-3 px-2 py-2 rounded-md w-full">
{{.Title}} {{.Article.Title}}
</div> </div>
<span>Beschreibung</span> <span>Beschreibung</span>
<div class="bg-white border mb-3 px-2 py-2 rounded-md w-full"> <div class="bg-white border mb-3 px-2 py-2 rounded-md w-full">
{{.Description}} {{.Article.Description}}
</div> </div>
<span>Artikel</span> <span>Artikel</span>
@ -21,16 +21,21 @@
<span>Tags</span> <span>Tags</span>
<div class="bg-white border mb-3 px-2 py-2 rounded-md w-full"> <div class="bg-white border mb-3 px-2 py-2 rounded-md w-full">
{{if .Article.IsInIssue}}
<span>Orient Express</span>
<br>
{{end}}
{{range .Tags}} {{range .Tags}}
{{.Name}} <span>{{.Name}}</span>
<br> <br>
{{end}} {{end}}
</div> </div>
<div class="btn-area"> <div class="btn-area">
<input class="action-btn" type="submit" value="Veröffentlichen" hx-get="/article/publish/{{.ID}}" <input class="action-btn" type="submit" value="Veröffentlichen" hx-get="/article/publish/{{.Article.ID}}"
hx-target="#page-content" />
<input class="btn" type="submit" value="Ablehnen" hx-get="/article/reject/{{.Article.ID}}"
hx-target="#page-content" /> hx-target="#page-content" />
<input class="btn" type="submit" value="Ablehnen" hx-get="/article/reject/{{.ID}}" hx-target="#page-content" />
<button class="btn" hx-get="/hub" hx-target="#page-content">Zurück</button> <button class="btn" hx-get="/hub" hx-target="#page-content">Zurück</button>
</div> </div>
</div> </div>