From c59029cf3db59e345f225aac45ea176a230d1b91 Mon Sep 17 00:00:00 2001 From: Jason Streifling Date: Fri, 1 Nov 2024 16:22:31 +0100 Subject: [PATCH 01/18] Add rel="self" to Atom feed --- cmd/backend/atom.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cmd/backend/atom.go b/cmd/backend/atom.go index 6133818..5db78ae 100644 --- a/cmd/backend/atom.go +++ b/cmd/backend/atom.go @@ -12,7 +12,9 @@ func GenerateAtomFeed(c *Config, db *DB) (*string, error) { feed := atom.NewFeed(c.Title) feed.ID = atom.NewID("urn:feed:1") feed.Subtitle = atom.NewText("text", c.Description) - feed.AddLink(atom.NewLink(c.Link)) + + linkID := feed.AddLink(atom.NewLink(c.Link)) + feed.Links[linkID].Rel = "self" feed.Generator = atom.NewGenerator("cpolis") feed.Generator.URI = "https://git.streifling.com/jason/cpolis" From f663592019591cdc5daf9d3705eff0c85df67abb Mon Sep 17 00:00:00 2001 From: Jason Streifling Date: Fri, 1 Nov 2024 16:22:50 +0100 Subject: [PATCH 02/18] Changed version --- cmd/backend/config.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/backend/config.go b/cmd/backend/config.go index 8d52a8b..4d97bab 100644 --- a/cmd/backend/config.go +++ b/cmd/backend/config.go @@ -52,7 +52,7 @@ func newConfig() *Config { PDFDir: "/var/www/cpolis/pdfs", PicsDir: "/var/www/cpolis/pics", Port: ":8080", - Version: "v0.13.4", + Version: "v0.14.0", WebDir: "/var/www/cpolis/web", } } From dbddff6e55c8f7da1b6e5c847a8c70ecb54583e5 Mon Sep 17 00:00:00 2001 From: Jason Streifling Date: Fri, 1 Nov 2024 16:26:37 +0100 Subject: [PATCH 03/18] Delete content link from everywhere since it is only a combination of already saved info --- cmd/backend/articles.go | 19 +++++++++---------- cmd/backend/atom.go | 2 +- cmd/frontend/articles.go | 7 ------- cmd/frontend/issues.go | 13 ------------- create_db.sql | 1 - 5 files changed, 10 insertions(+), 32 deletions(-) diff --git a/cmd/backend/articles.go b/cmd/backend/articles.go index e5ef324..a15d5fd 100644 --- a/cmd/backend/articles.go +++ b/cmd/backend/articles.go @@ -13,7 +13,6 @@ type Article struct { Title string BannerLink string Summary string - ContentLink string ID int64 AuthorID int64 IssueID int64 @@ -30,8 +29,8 @@ func (db *DB) AddArticle(a *Article) (int64, error) { selectQuery := "SELECT id FROM issues WHERE published = false" insertQuery := ` INSERT INTO articles - (title, banner_link, summary, content_link, published, rejected, author_id, issue_id, edited_id, is_in_issue, auto_generated) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + (title, banner_link, summary, published, rejected, author_id, issue_id, edited_id, is_in_issue, auto_generated) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ` for i := 0; i < TxMaxRetries; i++ { @@ -48,7 +47,7 @@ func (db *DB) AddArticle(a *Article) (int64, error) { return 0, fmt.Errorf("error getting issue ID when adding article to DB: %v", err) } - result, err := tx.Exec(insertQuery, a.Title, a.BannerLink, a.Summary, a.ContentLink, a.Published, a.Rejected, a.AuthorID, id, a.EditedID, a.IsInIssue, a.AutoGenerated) + result, err := tx.Exec(insertQuery, a.Title, a.BannerLink, a.Summary, a.Published, a.Rejected, a.AuthorID, id, a.EditedID, a.IsInIssue, a.AutoGenerated) if err != nil { if rollbackErr := tx.Rollback(); rollbackErr != nil { log.Fatalf("transaction error: %v, rollback error: %v", err, rollbackErr) @@ -82,7 +81,7 @@ func (db *DB) AddArticle(a *Article) (int64, error) { func (db *DB) GetArticle(id int64) (*Article, error) { query := ` - SELECT title, created, banner_link, summary, content_link, published, author_id, issue_id, edited_id, is_in_issue, auto_generated + SELECT title, created, banner_link, summary, published, author_id, issue_id, edited_id, is_in_issue, auto_generated FROM articles WHERE id = ? ` @@ -92,7 +91,7 @@ func (db *DB) GetArticle(id int64) (*Article, error) { var created []byte var err error - if err := row.Scan(&article.Title, &created, &article.BannerLink, &article.Summary, &article.ContentLink, &article.Published, &article.AuthorID, &article.IssueID, &article.EditedID, &article.IsInIssue, &article.AutoGenerated); err != nil { + if err := row.Scan(&article.Title, &created, &article.BannerLink, &article.Summary, &article.Published, &article.AuthorID, &article.IssueID, &article.EditedID, &article.IsInIssue, &article.AutoGenerated); err != nil { return nil, fmt.Errorf("error scanning article row: %v", err) } @@ -107,7 +106,7 @@ func (db *DB) GetArticle(id int64) (*Article, error) { func (db *DB) GetCertainArticles(attribute string, value bool) ([]*Article, error) { query := fmt.Sprintf(` - SELECT id, title, created, banner_link, summary, content_link, author_id, issue_id, published, rejected, is_in_issue, auto_generated + SELECT id, title, created, banner_link, summary, author_id, issue_id, published, rejected, is_in_issue, auto_generated FROM articles WHERE %s = ? `, attribute) @@ -121,7 +120,7 @@ func (db *DB) GetCertainArticles(attribute string, value bool) ([]*Article, erro article := new(Article) var created []byte - if err = rows.Scan(&article.ID, &article.Title, &created, &article.BannerLink, &article.Summary, &article.ContentLink, &article.AuthorID, &article.IssueID, &article.Published, &article.Rejected, &article.IsInIssue, &article.AutoGenerated); err != nil { + if err = rows.Scan(&article.ID, &article.Title, &created, &article.BannerLink, &article.Summary, &article.AuthorID, &article.IssueID, &article.Published, &article.Rejected, &article.IsInIssue, &article.AutoGenerated); err != nil { return nil, fmt.Errorf("error scanning article row: %v", err) } @@ -141,7 +140,7 @@ func (db *DB) GetCurrentIssueArticles() ([]*Article, error) { txOptions := &sql.TxOptions{Isolation: sql.LevelSerializable} issueQuery := "SELECT id FROM issues WHERE published = false" articlesQuery := ` - SELECT id, title, created, banner_link, summary, content_link, author_id, auto_generated + SELECT id, title, created, banner_link, summary, author_id, auto_generated FROM articles WHERE issue_id = ? AND published = true AND is_in_issue = true ` @@ -174,7 +173,7 @@ func (db *DB) GetCurrentIssueArticles() ([]*Article, error) { article := new(Article) var created []byte - if err = rows.Scan(&article.ID, &article.Title, &created, &article.BannerLink, &article.Summary, &article.ContentLink, &article.AuthorID, &article.AutoGenerated); err != nil { + if err = rows.Scan(&article.ID, &article.Title, &created, &article.BannerLink, &article.Summary, &article.AuthorID, &article.AutoGenerated); err != nil { if rollbackErr := tx.Rollback(); rollbackErr != nil { log.Fatalf("transaction error: %v, rollback error: %v", err, rollbackErr) } diff --git a/cmd/backend/atom.go b/cmd/backend/atom.go index 5db78ae..4a894f4 100644 --- a/cmd/backend/atom.go +++ b/cmd/backend/atom.go @@ -33,7 +33,7 @@ func GenerateAtomFeed(c *Config, db *DB) (*string, error) { entry := atom.NewEntry(articleTitle) entry.ID = atom.NewID(fmt.Sprint("urn:entry:", article.ID)) entry.Published = atom.NewDate(article.Created) - entry.Content = atom.NewContent(atom.OutOfLine, "text/hmtl", article.ContentLink) + entry.Content = atom.NewContent(atom.OutOfLine, "text/hmtl", fmt.Sprint(c.Domain, "/article/serve/", article.ID)) if article.AutoGenerated { entry.Summary = atom.NewText("text", "automatically generated") diff --git a/cmd/frontend/articles.go b/cmd/frontend/articles.go index 305c26b..9886490 100644 --- a/cmd/frontend/articles.go +++ b/cmd/frontend/articles.go @@ -118,13 +118,6 @@ func SubmitArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc { return } - article.ContentLink = fmt.Sprint(c.Domain, "/article/serve/", article.ID) - if err = db.UpdateAttributes(&b.Attribute{Table: "articles", ID: article.ID, AttName: "content_link", Value: article.ContentLink}); err != nil { - log.Println(err) - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - r.ParseForm() tags := make([]int64, 0) for _, tag := range r.Form["tags"] { diff --git a/cmd/frontend/issues.go b/cmd/frontend/issues.go index a3f3ea4..fcb4c45 100644 --- a/cmd/frontend/issues.go +++ b/cmd/frontend/issues.go @@ -62,19 +62,6 @@ func PublishLatestIssue(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFun return } - article.ContentLink = fmt.Sprint(c.Domain, "/article/serve/", article.ID) - if err = db.UpdateAttributes(&b.Attribute{Table: "articles", ID: article.ID, AttName: "content_link", Value: article.ContentLink}); 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: "content_link", Value: article.ContentLink}); err != nil { - log.Println(err) - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - if err = db.AddArticleToCurrentIssue(article.ID); err != nil { log.Println(err) http.Error(w, err.Error(), http.StatusInternalServerError) diff --git a/create_db.sql b/create_db.sql index 44df846..0a0763c 100644 --- a/create_db.sql +++ b/create_db.sql @@ -28,7 +28,6 @@ CREATE TABLE articles ( created TIMESTAMP DEFAULT CURRENT_TIMESTAMP, banner_link VARCHAR(255) NOT NULL, summary TEXT NOT NULL, - content_link VARCHAR(255) NOT NULL, published BOOL NOT NULL, rejected BOOL NOT NULL, author_id INT NOT NULL, From b2a8578c72d3422d53c3278e02b78b4d0d32b168 Mon Sep 17 00:00:00 2001 From: Jason Streifling Date: Fri, 1 Nov 2024 16:31:47 +0100 Subject: [PATCH 04/18] Add profile pic and correct usage of banner link --- cmd/backend/atom.go | 5 +- cmd/backend/users.go | 32 +++++++------ cmd/frontend/articles.go | 21 +++------ cmd/frontend/images.go | 8 ++-- cmd/frontend/sessions.go | 12 ++++- cmd/frontend/users.go | 78 ++++++++++++++++++++++--------- cmd/main.go | 7 +-- create_db.sql | 4 +- web/static/css/input.css | 18 +++---- web/templates/add-user.html | 66 -------------------------- web/templates/current-issue.html | 6 +-- web/templates/edit-self.html | 53 --------------------- web/templates/edit-user.html | 71 ++++++++++++++++++---------- web/templates/editor.html | 24 +++++----- web/templates/first-user.html | 46 ------------------ web/templates/review-article.html | 2 +- 16 files changed, 176 insertions(+), 277 deletions(-) delete mode 100644 web/templates/add-user.html delete mode 100644 web/templates/edit-self.html delete mode 100644 web/templates/first-user.html diff --git a/cmd/backend/atom.go b/cmd/backend/atom.go index 4a894f4..ef2f12e 100644 --- a/cmd/backend/atom.go +++ b/cmd/backend/atom.go @@ -46,7 +46,7 @@ func GenerateAtomFeed(c *Config, db *DB) (*string, error) { } if len(article.BannerLink) > 0 { - linkID := entry.AddLink(atom.NewLink(article.BannerLink)) + linkID := entry.AddLink(atom.NewLink(c.Domain + "/image/serve/" + article.BannerLink)) entry.Links[linkID].Rel = "enclosure" entry.Links[linkID].Type = "image/webp" } @@ -55,7 +55,8 @@ func GenerateAtomFeed(c *Config, db *DB) (*string, error) { if err != nil { return nil, fmt.Errorf("error getting user user info for Atom feed: %v", err) } - entry.AddAuthor(atom.NewPerson(user.FirstName + " " + user.LastName)) + authorID := entry.AddAuthor(atom.NewPerson(user.FirstName + " " + user.LastName)) + entry.Authors[authorID].URI = c.Domain + "/image/serve/" + user.ProfilePicLink tags, err := db.GetArticleTags(article.ID) if err != nil { diff --git a/cmd/backend/users.go b/cmd/backend/users.go index 0677334..5a1c08e 100644 --- a/cmd/backend/users.go +++ b/cmd/backend/users.go @@ -149,11 +149,11 @@ func (db *DB) AddUser(c *Config, u *User, pass string) (int64, error) { } query := ` - INSERT INTO users (username, password, first_name, last_name, email, role) - VALUES (?, ?, ?, ?, ?, ?) + INSERT INTO users (username, password, first_name, last_name, email, profile_pic_link, role) + VALUES (?, ?, ?, ?, ?, ?, ?) ` - result, err := db.Exec(query, u.UserName, string(hashedPass), aesFirstName, aesLastName, aesEmail, u.Role) + result, err := db.Exec(query, u.UserName, string(hashedPass), aesFirstName, aesLastName, aesEmail, u.ProfilePicLink, u.Role) if err != nil { return 0, fmt.Errorf("error inserting new user %v into DB: %v", u.UserName, err) } @@ -253,13 +253,13 @@ func (db *DB) GetUser(c *Config, id int64) (*User, error) { user := new(User) query := ` - SELECT id, username, first_name, last_name, email, role + SELECT id, username, first_name, last_name, email, profile_pic_link, role FROM users WHERE id = ? ` row := db.QueryRow(query, id) - if err := row.Scan(&user.ID, &user.UserName, &aesFirstName, &aesLastName, &aesEmail, &user.Role); err != nil { + if err := row.Scan(&user.ID, &user.UserName, &aesFirstName, &aesLastName, &aesEmail, &user.ProfilePicLink, &user.Role); err != nil { return nil, fmt.Errorf("error reading user information: %v", err) } @@ -281,10 +281,10 @@ func (db *DB) GetUser(c *Config, id int64) (*User, error) { return user, nil } -func (db *DB) UpdateOwnUserAttributes(c *Config, id int64, userName, firstName, lastName, email, oldPass, newPass string) error { +func (db *DB) UpdateOwnUserAttributes(c *Config, id int64, userName, firstName, lastName, email, profilePicLink, oldPass, newPass string) error { var err error tx := new(Tx) - passwordEmpty := len(newPass) > 0 + passwordEmpty := len(newPass) == 0 for i := 0; i < TxMaxRetries; i++ { err := func() error { @@ -293,6 +293,7 @@ func (db *DB) UpdateOwnUserAttributes(c *Config, id int64, userName, firstName, return fmt.Errorf("error starting transaction: %v", err) } + fmt.Println(len(newPass), passwordEmpty) if !passwordEmpty { if err = tx.ChangePassword(id, oldPass, newPass); err != nil { if rollbackErr := tx.Rollback(); rollbackErr != nil { @@ -326,11 +327,13 @@ func (db *DB) UpdateOwnUserAttributes(c *Config, id int64, userName, firstName, return fmt.Errorf("error encrypting email: %v", err) } + fmt.Println("profilePicLink:", profilePicLink) if err = tx.UpdateAttributes( &Attribute{Table: "users", ID: id, AttName: "username", Value: userName}, &Attribute{Table: "users", ID: id, AttName: "first_name", Value: aesFirstName}, &Attribute{Table: "users", ID: id, AttName: "last_name", Value: aesLastName}, &Attribute{Table: "users", ID: id, AttName: "email", Value: aesEmail}, + &Attribute{Table: "users", ID: id, AttName: "profile_pic_link", Value: profilePicLink}, ); err != nil { if rollbackErr := tx.Rollback(); rollbackErr != nil { log.Fatalf("transaction error: %v, rollback error: %v", err, rollbackErr) @@ -360,8 +363,8 @@ func (db *DB) AddFirstUser(c *Config, u *User, pass string) (int64, error) { txOptions := &sql.TxOptions{Isolation: sql.LevelSerializable} selectQuery := "SELECT COUNT(*) FROM users" insertQuery := ` - INSERT INTO users (username, password, first_name, last_name, email, role) - VALUES (?, ?, ?, ?, ?, ?) + INSERT INTO users (username, password, first_name, last_name, email, profile_pic_link, role) + VALUES (?, ?, ?, ?, ?, ?, ?) ` for i := 0; i < TxMaxRetries; i++ { @@ -416,7 +419,7 @@ func (db *DB) AddFirstUser(c *Config, u *User, pass string) (int64, error) { return 0, fmt.Errorf("error encrypting email: %v", err) } - result, err := tx.Exec(insertQuery, u.UserName, string(hashedPass), aesFirstName, aesLastName, aesEmail, u.Role) + result, err := tx.Exec(insertQuery, u.UserName, string(hashedPass), aesFirstName, aesLastName, aesEmail, u.ProfilePicLink, u.Role) if err != nil { if rollbackErr := tx.Rollback(); rollbackErr != nil { log.Fatalf("transaction error: %v, rollback error: %v", err, rollbackErr) @@ -451,7 +454,7 @@ func (db *DB) GetAllUsers(c *Config) (map[int64]*User, error) { var aesFirstName, aesLastName, aesEmail string var err error - query := "SELECT id, username, first_name, last_name, email, role FROM users" + query := "SELECT id, username, first_name, last_name, email, profile_pic_link, role FROM users" rows, err := db.Query(query) if err != nil { @@ -461,7 +464,7 @@ func (db *DB) GetAllUsers(c *Config) (map[int64]*User, error) { users := make(map[int64]*User, 0) for rows.Next() { user := new(User) - if err = rows.Scan(&user.ID, &user.UserName, &aesFirstName, &aesLastName, &aesEmail, &user.Role); err != nil { + if err = rows.Scan(&user.ID, &user.UserName, &aesFirstName, &aesLastName, &aesEmail, &user.ProfilePicLink, &user.Role); err != nil { return nil, fmt.Errorf("error getting user info: %v", err) } @@ -506,10 +509,10 @@ func (tx *Tx) SetPassword(id int64, newPass string) error { return nil } -func (db *DB) UpdateUserAttributes(c *Config, id int64, userName, firstName, lastName, email, newPass string, role int) error { +func (db *DB) UpdateUserAttributes(c *Config, id int64, userName, firstName, lastName, email, profilePicLink, newPass string, role int) error { var err error tx := new(Tx) - passwordEmpty := len(newPass) > 0 + passwordEmpty := len(newPass) == 0 for i := 0; i < TxMaxRetries; i++ { err := func() error { @@ -556,6 +559,7 @@ func (db *DB) UpdateUserAttributes(c *Config, id int64, userName, firstName, las &Attribute{Table: "users", ID: id, AttName: "first_name", Value: aesFirstName}, &Attribute{Table: "users", ID: id, AttName: "last_name", Value: aesLastName}, &Attribute{Table: "users", ID: id, AttName: "email", Value: aesEmail}, + &Attribute{Table: "users", ID: id, AttName: "profile_pic_link", Value: profilePicLink}, &Attribute{Table: "users", ID: id, AttName: "role", Value: role}, ); err != nil { if rollbackErr := tx.Rollback(); rollbackErr != nil { diff --git a/cmd/frontend/articles.go b/cmd/frontend/articles.go index 9886490..019fcc6 100644 --- a/cmd/frontend/articles.go +++ b/cmd/frontend/articles.go @@ -7,7 +7,6 @@ import ( "net/http" "os" "strconv" - "strings" "time" b "streifling.com/jason/cpolis/cmd/backend" @@ -24,7 +23,7 @@ type EditorHTMLData struct { Action string ActionTitle string ActionButton string - BannerImage string + Image string HTMLContent template.HTML Article *b.Article Tags []*b.Tag @@ -80,7 +79,7 @@ func SubmitArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc { article := &b.Article{ Title: r.PostFormValue("article-title"), - BannerLink: c.Domain + "/image/serve/" + r.PostFormValue("article-banner-url"), + BannerLink: r.PostFormValue("article-banner-url"), Summary: r.PostFormValue("article-summary"), Published: false, Rejected: false, @@ -170,11 +169,6 @@ func ResubmitArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc { return } - bannerLink := r.PostFormValue("article-banner-url") - if len(bannerLink) != 0 { - bannerLink = c.Domain + "/image/serve/" + bannerLink - } - summary := r.PostFormValue("article-summary") if len(summary) == 0 { http.Error(w, "Bitte die Beschreibung eingeben.", http.StatusBadRequest) @@ -196,7 +190,7 @@ func ResubmitArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc { if err = db.UpdateAttributes( &b.Attribute{Table: "articles", ID: id, AttName: "title", Value: title}, - &b.Attribute{Table: "articles", ID: id, AttName: "banner_link", Value: bannerLink}, + &b.Attribute{Table: "articles", ID: id, AttName: "banner_link", Value: r.PostFormValue("article-banner-url")}, &b.Attribute{Table: "articles", ID: id, AttName: "summary", Value: summary}, &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"}, @@ -341,8 +335,7 @@ func ReviewRejectedArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.Handler return } - imgURL := strings.Split(data.Article.BannerLink, "/") - data.BannerImage = imgURL[len(imgURL)-1] + data.Image = data.Article.BannerLink articleAbsName := fmt.Sprint(c.ArticleDir, "/", data.Article.ID, ".md") content, err := os.ReadFile(articleAbsName) @@ -612,8 +605,7 @@ func ReviewArticle(c *b.Config, db *b.DB, s *b.CookieStore, action, title, butto return } - imgURL := strings.Split(article.BannerLink, "/") - data.BannerImage = imgURL[len(imgURL)-1] + data.Image = article.BannerLink data.Article.Summary, err = b.ConvertToPlain(article.Summary) if err != nil { @@ -789,8 +781,7 @@ func EditArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc { return } - imgURL := strings.Split(data.Article.BannerLink, "/") - data.BannerImage = imgURL[len(imgURL)-1] + data.Image = data.Article.BannerLink content, err := os.ReadFile(fmt.Sprint(c.ArticleDir, "/", data.Article.ID, ".md")) if err != nil { diff --git a/cmd/frontend/images.go b/cmd/frontend/images.go index d6aecbe..079929c 100644 --- a/cmd/frontend/images.go +++ b/cmd/frontend/images.go @@ -9,7 +9,7 @@ import ( b "streifling.com/jason/cpolis/cmd/backend" ) -func UploadImage(c *b.Config, s *b.CookieStore) http.HandlerFunc { +func UploadEasyMDEImage(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 { log.Println(err) @@ -42,7 +42,7 @@ func UploadImage(c *b.Config, s *b.CookieStore) http.HandlerFunc { } } -func UploadBanner(c *b.Config, s *b.CookieStore, fileKey, htmlFile, htmlTemplate string) http.HandlerFunc { +func UploadImage(c *b.Config, s *b.CookieStore, fileKey, htmlFile, htmlTemplate string) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if _, err := GetSession(w, r, c, s); err != nil { log.Println(err) @@ -69,8 +69,8 @@ func UploadBanner(c *b.Config, s *b.CookieStore, fileKey, htmlFile, htmlTemplate return } - data := new(struct{ BannerImage string }) - data.BannerImage = filename + data := new(struct{ Image string }) + data.Image = filename tmpl, err := template.ParseFiles(c.WebDir + "/templates/" + htmlFile) if err = template.Must(tmpl, err).ExecuteTemplate(w, htmlTemplate, data); err != nil { diff --git a/cmd/frontend/sessions.go b/cmd/frontend/sessions.go index ffef532..788a484 100644 --- a/cmd/frontend/sessions.go +++ b/cmd/frontend/sessions.go @@ -57,15 +57,21 @@ func HomePage(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc { } data := new(struct { + *UserHTMLData Version string - Role int }) + data.UserHTMLData = &UserHTMLData{User: new(b.User)} data.Version = c.Version files := make([]string, 2) files[0] = c.WebDir + "/templates/index.html" if numRows == 0 { - files[1] = c.WebDir + "/templates/first-user.html" + data.Role = b.NonExistent + data.Title = "Erster Benutzer (Administrator)" + data.ButtonText = "Anlegen" + data.URL = "/user/add-first" + + files[1] = c.WebDir + "/templates/edit-user.html" tmpl, err := template.ParseFiles(files...) if err = template.Must(tmpl, err).Execute(w, data); err != nil { log.Println(err) @@ -79,6 +85,7 @@ func HomePage(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc { http.Error(w, err.Error(), http.StatusInternalServerError) return } + if auth, ok := session.Values["authenticated"].(bool); auth && ok { data.Role = session.Values["role"].(int) files[1] = c.WebDir + "/templates/hub.html" @@ -89,6 +96,7 @@ func HomePage(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc { return } } else { + data.Role = b.Author files[1] = c.WebDir + "/templates/login.html" tmpl, err := template.ParseFiles(files...) if err = template.Must(tmpl, err).Execute(w, data); err != nil { diff --git a/cmd/frontend/users.go b/cmd/frontend/users.go index e3aed55..be3c978 100644 --- a/cmd/frontend/users.go +++ b/cmd/frontend/users.go @@ -10,6 +10,14 @@ import ( b "streifling.com/jason/cpolis/cmd/backend" ) +type UserHTMLData struct { + *b.User + Title string + ButtonText string + URL string + Image string +} + func checkUserStrings(user *b.User) (string, int, bool) { userLen := 63 // max value for utf-8 at 255 bytes nameLen := 56 // max value when aes encrypting utf-8 at up to 255 bytes @@ -33,8 +41,15 @@ func CreateUser(c *b.Config, s *b.CookieStore) http.HandlerFunc { return } - tmpl, err := template.ParseFiles(c.WebDir + "/templates/add-user.html") - if err = template.Must(tmpl, err).ExecuteTemplate(w, "page-content", nil); err != nil { + data := &UserHTMLData{ + User: &b.User{Role: b.Author}, + Title: "Neuer Benutzer", + ButtonText: "Anlegen", + URL: "/user/add", + } + + tmpl, err := template.ParseFiles(c.WebDir + "/templates/edit-user.html") + if err = template.Must(tmpl, err).ExecuteTemplate(w, "page-content", data); err != nil { log.Println(err) http.Error(w, err.Error(), http.StatusInternalServerError) return @@ -52,10 +67,11 @@ func AddUser(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc { } user := &b.User{ - UserName: r.PostFormValue("username"), - FirstName: r.PostFormValue("first-name"), - LastName: r.PostFormValue("last-name"), - Email: r.PostFormValue("email"), + UserName: r.PostFormValue("username"), + FirstName: r.PostFormValue("first-name"), + LastName: r.PostFormValue("last-name"), + Email: r.PostFormValue("email"), + ProfilePicLink: r.PostFormValue("profile-pic-url"), } pass := r.PostFormValue("password") pass2 := r.PostFormValue("password2") @@ -136,8 +152,16 @@ func EditSelf(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc { return } - tmpl, err := template.ParseFiles(c.WebDir + "/templates/edit-self.html") - if err = template.Must(tmpl, err).ExecuteTemplate(w, "page-content", user); err != nil { + data := &UserHTMLData{ + User: user, + Title: "Mein Profil bearbeiten", + ButtonText: "Übernehmen", + URL: "/user/update/self", + Image: user.ProfilePicLink, + } + + tmpl, err := template.ParseFiles(c.WebDir + "/templates/edit-user.html") + if err = template.Must(tmpl, err).ExecuteTemplate(w, "page-content", data); err != nil { log.Println(err) http.Error(w, err.Error(), http.StatusInternalServerError) return @@ -155,11 +179,12 @@ func UpdateSelf(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc { } user := &b.User{ - ID: session.Values["id"].(int64), - UserName: r.PostFormValue("username"), - FirstName: r.PostFormValue("first-name"), - LastName: r.PostFormValue("last-name"), - Email: r.PostFormValue("email"), + ID: session.Values["id"].(int64), + UserName: r.PostFormValue("username"), + FirstName: r.PostFormValue("first-name"), + LastName: r.PostFormValue("last-name"), + Email: r.PostFormValue("email"), + ProfilePicLink: r.PostFormValue("profile-pic-url"), } oldPass := r.PostFormValue("old-password") @@ -202,7 +227,7 @@ func UpdateSelf(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc { return } - if err = db.UpdateOwnUserAttributes(c, user.ID, user.UserName, user.FirstName, user.LastName, user.Email, oldPass, newPass); err != nil { + if err = db.UpdateOwnUserAttributes(c, user.ID, user.UserName, user.FirstName, user.LastName, user.Email, user.ProfilePicLink, oldPass, newPass); err != nil { log.Println("error: user:", user.ID, err) http.Error(w, "Benutzerdaten konnten nicht aktualisiert werden.", http.StatusInternalServerError) return @@ -225,11 +250,12 @@ func AddFirstUser(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var err error user := &b.User{ - UserName: r.PostFormValue("username"), - FirstName: r.PostFormValue("first-name"), - LastName: r.PostFormValue("last-name"), - Email: r.PostFormValue("email"), - Role: b.Admin, + UserName: r.PostFormValue("username"), + FirstName: r.PostFormValue("first-name"), + LastName: r.PostFormValue("last-name"), + Email: r.PostFormValue("email"), + ProfilePicLink: r.PostFormValue("profile-pic-url"), + Role: b.Admin, } pass := r.PostFormValue("password") pass2 := r.PostFormValue("password2") @@ -345,8 +371,16 @@ func EditUser(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc { return } + data := &UserHTMLData{ + User: user, + Title: "Profil von " + user.FirstName + " " + user.LastName + " bearbeiten", + ButtonText: "Übernehmen", + URL: fmt.Sprint("/user/update/", user.ID), + Image: user.ProfilePicLink, + } + tmpl, err := template.ParseFiles(c.WebDir + "/templates/edit-user.html") - if err = template.Must(tmpl, err).ExecuteTemplate(w, "page-content", user); err != nil { + if err = template.Must(tmpl, err).ExecuteTemplate(w, "page-content", data); err != nil { log.Println(err) http.Error(w, err.Error(), http.StatusInternalServerError) return @@ -402,6 +436,8 @@ func UpdateUser(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc { return } + user.ProfilePicLink = r.PostFormValue("profile-pic-url") + newPass := r.PostFormValue("password") newPass2 := r.PostFormValue("password2") if newPass != newPass2 { @@ -420,7 +456,7 @@ func UpdateUser(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc { return } - if err = db.UpdateUserAttributes(c, user.ID, user.UserName, user.FirstName, user.LastName, user.Email, newPass, user.Role); err != nil { + if err = db.UpdateUserAttributes(c, user.ID, user.UserName, user.FirstName, user.LastName, user.Email, user.ProfilePicLink, newPass, user.Role); err != nil { log.Println("error: user:", user.ID, err) http.Error(w, "Benutzerdaten konnten nicht aktualisiert werden.", http.StatusInternalServerError) return diff --git a/cmd/main.go b/cmd/main.go index e2f05db..8a43477 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -83,10 +83,10 @@ func main() { mux.HandleFunc("POST /article/resubmit/{id}", f.ResubmitArticle(config, db, store)) mux.HandleFunc("POST /article/submit", f.SubmitArticle(config, db, store)) - mux.HandleFunc("POST /article/upload-banner", f.UploadBanner(config, store, "article-banner", "editor.html", "article-banner-template")) - mux.HandleFunc("POST /article/upload-image", f.UploadImage(config, store)) + mux.HandleFunc("POST /article/upload-banner", f.UploadImage(config, store, "article-banner", "editor.html", "article-banner-template")) + mux.HandleFunc("POST /article/upload-image", f.UploadEasyMDEImage(config, store)) mux.HandleFunc("POST /issue/publish", f.PublishLatestIssue(config, db, store)) - mux.HandleFunc("POST /issue/upload-banner", f.UploadBanner(config, store, "issue-banner", "current-issue.html", "issue-banner-template")) + mux.HandleFunc("POST /issue/upload-banner", f.UploadImage(config, store, "issue-banner", "current-issue.html", "issue-banner-template")) 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)) @@ -94,6 +94,7 @@ func main() { mux.HandleFunc("POST /user/add-first", f.AddFirstUser(config, db, store)) mux.HandleFunc("POST /user/update/{id}", f.UpdateUser(config, db, store)) mux.HandleFunc("POST /user/update/self", f.UpdateSelf(config, db, store)) + mux.HandleFunc("POST /user/upload-profile-pic", f.UploadImage(config, store, "upload-profile-pic", "edit-user.html", "profile-pic-template")) log.Fatalln(http.ListenAndServe(config.Port, mux)) } diff --git a/create_db.sql b/create_db.sql index 0a0763c..f32822e 100644 --- a/create_db.sql +++ b/create_db.sql @@ -11,7 +11,7 @@ CREATE TABLE users ( first_name VARCHAR(255) NOT NULL, last_name VARCHAR(255) NOT NULL, email VARCHAR(255) NOT NULL, - -- profile_pic_link VARCHAR(255) NOT NULL, + profile_pic_link VARCHAR(255), role INT NOT NULL, PRIMARY KEY (id) ); @@ -26,7 +26,7 @@ CREATE TABLE articles ( id INT AUTO_INCREMENT, title VARCHAR(255) NOT NULL, created TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - banner_link VARCHAR(255) NOT NULL, + banner_link VARCHAR(255), summary TEXT NOT NULL, published BOOL NOT NULL, rejected BOOL NOT NULL, diff --git a/web/static/css/input.css b/web/static/css/input.css index b7778b5..b7475d6 100644 --- a/web/static/css/input.css +++ b/web/static/css/input.css @@ -24,7 +24,7 @@ textarea { } .btn-area { - @apply grid grid-cols-1 md:grid-cols-2 gap-x-4 gap-y-1 mt-4; + @apply grid grid-cols-1 md:grid-cols-2 gap-x-4 gap-y-1; } .btn-area-3 { @@ -40,33 +40,33 @@ textarea { } .EasyMDEContainer .CodeMirror { - @apply bg-slate-50 dark:bg-slate-950 border-slate-200 dark:border-slate-800 text-slate-900 dark:text-slate-100 + @apply bg-slate-50 dark:bg-slate-950 border-slate-200 dark:border-slate-800 text-slate-900 dark:text-slate-100; } .EasyMDEContainer .cm-s-easymde .CodeMirror-cursor { - @apply border-slate-900 dark:border-slate-100 + @apply border-slate-900 dark:border-slate-100; } .EasyMDEContainer .editor-toolbar > * { - @apply text-slate-900 dark:text-slate-100 + @apply text-slate-900 dark:text-slate-100; } .EasyMDEContainer .editor-toolbar > .active, .editor-toolbar > button:hover, .editor-preview pre, .cm-s-easymde .cm-comment { - @apply bg-slate-100 dark:bg-slate-900 + @apply bg-slate-100 dark:bg-slate-900; } .EasyMDEContainer .CodeMirror-fullscreen { - @apply bg-slate-50 dark:bg-slate-950 + @apply bg-slate-50 dark:bg-slate-950; } .editor-toolbar { - @apply border border-slate-200 dark:border-slate-800 + @apply border border-slate-200 dark:border-slate-800; } .editor-toolbar.fullscreen { - @apply bg-slate-50 dark:bg-slate-950 + @apply bg-slate-50 dark:bg-slate-950; } .editor-preview { - @apply bg-slate-50 dark:bg-slate-950 + @apply bg-slate-50 dark:bg-slate-950; } diff --git a/web/templates/add-user.html b/web/templates/add-user.html deleted file mode 100644 index 013b8eb..0000000 --- a/web/templates/add-user.html +++ /dev/null @@ -1,66 +0,0 @@ -{{define "page-content"}} -

Neuer Benutzer

- -
-
-
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
-
- -
-
- - -
-
- - -
-
- - -
-
- - -
-
- -
- - -
-
-{{end}} diff --git a/web/templates/current-issue.html b/web/templates/current-issue.html index 0c570de..54f0c05 100644 --- a/web/templates/current-issue.html +++ b/web/templates/current-issue.html @@ -79,8 +79,8 @@ {{end}} {{define "issue-banner-template"}} -
- Banner Image - +
+ +
{{end}} diff --git a/web/templates/edit-self.html b/web/templates/edit-self.html deleted file mode 100644 index c5de034..0000000 --- a/web/templates/edit-self.html +++ /dev/null @@ -1,53 +0,0 @@ -{{define "page-content"}} -

Profil bearbeiten

- -
-
-
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
-
- -
- - -
-
-{{end}} diff --git a/web/templates/edit-user.html b/web/templates/edit-user.html index 6725f2a..2366b31 100644 --- a/web/templates/edit-user.html +++ b/web/templates/edit-user.html @@ -1,67 +1,90 @@ {{define "page-content"}} -

Profil von {{.FirstName}} {{.LastName}} bearbeiten

+

{{.Title}}

+ +
+
+ {{template "profile-pic-template" .}} - -
- -
- -
- - -
- -
- - +
- +
- + +
+ +
+ + +
+ +
+ +
- +
- +
+ {{if lt .Role 4}}
- +
- +
- +
- +
+ {{end}}
- +
{{end}} + +{{define "profile-pic-template"}} +
+ + + +
+{{end}} diff --git a/web/templates/editor.html b/web/templates/editor.html index 5ccd247..b619163 100644 --- a/web/templates/editor.html +++ b/web/templates/editor.html @@ -1,7 +1,7 @@ {{define "page-content"}}

Editor

-
+
{{template "article-banner-template" .}} @@ -11,7 +11,7 @@
-
+
@@ -19,20 +19,14 @@
-
+
-
- - - -
-
Tags -
+
@@ -48,6 +42,12 @@
+
+ + + +
+
@@ -90,7 +90,7 @@ {{define "article-banner-template"}}
- - + +
{{end}} diff --git a/web/templates/first-user.html b/web/templates/first-user.html deleted file mode 100644 index 7019acc..0000000 --- a/web/templates/first-user.html +++ /dev/null @@ -1,46 +0,0 @@ -{{define "page-content"}} -

Erster Benutzer (Administrator)

- - -
-
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
-
- -
- -
- -{{end}} diff --git a/web/templates/review-article.html b/web/templates/review-article.html index 1266fd1..973f4d9 100644 --- a/web/templates/review-article.html +++ b/web/templates/review-article.html @@ -3,7 +3,7 @@
- Banner Image + Banner Image
Titel From 485eaaca706fbc9b0293472915c385a70c271908 Mon Sep 17 00:00:00 2001 From: Jason Streifling Date: Fri, 1 Nov 2024 16:32:42 +0100 Subject: [PATCH 05/18] Delete fmt.Println() uses --- cmd/backend/users.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/cmd/backend/users.go b/cmd/backend/users.go index 5a1c08e..a9f893e 100644 --- a/cmd/backend/users.go +++ b/cmd/backend/users.go @@ -293,7 +293,6 @@ func (db *DB) UpdateOwnUserAttributes(c *Config, id int64, userName, firstName, return fmt.Errorf("error starting transaction: %v", err) } - fmt.Println(len(newPass), passwordEmpty) if !passwordEmpty { if err = tx.ChangePassword(id, oldPass, newPass); err != nil { if rollbackErr := tx.Rollback(); rollbackErr != nil { @@ -327,7 +326,6 @@ func (db *DB) UpdateOwnUserAttributes(c *Config, id int64, userName, firstName, return fmt.Errorf("error encrypting email: %v", err) } - fmt.Println("profilePicLink:", profilePicLink) if err = tx.UpdateAttributes( &Attribute{Table: "users", ID: id, AttName: "username", Value: userName}, &Attribute{Table: "users", ID: id, AttName: "first_name", Value: aesFirstName}, From eb8e14ff6d27995e2b1a5d32f6f146526570571c Mon Sep 17 00:00:00 2001 From: Jason Streifling Date: Sat, 2 Nov 2024 16:24:39 +0100 Subject: [PATCH 06/18] Adapt DB layout to multiple authors and contributors --- create_db.sql | 67 +++++++++++++++++++++++++++++++-------------------- 1 file changed, 41 insertions(+), 26 deletions(-) diff --git a/create_db.sql b/create_db.sql index f32822e..2fc4dfa 100644 --- a/create_db.sql +++ b/create_db.sql @@ -5,50 +5,65 @@ DROP TABLE IF EXISTS issues; DROP TABLE IF EXISTS users; CREATE TABLE users ( - id INT AUTO_INCREMENT, - username VARCHAR(255) NOT NULL UNIQUE, - password VARCHAR(60) NOT NULL, - first_name VARCHAR(255) NOT NULL, - last_name VARCHAR(255) NOT NULL, - email VARCHAR(255) NOT NULL, - profile_pic_link VARCHAR(255), - role INT NOT NULL, + id INT AUTO_INCREMENT, + username VARCHAR(255) NOT NULL UNIQUE, + password VARCHAR(60) NOT NULL, + first_name VARCHAR(255) NOT NULL, + last_name VARCHAR(255) NOT NULL, + email VARCHAR(255) NOT NULL, + profile_pic_link VARCHAR(255), + role INT NOT NULL, PRIMARY KEY (id) ); CREATE TABLE issues ( - id INT AUTO_INCREMENT, - published BOOL NOT NULL, + id INT AUTO_INCREMENT, + published BOOL NOT NULL, PRIMARY KEY (id) ); CREATE TABLE articles ( - id INT AUTO_INCREMENT, - title VARCHAR(255) NOT NULL, - created TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - banner_link VARCHAR(255), - summary TEXT NOT NULL, - published BOOL NOT NULL, - rejected BOOL NOT NULL, - author_id INT NOT NULL, - issue_id INT NOT NULL, - edited_id INT, - is_in_issue BOOL NOT NULL, - auto_generated BOOL NOT NULL, + id INT AUTO_INCREMENT, + title VARCHAR(255) NOT NULL, + created TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + banner_link VARCHAR(255), + summary TEXT NOT NULL, + published BOOL NOT NULL, + rejected BOOL NOT NULL, + issue_id INT NOT NULL, + edited_id INT, + is_in_issue BOOL NOT NULL, + auto_generated BOOL NOT NULL, PRIMARY KEY (id), FOREIGN KEY (author_id) REFERENCES users (id), FOREIGN KEY (issue_id) REFERENCES issues (id) ); CREATE TABLE tags ( - id INT AUTO_INCREMENT, - name VARCHAR(50) NOT NULL UNIQUE, + id INT AUTO_INCREMENT, + name VARCHAR(50) NOT NULL UNIQUE, PRIMARY KEY (id) ); +CREATE TABLE articles_authors ( + article_id INT, + author_id INT, + PRIMARY KEY (article_id, author_id), + FOREIGN KEY (article_id) REFERENCES articles (id), + FOREIGN KEY (author_id) REFERENCES users (id) +); + +CREATE TABLE articles_contributors ( + article_id INT, + contributor_id INT, + PRIMARY KEY (article_id, contributor_id), + FOREIGN KEY (article_id) REFERENCES articles (id), + FOREIGN KEY (contributor_id) REFERENCES users (id) +); + CREATE TABLE articles_tags ( - article_id INT, - tag_id INT, + article_id INT, + tag_id INT, PRIMARY KEY (article_id, tag_id), FOREIGN KEY (article_id) REFERENCES articles (id), FOREIGN KEY (tag_id) REFERENCES tags (id) From 2078be920e16c252a168a393048d5f700bc2c2dd Mon Sep 17 00:00:00 2001 From: Jason Streifling Date: Sat, 2 Nov 2024 16:25:15 +0100 Subject: [PATCH 07/18] Change articles.go to not include an author --- cmd/backend/articles.go | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/cmd/backend/articles.go b/cmd/backend/articles.go index a15d5fd..63c0f48 100644 --- a/cmd/backend/articles.go +++ b/cmd/backend/articles.go @@ -14,7 +14,6 @@ type Article struct { BannerLink string Summary string ID int64 - AuthorID int64 IssueID int64 EditedID int64 Published bool @@ -29,8 +28,8 @@ func (db *DB) AddArticle(a *Article) (int64, error) { selectQuery := "SELECT id FROM issues WHERE published = false" insertQuery := ` INSERT INTO articles - (title, banner_link, summary, published, rejected, author_id, issue_id, edited_id, is_in_issue, auto_generated) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + (title, banner_link, summary, published, rejected, issue_id, edited_id, is_in_issue, auto_generated) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) ` for i := 0; i < TxMaxRetries; i++ { @@ -47,7 +46,7 @@ func (db *DB) AddArticle(a *Article) (int64, error) { return 0, fmt.Errorf("error getting issue ID when adding article to DB: %v", err) } - result, err := tx.Exec(insertQuery, a.Title, a.BannerLink, a.Summary, a.Published, a.Rejected, a.AuthorID, id, a.EditedID, a.IsInIssue, a.AutoGenerated) + result, err := tx.Exec(insertQuery, a.Title, a.BannerLink, a.Summary, a.Published, a.Rejected, id, a.EditedID, a.IsInIssue, a.AutoGenerated) if err != nil { if rollbackErr := tx.Rollback(); rollbackErr != nil { log.Fatalf("transaction error: %v, rollback error: %v", err, rollbackErr) @@ -81,7 +80,7 @@ func (db *DB) AddArticle(a *Article) (int64, error) { func (db *DB) GetArticle(id int64) (*Article, error) { query := ` - SELECT title, created, banner_link, summary, published, author_id, issue_id, edited_id, is_in_issue, auto_generated + SELECT title, created, banner_link, summary, published, issue_id, edited_id, is_in_issue, auto_generated FROM articles WHERE id = ? ` @@ -91,7 +90,7 @@ func (db *DB) GetArticle(id int64) (*Article, error) { var created []byte var err error - if err := row.Scan(&article.Title, &created, &article.BannerLink, &article.Summary, &article.Published, &article.AuthorID, &article.IssueID, &article.EditedID, &article.IsInIssue, &article.AutoGenerated); err != nil { + if err := row.Scan(&article.Title, &created, &article.BannerLink, &article.Summary, &article.Published, &article.IssueID, &article.EditedID, &article.IsInIssue, &article.AutoGenerated); err != nil { return nil, fmt.Errorf("error scanning article row: %v", err) } @@ -106,7 +105,7 @@ func (db *DB) GetArticle(id int64) (*Article, error) { func (db *DB) GetCertainArticles(attribute string, value bool) ([]*Article, error) { query := fmt.Sprintf(` - SELECT id, title, created, banner_link, summary, author_id, issue_id, published, rejected, is_in_issue, auto_generated + SELECT id, title, created, banner_link, summary, issue_id, published, rejected, is_in_issue, auto_generated FROM articles WHERE %s = ? `, attribute) @@ -120,7 +119,7 @@ func (db *DB) GetCertainArticles(attribute string, value bool) ([]*Article, erro article := new(Article) var created []byte - if err = rows.Scan(&article.ID, &article.Title, &created, &article.BannerLink, &article.Summary, &article.AuthorID, &article.IssueID, &article.Published, &article.Rejected, &article.IsInIssue, &article.AutoGenerated); err != nil { + if err = rows.Scan(&article.ID, &article.Title, &created, &article.BannerLink, &article.Summary, &article.IssueID, &article.Published, &article.Rejected, &article.IsInIssue, &article.AutoGenerated); err != nil { return nil, fmt.Errorf("error scanning article row: %v", err) } @@ -140,7 +139,7 @@ func (db *DB) GetCurrentIssueArticles() ([]*Article, error) { txOptions := &sql.TxOptions{Isolation: sql.LevelSerializable} issueQuery := "SELECT id FROM issues WHERE published = false" articlesQuery := ` - SELECT id, title, created, banner_link, summary, author_id, auto_generated + SELECT id, title, created, banner_link, summary, auto_generated FROM articles WHERE issue_id = ? AND published = true AND is_in_issue = true ` @@ -173,7 +172,7 @@ func (db *DB) GetCurrentIssueArticles() ([]*Article, error) { article := new(Article) var created []byte - if err = rows.Scan(&article.ID, &article.Title, &created, &article.BannerLink, &article.Summary, &article.AuthorID, &article.AutoGenerated); err != nil { + if err = rows.Scan(&article.ID, &article.Title, &created, &article.BannerLink, &article.Summary, &article.AutoGenerated); err != nil { if rollbackErr := tx.Rollback(); rollbackErr != nil { log.Fatalf("transaction error: %v, rollback error: %v", err, rollbackErr) } From 19da2ae60c7ad297cd0136191c9343354a49d803 Mon Sep 17 00:00:00 2001 From: Jason Streifling Date: Sat, 2 Nov 2024 16:25:47 +0100 Subject: [PATCH 08/18] Add articles_authors.go and articles_contributors.go --- cmd/backend/articles_authors.go | 114 +++++++++++++++++++++++++++ cmd/backend/articles_contributors.go | 114 +++++++++++++++++++++++++++ 2 files changed, 228 insertions(+) create mode 100644 cmd/backend/articles_authors.go create mode 100644 cmd/backend/articles_contributors.go diff --git a/cmd/backend/articles_authors.go b/cmd/backend/articles_authors.go new file mode 100644 index 0000000..d5b89f5 --- /dev/null +++ b/cmd/backend/articles_authors.go @@ -0,0 +1,114 @@ +package backend + +import ( + "fmt" + "log" +) + +func (db *DB) WriteArticleAuthors(articleID int64, authorIDs []int64) error { + query := "INSERT INTO articles_authors (article_id, author_id) VALUES (?, ?)" + + for i := 0; i < TxMaxRetries; i++ { + err := func() error { + tx, err := db.Begin() + if err != nil { + return fmt.Errorf("error starting transaction: %v", err) + } + + for _, authorID := range authorIDs { + if _, err := tx.Exec(query, articleID, authorID); err != nil { + if rollbackErr := tx.Rollback(); rollbackErr != nil { + log.Fatalf("transaction error: %v, rollback error: %v", err, rollbackErr) + } + return fmt.Errorf("error inserting into articles_authors: %v", err) + } + } + + if err = tx.Commit(); err != nil { + return fmt.Errorf("error committing transaction: %v", err) + } + return nil + }() + if err == nil { + return nil + } + + log.Println(err) + wait(i) + } + return fmt.Errorf("error: %v unsuccessful retries for DB operation, aborting", TxMaxRetries) +} + +func (db *DB) GetArticleAuthors(c *Config, articleID int64) ([]*User, error) { + query := ` + SELECT u.id + FROM articles a + INNER JOIN articles_tags at ON a.id = at.article_id + INNER JOIN users u ON at.author_id = u.id + WHERE a.id = ? + ` + rows, err := db.Query(query, articleID) + if err != nil { + return nil, fmt.Errorf("error querying articles_authors: %v", err) + } + + authors := make([]*User, 0) + for rows.Next() { + var authorID int64 + + if err = rows.Scan(&authorID); err != nil { + return nil, fmt.Errorf("error scanning rows: %v", err) + } + + author, err := db.GetUser(c, authorID) + if err != nil { + return nil, fmt.Errorf("error getting user info for article author: %v", err) + } + + authors = append(authors, author) + } + + return authors, nil +} + +func (db *DB) UpdateArticleAuthors(articleID int64, authorIDs []int64) error { + deleteQuery := "DELETE FROM articles_authors WHERE article_id = ?" + insertQuery := "INSERT INTO articles_authors (article_id, author_id) VALUES (?, ?)" + + for i := 0; i < TxMaxRetries; i++ { + err := func() error { + tx, err := db.Begin() + if err != nil { + return fmt.Errorf("error starting transaction: %v", err) + } + + if _, err := tx.Exec(deleteQuery, articleID); err != nil { + if rollbackErr := tx.Rollback(); rollbackErr != nil { + log.Fatalf("transaction error: %v, rollback error: %v", err, rollbackErr) + } + return fmt.Errorf("error deleting entries from articles_authors before inserting new ones: %v", err) + } + + for _, authorID := range authorIDs { + if _, err := tx.Exec(insertQuery, articleID, authorID); err != nil { + if rollbackErr := tx.Rollback(); rollbackErr != nil { + log.Fatalf("transaction error: %v, rollback error: %v", err, rollbackErr) + } + return fmt.Errorf("error inserting new entries into articles_authors: %v", err) + } + } + + if err = tx.Commit(); err != nil { + return fmt.Errorf("error committing transaction: %v", err) + } + return nil + }() + if err == nil { + return nil + } + + log.Println(err) + wait(i) + } + return fmt.Errorf("error: %v unsuccessful retries for DB operation, aborting", TxMaxRetries) +} diff --git a/cmd/backend/articles_contributors.go b/cmd/backend/articles_contributors.go new file mode 100644 index 0000000..2f3da69 --- /dev/null +++ b/cmd/backend/articles_contributors.go @@ -0,0 +1,114 @@ +package backend + +import ( + "fmt" + "log" +) + +func (db *DB) WriteArticleContributors(articleID int64, contributorIDs []int64) error { + query := "INSERT INTO articles_contributors (article_id, contributor_id) VALUES (?, ?)" + + for i := 0; i < TxMaxRetries; i++ { + err := func() error { + tx, err := db.Begin() + if err != nil { + return fmt.Errorf("error starting transaction: %v", err) + } + + for _, contributorID := range contributorIDs { + if _, err := tx.Exec(query, articleID, contributorID); err != nil { + if rollbackErr := tx.Rollback(); rollbackErr != nil { + log.Fatalf("transaction error: %v, rollback error: %v", err, rollbackErr) + } + return fmt.Errorf("error inserting into articles_contributors: %v", err) + } + } + + if err = tx.Commit(); err != nil { + return fmt.Errorf("error committing transaction: %v", err) + } + return nil + }() + if err == nil { + return nil + } + + log.Println(err) + wait(i) + } + return fmt.Errorf("error: %v unsuccessful retries for DB operation, aborting", TxMaxRetries) +} + +func (db *DB) GetArticleContributors(c *Config, articleID int64) ([]*User, error) { + query := ` + SELECT u.id + FROM articles a + INNER JOIN articles_tags at ON a.id = at.article_id + INNER JOIN users u ON at.contributor_id = u.id + WHERE a.id = ? + ` + rows, err := db.Query(query, articleID) + if err != nil { + return nil, fmt.Errorf("error querying articles_contributors: %v", err) + } + + contributors := make([]*User, 0) + for rows.Next() { + var contributorID int64 + + if err = rows.Scan(&contributorID); err != nil { + return nil, fmt.Errorf("error scanning rows: %v", err) + } + + contributor, err := db.GetUser(c, contributorID) + if err != nil { + return nil, fmt.Errorf("error getting user info for article contributor: %v", err) + } + + contributors = append(contributors, contributor) + } + + return contributors, nil +} + +func (db *DB) UpdateArticleContributors(articleID int64, contributorIDs []int64) error { + deleteQuery := "DELETE FROM articles_contributors WHERE article_id = ?" + insertQuery := "INSERT INTO articles_contributors (article_id, contributor_id) VALUES (?, ?)" + + for i := 0; i < TxMaxRetries; i++ { + err := func() error { + tx, err := db.Begin() + if err != nil { + return fmt.Errorf("error starting transaction: %v", err) + } + + if _, err := tx.Exec(deleteQuery, articleID); err != nil { + if rollbackErr := tx.Rollback(); rollbackErr != nil { + log.Fatalf("transaction error: %v, rollback error: %v", err, rollbackErr) + } + return fmt.Errorf("error deleting entries from articles_contributors before inserting new ones: %v", err) + } + + for _, contributorID := range contributorIDs { + if _, err := tx.Exec(insertQuery, articleID, contributorID); err != nil { + if rollbackErr := tx.Rollback(); rollbackErr != nil { + log.Fatalf("transaction error: %v, rollback error: %v", err, rollbackErr) + } + return fmt.Errorf("error inserting new entries into articles_contributors: %v", err) + } + } + + if err = tx.Commit(); err != nil { + return fmt.Errorf("error committing transaction: %v", err) + } + return nil + }() + if err == nil { + return nil + } + + log.Println(err) + wait(i) + } + return fmt.Errorf("error: %v unsuccessful retries for DB operation, aborting", TxMaxRetries) +} From 81f0e46ba637a15b9463d4a7a25031a3d28b0f29 Mon Sep 17 00:00:00 2001 From: Jason Streifling Date: Sat, 2 Nov 2024 16:26:02 +0100 Subject: [PATCH 09/18] Correct spelling mistake --- cmd/backend/atom.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/backend/atom.go b/cmd/backend/atom.go index ef2f12e..c8d9d1f 100644 --- a/cmd/backend/atom.go +++ b/cmd/backend/atom.go @@ -53,7 +53,7 @@ func GenerateAtomFeed(c *Config, db *DB) (*string, error) { user, err := db.GetUser(c, article.AuthorID) if err != nil { - return nil, fmt.Errorf("error getting user user info for Atom feed: %v", err) + return nil, fmt.Errorf("error getting user info for Atom feed: %v", err) } authorID := entry.AddAuthor(atom.NewPerson(user.FirstName + " " + user.LastName)) entry.Authors[authorID].URI = c.Domain + "/image/serve/" + user.ProfilePicLink From 8fb0733908cbb9a3ea0b854950bba86fa553c1f0 Mon Sep 17 00:00:00 2001 From: Jason Streifling Date: Sun, 1 Dec 2024 09:59:39 +0100 Subject: [PATCH 10/18] Make atom feed compatible with multiple authors --- cmd/backend/atom.go | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/cmd/backend/atom.go b/cmd/backend/atom.go index c8d9d1f..4b4b685 100644 --- a/cmd/backend/atom.go +++ b/cmd/backend/atom.go @@ -51,12 +51,18 @@ func GenerateAtomFeed(c *Config, db *DB) (*string, error) { entry.Links[linkID].Type = "image/webp" } - user, err := db.GetUser(c, article.AuthorID) + authors, err := db.GetArticleAuthors(c, article.ID) if err != nil { - return nil, fmt.Errorf("error getting user info for Atom feed: %v", err) + return nil, fmt.Errorf("error getting article's authors for Atom feed: %v", err) + } + for _, author := range authors { + user, err := db.GetUser(c, author.ID) + if err != nil { + return nil, fmt.Errorf("error getting user info for Atom feed: %v", err) + } + authorID := entry.AddAuthor(atom.NewPerson(user.FirstName + " " + user.LastName)) + entry.Authors[authorID].URI = c.Domain + "/image/serve/" + user.ProfilePicLink } - authorID := entry.AddAuthor(atom.NewPerson(user.FirstName + " " + user.LastName)) - entry.Authors[authorID].URI = c.Domain + "/image/serve/" + user.ProfilePicLink tags, err := db.GetArticleTags(article.ID) if err != nil { From 8d41caf40ad699b829b1b4bc2d8616fb7c175ca3 Mon Sep 17 00:00:00 2001 From: Jason Streifling Date: Sun, 1 Dec 2024 10:04:42 +0100 Subject: [PATCH 11/18] Make issue compatible with multiple authors --- cmd/frontend/issues.go | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/cmd/frontend/issues.go b/cmd/frontend/issues.go index fcb4c45..171ac9d 100644 --- a/cmd/frontend/issues.go +++ b/cmd/frontend/issues.go @@ -33,7 +33,6 @@ func PublishLatestIssue(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFun Published: true, Rejected: false, Created: time.Now(), - AuthorID: session.Values["id"].(int64), AutoGenerated: true, } @@ -49,6 +48,22 @@ func PublishLatestIssue(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFun return } + authorIDs := make([]int64, 1) + var ok bool + + if authorIDs[0], ok = session.Values["id"].(int64); !ok { + msg := "fälschlicherweise session.Values[\"id\"].(int64) für authorIDs[0] angenommen" + log.Println(msg) + http.Error(w, msg, http.StatusInternalServerError) + return + } + + if err = db.WriteArticleAuthors(article.ID, authorIDs); err != nil { + log.Println(err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + content := []byte(r.PostFormValue("issue-content")) if len(content) == 0 { http.Error(w, "Bitte eine Beschreibung eingeben.", http.StatusBadRequest) From 0a14545a19f342793ba18d70b76104d1bc79091d Mon Sep 17 00:00:00 2001 From: Jason Streifling Date: Fri, 27 Dec 2024 07:20:24 +0100 Subject: [PATCH 12/18] Bug fix --- cmd/backend/articles_authors.go | 4 ++-- cmd/backend/articles_contributors.go | 4 ++-- cmd/backend/articles_tags.go | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/cmd/backend/articles_authors.go b/cmd/backend/articles_authors.go index d5b89f5..0f4ef9e 100644 --- a/cmd/backend/articles_authors.go +++ b/cmd/backend/articles_authors.go @@ -43,8 +43,8 @@ func (db *DB) GetArticleAuthors(c *Config, articleID int64) ([]*User, error) { query := ` SELECT u.id FROM articles a - INNER JOIN articles_tags at ON a.id = at.article_id - INNER JOIN users u ON at.author_id = u.id + INNER JOIN articles_authors aa ON a.id = aa.article_id + INNER JOIN users u ON aa.author_id = u.id WHERE a.id = ? ` rows, err := db.Query(query, articleID) diff --git a/cmd/backend/articles_contributors.go b/cmd/backend/articles_contributors.go index 2f3da69..696c606 100644 --- a/cmd/backend/articles_contributors.go +++ b/cmd/backend/articles_contributors.go @@ -43,8 +43,8 @@ func (db *DB) GetArticleContributors(c *Config, articleID int64) ([]*User, error query := ` SELECT u.id FROM articles a - INNER JOIN articles_tags at ON a.id = at.article_id - INNER JOIN users u ON at.contributor_id = u.id + INNER JOIN articles_contributors ac ON a.id = ac.article_id + INNER JOIN users u ON ac.contributor_id = u.id WHERE a.id = ? ` rows, err := db.Query(query, articleID) diff --git a/cmd/backend/articles_tags.go b/cmd/backend/articles_tags.go index 72e7273..0cc9652 100644 --- a/cmd/backend/articles_tags.go +++ b/cmd/backend/articles_tags.go @@ -43,8 +43,8 @@ func (db *DB) GetArticleTags(articleID int64) ([]*Tag, error) { query := ` SELECT t.id, t.name FROM articles a - INNER JOIN articles_tags at ON a.id = at.article_id - INNER JOIN tags t ON at.tag_id = t.id + INNER JOIN articles_tags at ON a.id = at.article_id + INNER JOIN tags t ON at.tag_id = t.id WHERE a.id = ? ` rows, err := db.Query(query, articleID) From ca43ec1a810a09ef86ac58ee55ff2cde4696fb9d Mon Sep 17 00:00:00 2001 From: Jason Streifling Date: Fri, 27 Dec 2024 10:30:15 +0100 Subject: [PATCH 13/18] Add support for multiple authors and contributors --- cmd/backend/articles.go | 26 ++- cmd/backend/articles_authors.go | 11 + cmd/backend/articles_contributors.go | 11 + cmd/backend/atom.go | 15 ++ cmd/backend/users.go | 41 +++- cmd/frontend/articles.go | 304 +++++++++++++++++++++++---- cmd/frontend/users.go | 14 +- create_db.sql | 5 +- web/templates/editor.html | 46 +++- web/templates/review-article.html | 24 ++- 10 files changed, 436 insertions(+), 61 deletions(-) diff --git a/cmd/backend/articles.go b/cmd/backend/articles.go index 63c0f48..cfbc49c 100644 --- a/cmd/backend/articles.go +++ b/cmd/backend/articles.go @@ -5,6 +5,7 @@ import ( "database/sql" "fmt" "log" + "os" "time" ) @@ -14,6 +15,7 @@ type Article struct { BannerLink string Summary string ID int64 + CreatorID int64 IssueID int64 EditedID int64 Published bool @@ -28,8 +30,8 @@ func (db *DB) AddArticle(a *Article) (int64, error) { selectQuery := "SELECT id FROM issues WHERE published = false" insertQuery := ` INSERT INTO articles - (title, banner_link, summary, published, rejected, issue_id, edited_id, is_in_issue, auto_generated) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + (title, banner_link, summary, published, rejected, creator_id, issue_id, edited_id, is_in_issue, auto_generated) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ` for i := 0; i < TxMaxRetries; i++ { @@ -46,7 +48,7 @@ func (db *DB) AddArticle(a *Article) (int64, error) { return 0, fmt.Errorf("error getting issue ID when adding article to DB: %v", err) } - result, err := tx.Exec(insertQuery, a.Title, a.BannerLink, a.Summary, a.Published, a.Rejected, id, a.EditedID, a.IsInIssue, a.AutoGenerated) + result, err := tx.Exec(insertQuery, a.Title, a.BannerLink, a.Summary, a.Published, a.Rejected, a.CreatorID, id, a.EditedID, a.IsInIssue, a.AutoGenerated) if err != nil { if rollbackErr := tx.Rollback(); rollbackErr != nil { log.Fatalf("transaction error: %v, rollback error: %v", err, rollbackErr) @@ -80,7 +82,7 @@ func (db *DB) AddArticle(a *Article) (int64, error) { func (db *DB) GetArticle(id int64) (*Article, error) { query := ` - SELECT title, created, banner_link, summary, published, issue_id, edited_id, is_in_issue, auto_generated + SELECT title, created, banner_link, summary, published, creator_id, issue_id, edited_id, is_in_issue, auto_generated FROM articles WHERE id = ? ` @@ -90,7 +92,7 @@ func (db *DB) GetArticle(id int64) (*Article, error) { var created []byte var err error - if err := row.Scan(&article.Title, &created, &article.BannerLink, &article.Summary, &article.Published, &article.IssueID, &article.EditedID, &article.IsInIssue, &article.AutoGenerated); err != nil { + if err := row.Scan(&article.Title, &created, &article.BannerLink, &article.Summary, &article.Published, &article.CreatorID, &article.IssueID, &article.EditedID, &article.IsInIssue, &article.AutoGenerated); err != nil { return nil, fmt.Errorf("error scanning article row: %v", err) } @@ -105,7 +107,7 @@ func (db *DB) GetArticle(id int64) (*Article, error) { func (db *DB) GetCertainArticles(attribute string, value bool) ([]*Article, error) { query := fmt.Sprintf(` - SELECT id, title, created, banner_link, summary, issue_id, published, rejected, is_in_issue, auto_generated + SELECT id, title, created, banner_link, summary, creator_id, issue_id, published, rejected, is_in_issue, auto_generated FROM articles WHERE %s = ? `, attribute) @@ -119,7 +121,7 @@ func (db *DB) GetCertainArticles(attribute string, value bool) ([]*Article, erro article := new(Article) var created []byte - if err = rows.Scan(&article.ID, &article.Title, &created, &article.BannerLink, &article.Summary, &article.IssueID, &article.Published, &article.Rejected, &article.IsInIssue, &article.AutoGenerated); err != nil { + if err = rows.Scan(&article.ID, &article.Title, &created, &article.BannerLink, &article.Summary, &article.CreatorID, &article.IssueID, &article.Published, &article.Rejected, &article.IsInIssue, &article.AutoGenerated); err != nil { return nil, fmt.Errorf("error scanning article row: %v", err) } @@ -268,3 +270,13 @@ func (db *DB) DeleteArticle(id int64) error { return nil } + +func WriteArticleToFile(c *Config, articleID int64, content []byte) error { + articleAbsName := fmt.Sprint(c.ArticleDir, "/", articleID, ".md") + + if err := os.WriteFile(articleAbsName, content, 0644); err != nil { + return fmt.Errorf("error writing article %v to file: %v", articleID, err) + } + + return nil +} diff --git a/cmd/backend/articles_authors.go b/cmd/backend/articles_authors.go index 0f4ef9e..2b5d451 100644 --- a/cmd/backend/articles_authors.go +++ b/cmd/backend/articles_authors.go @@ -112,3 +112,14 @@ func (db *DB) UpdateArticleAuthors(articleID int64, authorIDs []int64) error { } return fmt.Errorf("error: %v unsuccessful retries for DB operation, aborting", TxMaxRetries) } + +func (db *DB) DeleteArticleAuthors(articleID int64) error { + query := "DELETE FROM articles_authors WHERE article_id = ?" + + _, err := db.Exec(query, articleID) + if err != nil { + return fmt.Errorf("error deleting articles_authors %v from DB: %v", articleID, err) + } + + return nil +} diff --git a/cmd/backend/articles_contributors.go b/cmd/backend/articles_contributors.go index 696c606..4f6182d 100644 --- a/cmd/backend/articles_contributors.go +++ b/cmd/backend/articles_contributors.go @@ -112,3 +112,14 @@ func (db *DB) UpdateArticleContributors(articleID int64, contributorIDs []int64) } return fmt.Errorf("error: %v unsuccessful retries for DB operation, aborting", TxMaxRetries) } + +func (db *DB) DeleteArticleContributors(articleID int64) error { + query := "DELETE FROM articles_contributors WHERE article_id = ?" + + _, err := db.Exec(query, articleID) + if err != nil { + return fmt.Errorf("error deleting articles_contributors %v from DB: %v", articleID, err) + } + + return nil +} diff --git a/cmd/backend/atom.go b/cmd/backend/atom.go index 4b4b685..f3ec0c5 100644 --- a/cmd/backend/atom.go +++ b/cmd/backend/atom.go @@ -60,10 +60,25 @@ func GenerateAtomFeed(c *Config, db *DB) (*string, error) { if err != nil { return nil, fmt.Errorf("error getting user info for Atom feed: %v", err) } + authorID := entry.AddAuthor(atom.NewPerson(user.FirstName + " " + user.LastName)) entry.Authors[authorID].URI = c.Domain + "/image/serve/" + user.ProfilePicLink } + contributors, err := db.GetArticleContributors(c, article.ID) + if err != nil { + return nil, fmt.Errorf("error getting article's contributors for Atom feed: %v", err) + } + for _, contributor := range contributors { + user, err := db.GetUser(c, contributor.ID) + if err != nil { + return nil, fmt.Errorf("error getting user info for Atom feed: %v", err) + } + + contributorID := entry.AddContributor(atom.NewPerson(user.FirstName + " " + user.LastName)) + entry.Contributors[contributorID].URI = c.Domain + "/image/serve/" + user.ProfilePicLink + } + tags, err := db.GetArticleTags(article.ID) if err != nil { return nil, fmt.Errorf("error getting tags for articles for Atom feed: %v", err) diff --git a/cmd/backend/users.go b/cmd/backend/users.go index a9f893e..47d76e4 100644 --- a/cmd/backend/users.go +++ b/cmd/backend/users.go @@ -448,7 +448,46 @@ func (db *DB) AddFirstUser(c *Config, u *User, pass string) (int64, error) { return 0, fmt.Errorf("error: %v unsuccessful retries for DB operation, aborting", TxMaxRetries) } -func (db *DB) GetAllUsers(c *Config) (map[int64]*User, error) { +func (db *DB) GetAllUsers(c *Config) ([]*User, error) { + var aesFirstName, aesLastName, aesEmail string + var err error + + query := "SELECT id, username, first_name, last_name, email, profile_pic_link, role FROM users" + + rows, err := db.Query(query) + if err != nil { + return nil, fmt.Errorf("error getting all users from DB: %v", err) + } + + users := make([]*User, 0) + for rows.Next() { + user := new(User) + if err = rows.Scan(&user.ID, &user.UserName, &aesFirstName, &aesLastName, &aesEmail, &user.ProfilePicLink, &user.Role); err != nil { + return nil, fmt.Errorf("error getting user info: %v", err) + } + + user.FirstName, err = aesDecrypt(c, aesFirstName) + if err != nil { + return nil, fmt.Errorf("error decrypting first name: %v", err) + } + + user.LastName, err = aesDecrypt(c, aesLastName) + if err != nil { + return nil, fmt.Errorf("error decrypting last name: %v", err) + } + + user.Email, err = aesDecrypt(c, aesEmail) + if err != nil { + return nil, fmt.Errorf("error decrypting email: %v", err) + } + + users = append(users, user) + } + + return users, nil +} + +func (db *DB) GetAllUsersMap(c *Config) (map[int64]*User, error) { var aesFirstName, aesLastName, aesEmail string var err error diff --git a/cmd/frontend/articles.go b/cmd/frontend/articles.go index 019fcc6..be873e4 100644 --- a/cmd/frontend/articles.go +++ b/cmd/frontend/articles.go @@ -7,6 +7,7 @@ import ( "net/http" "os" "strconv" + "strings" "time" b "streifling.com/jason/cpolis/cmd/backend" @@ -17,6 +18,17 @@ const ( PreviewMode ) +const ( + None = iota + Author + Contributor +) + +type ArticleUser struct { + *b.User + ArticleRole int +} + type EditorHTMLData struct { Selected map[int64]bool Content string @@ -27,6 +39,10 @@ type EditorHTMLData struct { HTMLContent template.HTML Article *b.Article Tags []*b.Tag + ArticleUsers map[string]*ArticleUser // A map is way more efficient in ReviewRejectedArticle() + Creator *ArticleUser + Authors []*b.User + Contributors []*b.User } func WriteArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc { @@ -40,11 +56,30 @@ func WriteArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc { var data *EditorHTMLData if session.Values["article"] == nil { - data = &EditorHTMLData{Action: "submit", Article: new(b.Article)} + data = &EditorHTMLData{Action: "submit", Article: new(b.Article), ArticleUsers: make(map[string]*ArticleUser)} } else { data = session.Values["article"].(*EditorHTMLData) } + users, err := db.GetAllUsers(c) + if err != nil { + log.Println(err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + for _, user := range users { + data.ArticleUsers[fmt.Sprint(user.LastName, user.FirstName, user.ID)] = &ArticleUser{User: user, ArticleRole: None} + } + + creator, err := db.GetUser(c, session.Values["id"].(int64)) + if err != nil { + log.Println(err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + data.Creator = data.ArticleUsers[fmt.Sprint(creator.LastName, creator.FirstName, creator.ID)] + delete(data.ArticleUsers, fmt.Sprint(creator.LastName, creator.FirstName, creator.ID)) + data.Tags, err = db.GetTagList() if err != nil { log.Println(err) @@ -81,9 +116,9 @@ func SubmitArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc { Title: r.PostFormValue("article-title"), BannerLink: r.PostFormValue("article-banner-url"), Summary: r.PostFormValue("article-summary"), + CreatorID: session.Values["id"].(int64), Published: false, Rejected: false, - AuthorID: session.Values["id"].(int64), IsInIssue: r.PostFormValue("issue") == "on", AutoGenerated: false, } @@ -97,6 +132,38 @@ func SubmitArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc { return } + r.ParseForm() + authors := make([]int64, 0) + contributors := make([]int64, 0) + + for key, values := range r.Form { + if strings.HasPrefix(key, "user-") && len(values) > 0 { + id, err := strconv.ParseInt(strings.Split(key, "-")[1], 10, 64) + if err != nil { + log.Println(err) + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + switch values[0] { + case "author": + authors = append(authors, id) + case "contributor": + contributors = append(contributors, id) + } + } + } + + if r.PostFormValue("creator") == "contributor" { + contributors = append(contributors, article.CreatorID) + } else { + authors = append(authors, article.CreatorID) + } + if len(authors) == 0 { + http.Error(w, "Es muss mindestens einen Autor geben.", http.StatusBadRequest) + return + } + article.ID, err = db.AddArticle(article) if err != nil { log.Println(err) @@ -109,23 +176,34 @@ func SubmitArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc { http.Error(w, "Bitte den Artikel eingeben.", http.StatusBadRequest) return } - - articleAbsName := fmt.Sprint(c.ArticleDir, "/", article.ID, ".md") - if err = os.WriteFile(articleAbsName, content, 0644); err != nil { + if err := b.WriteArticleToFile(c, article.ID, content); err != nil { log.Println(err) http.Error(w, err.Error(), http.StatusInternalServerError) return } - r.ParseForm() + if err = db.WriteArticleAuthors(article.ID, authors); err != nil { + log.Println(err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + if len(contributors) > 0 { + if err = db.WriteArticleContributors(article.ID, contributors); err != nil { + log.Println(err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + } + tags := make([]int64, 0) for _, tag := range r.Form["tags"] { tagID, err := strconv.ParseInt(tag, 10, 64) if err != nil { log.Println(err) - http.Error(w, err.Error(), http.StatusInternalServerError) + http.Error(w, err.Error(), http.StatusBadRequest) return } + tags = append(tags, tagID) } if err = db.WriteArticleTags(article.ID, tags); err != nil { @@ -156,32 +234,68 @@ func ResubmitArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc { return } - id, err := strconv.ParseInt(r.PathValue("id"), 10, 64) - if err != nil { - log.Println(err) - http.Error(w, err.Error(), http.StatusInternalServerError) - return + article := &b.Article{ + Title: r.PostFormValue("article-title"), + BannerLink: r.PostFormValue("article-banner-url"), + Summary: r.PostFormValue("article-summary"), + CreatorID: session.Values["id"].(int64), + IsInIssue: r.PostFormValue("issue") == "on", } - title := r.PostFormValue("article-title") - if len(title) == 0 { + if len(article.Title) == 0 { http.Error(w, "Bitte den Titel eingeben.", http.StatusBadRequest) return } - - summary := r.PostFormValue("article-summary") - if len(summary) == 0 { + if len(article.Summary) == 0 { http.Error(w, "Bitte die Beschreibung eingeben.", http.StatusBadRequest) return } + r.ParseForm() + authors := make([]int64, 0) + contributors := make([]int64, 0) + + for key, values := range r.Form { + if strings.HasPrefix(key, "user-") && len(values) > 0 { + id, err := strconv.ParseInt(strings.Split(key, "-")[1], 10, 64) + if err != nil { + log.Println(err) + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + switch values[0] { + case "author": + authors = append(authors, id) + case "contributor": + contributors = append(contributors, id) + } + } + } + + if r.PostFormValue("creator") == "contributor" { + contributors = append(contributors, article.CreatorID) + } else { + authors = append(authors, article.CreatorID) + } + if len(authors) == 0 { + http.Error(w, "Es muss mindestens einen Autor geben.", http.StatusBadRequest) + return + } + + article.ID, err = strconv.ParseInt(r.PathValue("id"), 10, 64) + if err != nil { + log.Println(err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + content := r.PostFormValue("article-content") if len(content) == 0 { http.Error(w, "Bitte den Artikel eingeben.", http.StatusBadRequest) return } - - contentLink := fmt.Sprint(c.ArticleDir, "/", id, ".md") + contentLink := fmt.Sprint(c.ArticleDir, "/", article.ID, ".md") if err = os.WriteFile(contentLink, []byte(content), 0644); err != nil { log.Println(err) http.Error(w, err.Error(), http.StatusInternalServerError) @@ -189,18 +303,30 @@ func ResubmitArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc { } if err = db.UpdateAttributes( - &b.Attribute{Table: "articles", ID: id, AttName: "title", Value: title}, - &b.Attribute{Table: "articles", ID: id, AttName: "banner_link", Value: r.PostFormValue("article-banner-url")}, - &b.Attribute{Table: "articles", ID: id, AttName: "summary", Value: summary}, - &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"}, + &b.Attribute{Table: "articles", ID: article.ID, AttName: "title", Value: article.Title}, + &b.Attribute{Table: "articles", ID: article.ID, AttName: "banner_link", Value: article.BannerLink}, + &b.Attribute{Table: "articles", ID: article.ID, AttName: "summary", Value: article.Summary}, + &b.Attribute{Table: "articles", ID: article.ID, AttName: "rejected", Value: false}, + &b.Attribute{Table: "articles", ID: article.ID, AttName: "is_in_issue", Value: article.IsInIssue}, ); err != nil { log.Println(err) http.Error(w, err.Error(), http.StatusInternalServerError) return } - r.ParseForm() + if err = db.UpdateArticleAuthors(article.ID, authors); err != nil { + log.Println(err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + if len(contributors) > 0 { + if err = db.UpdateArticleContributors(article.ID, contributors); err != nil { + log.Println(err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + } + tags := make([]int64, 0) for _, tag := range r.Form["tags"] { tagID, err := strconv.ParseInt(tag, 10, 64) @@ -211,7 +337,7 @@ func ResubmitArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc { } tags = append(tags, tagID) } - if err = db.UpdateArticleTags(id, tags); err != nil { + if err = db.UpdateArticleTags(article.ID, tags); err != nil { log.Println(err) http.Error(w, err.Error(), http.StatusInternalServerError) return @@ -297,7 +423,7 @@ func ShowRejectedArticles(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerF data.MyIDs = make(map[int64]bool) for _, article := range data.RejectedArticles { - if article.AuthorID == session.Values["id"].(int64) { + if article.CreatorID == session.Values["id"].(int64) { data.MyIDs[article.ID] = true } } @@ -314,7 +440,8 @@ func ShowRejectedArticles(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerF func ReviewRejectedArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - if _, err := GetSession(w, r, c, s); err != nil { + session, err := GetSession(w, r, c, s) + if err != nil { log.Println(err) http.Error(w, err.Error(), http.StatusInternalServerError) return @@ -353,6 +480,46 @@ func ReviewRejectedArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.Handler return } + data.ArticleUsers = make(map[string]*ArticleUser) + users, err := db.GetAllUsers(c) + if err != nil { + log.Println(err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + for _, user := range users { + data.ArticleUsers[fmt.Sprint(user.LastName, user.FirstName, user.ID)] = &ArticleUser{User: user, ArticleRole: None} + } + + authors, err := db.GetArticleAuthors(c, data.Article.ID) + if err != nil { + log.Println(err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + for _, author := range authors { + data.ArticleUsers[fmt.Sprint(author.LastName, author.FirstName, author.ID)].ArticleRole = Author + } + + contributors, err := db.GetArticleContributors(c, data.Article.ID) + if err != nil { + log.Println(err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + for _, contributor := range contributors { + data.ArticleUsers[fmt.Sprint(contributor.LastName, contributor.FirstName, contributor.ID)].ArticleRole = Contributor + } + + creator, err := db.GetUser(c, session.Values["id"].(int64)) + if err != nil { + log.Println(err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + data.Creator = data.ArticleUsers[fmt.Sprint(creator.LastName, creator.FirstName, creator.ID)] + delete(data.ArticleUsers, fmt.Sprint(creator.LastName, creator.FirstName, creator.ID)) + selectedTags, err := db.GetArticleTags(id) if err != nil { log.Println(err) @@ -406,9 +573,9 @@ func PublishArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc { } if err = db.UpdateAttributes( - &b.Attribute{Table: "articles", ID: id, AttName: "published", Value: true}, - &b.Attribute{Table: "articles", ID: id, AttName: "rejected", Value: false}, - &b.Attribute{Table: "articles", ID: id, AttName: "created", Value: time.Now().Format("2006-01-02 15:04:05")}, + &b.Attribute{Table: "articles", ID: article.ID, AttName: "published", Value: true}, + &b.Attribute{Table: "articles", ID: article.ID, AttName: "rejected", Value: false}, + &b.Attribute{Table: "articles", ID: article.ID, AttName: "created", Value: time.Now().Format("2006-01-02 15:04:05")}, ); err != nil { log.Println(err) http.Error(w, err.Error(), http.StatusInternalServerError) @@ -423,6 +590,18 @@ func PublishArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc { return } + if err = db.DeleteArticleContributors(oldArticle.ID); err != nil { + log.Println(err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + if err = db.DeleteArticleAuthors(oldArticle.ID); err != nil { + log.Println(err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + if err = db.DeleteArticle(oldArticle.ID); err != nil { log.Println(err) http.Error(w, err.Error(), http.StatusInternalServerError) @@ -435,10 +614,7 @@ func PublishArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc { return } - if err = db.UpdateAttributes( - &b.Attribute{Table: "articles", ID: id, AttName: "content_link", Value: fmt.Sprint(c.Domain, "/article/serve/", article.ID)}, - &b.Attribute{Table: "articles", ID: id, AttName: "edited_id", Value: 0}, - ); err != nil { + if err = db.UpdateAttributes(&b.Attribute{Table: "articles", ID: article.ID, AttName: "edited_id", Value: 0}); err != nil { log.Println(err) http.Error(w, err.Error(), http.StatusInternalServerError) return @@ -629,6 +805,22 @@ func ReviewArticle(c *b.Config, db *b.DB, s *b.CookieStore, action, title, butto } data.HTMLContent = template.HTML(data.Content) + data.Authors, err = db.GetArticleAuthors(c, data.Article.ID) + if err != nil { + log.Println(err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + sortUsersByName(data.Authors) + + data.Contributors, err = db.GetArticleContributors(c, data.Article.ID) + if err != nil { + log.Println(err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + sortUsersByName(data.Contributors) + data.Tags, err = db.GetArticleTags(id) if err != nil { log.Println(err) @@ -721,25 +913,59 @@ func AllowEditArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc return } - newArticle := oldArticle + newArticle := *oldArticle newArticle.Published = false newArticle.Rejected = true newArticle.EditedID = oldArticle.ID - newID, err := db.AddArticle(newArticle) + newArticle.ID, err = db.AddArticle(&newArticle) if err != nil { log.Println(err) http.Error(w, err.Error(), http.StatusInternalServerError) return } - if err = db.UpdateAttributes(&b.Attribute{Table: "articles", ID: oldID, AttName: "edited_id", Value: newID}); err != nil { + if err = db.UpdateAttributes(&b.Attribute{Table: "articles", ID: oldArticle.ID, AttName: "edited_id", Value: newArticle.ID}); err != nil { log.Println(err) http.Error(w, err.Error(), http.StatusInternalServerError) return } - if err = b.CopyFile(fmt.Sprint(c.ArticleDir, "/", oldID, ".md"), fmt.Sprint(c.ArticleDir, "/", newID, ".md")); err != nil { + src := fmt.Sprint(c.ArticleDir, "/", oldArticle.ID, ".md") + dst := fmt.Sprint(c.ArticleDir, "/", newArticle.ID, ".md") + if err = b.CopyFile(src, dst); err != nil { + log.Println(err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + authors, err := db.GetArticleAuthors(c, oldArticle.ID) + if err != nil { + log.Println(err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + authorIDs := make([]int64, len(authors)) + for i, author := range authors { + authorIDs[i] = author.ID + } + if err = db.WriteArticleAuthors(newArticle.ID, authorIDs); err != nil { + log.Println(err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + contributors, err := db.GetArticleContributors(c, oldArticle.ID) + if err != nil { + log.Println(err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + contributorIDs := make([]int64, len(contributors)) + for i, contributor := range contributors { + contributorIDs[i] = contributor.ID + } + if err = db.WriteArticleContributors(newArticle.ID, contributorIDs); err != nil { log.Println(err) http.Error(w, err.Error(), http.StatusInternalServerError) return diff --git a/cmd/frontend/users.go b/cmd/frontend/users.go index be3c978..39447a5 100644 --- a/cmd/frontend/users.go +++ b/cmd/frontend/users.go @@ -5,6 +5,7 @@ import ( "html/template" "log" "net/http" + "sort" "strconv" b "streifling.com/jason/cpolis/cmd/backend" @@ -33,6 +34,15 @@ func checkUserStrings(user *b.User) (string, int, bool) { } } +func sortUsersByName(users []*b.User) { + sort.SliceStable(users, func(i, j int) bool { + if users[i].LastName == users[j].LastName { + return users[i].FirstName < users[j].FirstName + } + return users[i].LastName < users[j].LastName + }) +} + func CreateUser(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 { @@ -332,14 +342,14 @@ func ShowAllUsers(c *b.Config, db *b.DB, s *b.CookieStore, action string) http.H }) data.Action = action - data.Users, err = db.GetAllUsers(c) + data.Users, err = db.GetAllUsersMap(c) if err != nil { log.Println(err) http.Error(w, err.Error(), http.StatusInternalServerError) return } - delete(data.Users, session.Values["id"].(int64)) + tmpl, err := template.ParseFiles(c.WebDir + "/templates/show-all-users.html") if err = template.Must(tmpl, err).ExecuteTemplate(w, "page-content", data); err != nil { log.Println(err) diff --git a/create_db.sql b/create_db.sql index 2fc4dfa..6f136ed 100644 --- a/create_db.sql +++ b/create_db.sql @@ -1,4 +1,6 @@ DROP TABLE IF EXISTS articles_tags; +DROP TABLE IF EXISTS articles_contributors; +DROP TABLE IF EXISTS articles_authors; DROP TABLE IF EXISTS tags; DROP TABLE IF EXISTS articles; DROP TABLE IF EXISTS issues; @@ -30,12 +32,13 @@ CREATE TABLE articles ( summary TEXT NOT NULL, published BOOL NOT NULL, rejected BOOL NOT NULL, + creator_id INT NOT NULL, issue_id INT NOT NULL, edited_id INT, is_in_issue BOOL NOT NULL, auto_generated BOOL NOT NULL, PRIMARY KEY (id), - FOREIGN KEY (author_id) REFERENCES users (id), + FOREIGN KEY (creator_id) REFERENCES users (id), FOREIGN KEY (issue_id) REFERENCES issues (id) ); diff --git a/web/templates/editor.html b/web/templates/editor.html index b619163..81cd163 100644 --- a/web/templates/editor.html +++ b/web/templates/editor.html @@ -7,7 +7,7 @@
- +

Titel

@@ -20,12 +20,18 @@
- +

Beschreibung

+
+

Artikel

+ + +
+
- Tags +

Tags

@@ -42,10 +48,36 @@
-
- - - +
+

Beteiligte

+ {{range .ArticleUsers}} +
+ {{.FirstName}} {{.LastName}}: + +
+ + +
+ +
+ + +
+ +
+ + +
+
+ {{end}} +
+ +
+ +
diff --git a/web/templates/review-article.html b/web/templates/review-article.html index 973f4d9..2c225e7 100644 --- a/web/templates/review-article.html +++ b/web/templates/review-article.html @@ -6,24 +6,24 @@ Banner Image
- Titel +

Titel

{{.Article.Title}}
- Beschreibung +

Beschreibung

{{.Article.Summary}}
- Artikel +

Artikel

{{.HTMLContent}}
- Tags +

Tags

{{if .Article.IsInIssue}} Orient Express @@ -35,6 +35,22 @@ {{end}}
+

Autoren

+
+ {{range .Authors}} + {{.FirstName}} {{.LastName}} +
+ {{end}} +
+ +

Mitwirkende

+
+ {{range .Contributors}} + {{.FirstName}} {{.LastName}} +
+ {{end}} +
+ {{if eq .Action "publish"}}
Date: Fri, 27 Dec 2024 10:37:19 +0100 Subject: [PATCH 14/18] Add backend support for clicks counter --- cmd/backend/articles.go | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/cmd/backend/articles.go b/cmd/backend/articles.go index cfbc49c..656e71e 100644 --- a/cmd/backend/articles.go +++ b/cmd/backend/articles.go @@ -18,6 +18,7 @@ type Article struct { CreatorID int64 IssueID int64 EditedID int64 + Clicks int Published bool Rejected bool IsInIssue bool @@ -30,8 +31,8 @@ func (db *DB) AddArticle(a *Article) (int64, error) { selectQuery := "SELECT id FROM issues WHERE published = false" insertQuery := ` INSERT INTO articles - (title, banner_link, summary, published, rejected, creator_id, issue_id, edited_id, is_in_issue, auto_generated) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + (title, banner_link, summary, published, rejected, creator_id, issue_id, edited_id, clicks, is_in_issue, auto_generated) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ` for i := 0; i < TxMaxRetries; i++ { @@ -48,7 +49,7 @@ func (db *DB) AddArticle(a *Article) (int64, error) { return 0, fmt.Errorf("error getting issue ID when adding article to DB: %v", err) } - result, err := tx.Exec(insertQuery, a.Title, a.BannerLink, a.Summary, a.Published, a.Rejected, a.CreatorID, id, a.EditedID, a.IsInIssue, a.AutoGenerated) + result, err := tx.Exec(insertQuery, a.Title, a.BannerLink, a.Summary, a.Published, a.Rejected, a.CreatorID, id, a.EditedID, 0, a.IsInIssue, a.AutoGenerated) if err != nil { if rollbackErr := tx.Rollback(); rollbackErr != nil { log.Fatalf("transaction error: %v, rollback error: %v", err, rollbackErr) @@ -82,7 +83,7 @@ func (db *DB) AddArticle(a *Article) (int64, error) { func (db *DB) GetArticle(id int64) (*Article, error) { query := ` - SELECT title, created, banner_link, summary, published, creator_id, issue_id, edited_id, is_in_issue, auto_generated + SELECT title, created, banner_link, summary, published, creator_id, issue_id, edited_id, clicks, is_in_issue, auto_generated FROM articles WHERE id = ? ` @@ -92,7 +93,7 @@ func (db *DB) GetArticle(id int64) (*Article, error) { var created []byte var err error - if err := row.Scan(&article.Title, &created, &article.BannerLink, &article.Summary, &article.Published, &article.CreatorID, &article.IssueID, &article.EditedID, &article.IsInIssue, &article.AutoGenerated); err != nil { + if err := row.Scan(&article.Title, &created, &article.BannerLink, &article.Summary, &article.Published, &article.CreatorID, &article.IssueID, &article.EditedID, &article.Clicks, &article.IsInIssue, &article.AutoGenerated); err != nil { return nil, fmt.Errorf("error scanning article row: %v", err) } @@ -107,7 +108,7 @@ func (db *DB) GetArticle(id int64) (*Article, error) { func (db *DB) GetCertainArticles(attribute string, value bool) ([]*Article, error) { query := fmt.Sprintf(` - SELECT id, title, created, banner_link, summary, creator_id, issue_id, published, rejected, is_in_issue, auto_generated + SELECT id, title, created, banner_link, summary, creator_id, issue_id, clicks, published, rejected, is_in_issue, auto_generated FROM articles WHERE %s = ? `, attribute) @@ -121,7 +122,7 @@ func (db *DB) GetCertainArticles(attribute string, value bool) ([]*Article, erro article := new(Article) var created []byte - if err = rows.Scan(&article.ID, &article.Title, &created, &article.BannerLink, &article.Summary, &article.CreatorID, &article.IssueID, &article.Published, &article.Rejected, &article.IsInIssue, &article.AutoGenerated); err != nil { + if err = rows.Scan(&article.ID, &article.Title, &created, &article.BannerLink, &article.Summary, &article.CreatorID, &article.IssueID, &article.Clicks, &article.Published, &article.Rejected, &article.IsInIssue, &article.AutoGenerated); err != nil { return nil, fmt.Errorf("error scanning article row: %v", err) } @@ -141,7 +142,7 @@ func (db *DB) GetCurrentIssueArticles() ([]*Article, error) { txOptions := &sql.TxOptions{Isolation: sql.LevelSerializable} issueQuery := "SELECT id FROM issues WHERE published = false" articlesQuery := ` - SELECT id, title, created, banner_link, summary, auto_generated + SELECT id, title, created, banner_link, summary, clicks, auto_generated FROM articles WHERE issue_id = ? AND published = true AND is_in_issue = true ` @@ -174,7 +175,7 @@ func (db *DB) GetCurrentIssueArticles() ([]*Article, error) { article := new(Article) var created []byte - if err = rows.Scan(&article.ID, &article.Title, &created, &article.BannerLink, &article.Summary, &article.AutoGenerated); err != nil { + if err = rows.Scan(&article.ID, &article.Title, &created, &article.BannerLink, &article.Summary, &article.Clicks, &article.AutoGenerated); err != nil { if rollbackErr := tx.Rollback(); rollbackErr != nil { log.Fatalf("transaction error: %v, rollback error: %v", err, rollbackErr) } From 82faacb9ec5218504c7f7be48ca74bd1ba7a5588 Mon Sep 17 00:00:00 2001 From: Jason Streifling Date: Fri, 27 Dec 2024 10:40:34 +0100 Subject: [PATCH 15/18] Let the article deletion logic happen entirely in the backend --- cmd/backend/articles.go | 14 +++++++++++++- cmd/backend/articles_authors.go | 11 ----------- cmd/backend/articles_contributors.go | 11 ----------- cmd/frontend/articles.go | 12 ------------ 4 files changed, 13 insertions(+), 35 deletions(-) diff --git a/cmd/backend/articles.go b/cmd/backend/articles.go index 656e71e..e8c5802 100644 --- a/cmd/backend/articles.go +++ b/cmd/backend/articles.go @@ -257,11 +257,23 @@ func (db *DB) AddArticleToCurrentIssue(id int64) error { func (db *DB) DeleteArticle(id int64) error { articlesTagsQuery := "DELETE FROM articles_tags WHERE article_id = ?" + articlesContributorsQuery := "DELETE FROM articles_contributors WHERE article_id = ?" + articlesAuthorsQuery := "DELETE FROM articles_authors WHERE article_id = ?" articlesQuery := "DELETE FROM articles WHERE id = ?" _, err := db.Exec(articlesTagsQuery, id) if err != nil { - return fmt.Errorf("error deleting article %v from DB: %v", id, err) + return fmt.Errorf("error deleting articles_tags %v from DB: %v", id, err) + } + + _, err = db.Exec(articlesContributorsQuery, id) + if err != nil { + return fmt.Errorf("error deleting articles_contributors %v from DB: %v", id, err) + } + + _, err = db.Exec(articlesAuthorsQuery, id) + if err != nil { + return fmt.Errorf("error deleting articles_authors %v from DB: %v", id, err) } _, err = db.Exec(articlesQuery, id) diff --git a/cmd/backend/articles_authors.go b/cmd/backend/articles_authors.go index 2b5d451..0f4ef9e 100644 --- a/cmd/backend/articles_authors.go +++ b/cmd/backend/articles_authors.go @@ -112,14 +112,3 @@ func (db *DB) UpdateArticleAuthors(articleID int64, authorIDs []int64) error { } return fmt.Errorf("error: %v unsuccessful retries for DB operation, aborting", TxMaxRetries) } - -func (db *DB) DeleteArticleAuthors(articleID int64) error { - query := "DELETE FROM articles_authors WHERE article_id = ?" - - _, err := db.Exec(query, articleID) - if err != nil { - return fmt.Errorf("error deleting articles_authors %v from DB: %v", articleID, err) - } - - return nil -} diff --git a/cmd/backend/articles_contributors.go b/cmd/backend/articles_contributors.go index 4f6182d..696c606 100644 --- a/cmd/backend/articles_contributors.go +++ b/cmd/backend/articles_contributors.go @@ -112,14 +112,3 @@ func (db *DB) UpdateArticleContributors(articleID int64, contributorIDs []int64) } return fmt.Errorf("error: %v unsuccessful retries for DB operation, aborting", TxMaxRetries) } - -func (db *DB) DeleteArticleContributors(articleID int64) error { - query := "DELETE FROM articles_contributors WHERE article_id = ?" - - _, err := db.Exec(query, articleID) - if err != nil { - return fmt.Errorf("error deleting articles_contributors %v from DB: %v", articleID, err) - } - - return nil -} diff --git a/cmd/frontend/articles.go b/cmd/frontend/articles.go index be873e4..5d768a3 100644 --- a/cmd/frontend/articles.go +++ b/cmd/frontend/articles.go @@ -590,18 +590,6 @@ func PublishArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc { return } - if err = db.DeleteArticleContributors(oldArticle.ID); err != nil { - log.Println(err) - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - if err = db.DeleteArticleAuthors(oldArticle.ID); err != nil { - log.Println(err) - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - if err = db.DeleteArticle(oldArticle.ID); err != nil { log.Println(err) http.Error(w, err.Error(), http.StatusInternalServerError) From 376a1264f5c8923d623bd7ce11c5670c61e799a3 Mon Sep 17 00:00:00 2001 From: Jason Streifling Date: Fri, 27 Dec 2024 10:52:57 +0100 Subject: [PATCH 16/18] Add further support for clicks counter --- cmd/calls/articles.go | 13 ++++++++++++- create_db.sql | 1 + 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/cmd/calls/articles.go b/cmd/calls/articles.go index 1568cdd..449365c 100644 --- a/cmd/calls/articles.go +++ b/cmd/calls/articles.go @@ -50,6 +50,17 @@ func ServeArticle(c *b.Config, db *b.DB) http.HandlerFunc { return } - fmt.Fprint(w, content) + article.Clicks++ + if err = db.UpdateAttributes(&b.Attribute{Table: "articles", ID: article.ID, AttName: "clicks", Value: article.Clicks}); err != nil { + log.Println(err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + if _, err = fmt.Fprint(w, content); err != nil { + log.Println(err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } } } diff --git a/create_db.sql b/create_db.sql index 6f136ed..4ad9067 100644 --- a/create_db.sql +++ b/create_db.sql @@ -35,6 +35,7 @@ CREATE TABLE articles ( creator_id INT NOT NULL, issue_id INT NOT NULL, edited_id INT, + clicks INT NOT NULL, is_in_issue BOOL NOT NULL, auto_generated BOOL NOT NULL, PRIMARY KEY (id), From 523bdb24cd9eee4a4641e5197d32c522b05d6060 Mon Sep 17 00:00:00 2001 From: Jason Streifling Date: Fri, 27 Dec 2024 10:53:06 +0100 Subject: [PATCH 17/18] Cleanup --- cmd/frontend/articles.go | 1 + create_db.sql | 14 +++++++------- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/cmd/frontend/articles.go b/cmd/frontend/articles.go index 5d768a3..7610777 100644 --- a/cmd/frontend/articles.go +++ b/cmd/frontend/articles.go @@ -121,6 +121,7 @@ func SubmitArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc { Rejected: false, IsInIssue: r.PostFormValue("issue") == "on", AutoGenerated: false, + EditedID: 0, } if len(article.Title) == 0 { diff --git a/create_db.sql b/create_db.sql index 4ad9067..a79e89c 100644 --- a/create_db.sql +++ b/create_db.sql @@ -34,7 +34,7 @@ CREATE TABLE articles ( rejected BOOL NOT NULL, creator_id INT NOT NULL, issue_id INT NOT NULL, - edited_id INT, + edited_id INT NOT NULL, clicks INT NOT NULL, is_in_issue BOOL NOT NULL, auto_generated BOOL NOT NULL, @@ -50,24 +50,24 @@ CREATE TABLE tags ( ); CREATE TABLE articles_authors ( - article_id INT, - author_id INT, + article_id INT NOT NULL, + author_id INT NOT NULL, PRIMARY KEY (article_id, author_id), FOREIGN KEY (article_id) REFERENCES articles (id), FOREIGN KEY (author_id) REFERENCES users (id) ); CREATE TABLE articles_contributors ( - article_id INT, - contributor_id INT, + article_id INT NOT NULL, + contributor_id INT NOT NULL, PRIMARY KEY (article_id, contributor_id), FOREIGN KEY (article_id) REFERENCES articles (id), FOREIGN KEY (contributor_id) REFERENCES users (id) ); CREATE TABLE articles_tags ( - article_id INT, - tag_id INT, + article_id INT NOT NULL, + tag_id INT NOT NULL, PRIMARY KEY (article_id, tag_id), FOREIGN KEY (article_id) REFERENCES articles (id), FOREIGN KEY (tag_id) REFERENCES tags (id) From 544dddc8933a365c41905cbf3b886adf58e68c6c Mon Sep 17 00:00:00 2001 From: Jason Streifling Date: Fri, 27 Dec 2024 11:09:36 +0100 Subject: [PATCH 18/18] Add a way to get info about clicks --- cmd/calls/articles.go | 49 +++++++++++++++++++++++++++++++++++++++++-- cmd/main.go | 1 + 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/cmd/calls/articles.go b/cmd/calls/articles.go index 449365c..4b5d518 100644 --- a/cmd/calls/articles.go +++ b/cmd/calls/articles.go @@ -10,6 +10,27 @@ import ( b "streifling.com/jason/cpolis/cmd/backend" ) +func incrementClicks(db *b.DB, a *b.Article) error { + a.Clicks++ + if err := db.UpdateAttributes(&b.Attribute{Table: "articles", ID: a.ID, AttName: "clicks", Value: a.Clicks}); err != nil { + return fmt.Errorf("error updating click attribute of article %v: %v", a.ID, err) + } + + if a.IsInIssue { + issue, err := db.GetArticle(a.IssueID) + if err != nil { + return fmt.Errorf("error getting issue %v: %v", a.IssueID, err) + } + + issue.Clicks++ + if err := db.UpdateAttributes(&b.Attribute{Table: "articles", ID: issue.ID, AttName: "clicks", Value: issue.Clicks}); err != nil { + return fmt.Errorf("error updating click attribute of issue %v: %v", issue.ID, err) + } + } + + return nil +} + func ServeArticle(c *b.Config, db *b.DB) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if !tokenIsVerified(w, r, c) { @@ -50,8 +71,7 @@ func ServeArticle(c *b.Config, db *b.DB) http.HandlerFunc { return } - article.Clicks++ - if err = db.UpdateAttributes(&b.Attribute{Table: "articles", ID: article.ID, AttName: "clicks", Value: article.Clicks}); err != nil { + if err = incrementClicks(db, article); err != nil { log.Println(err) http.Error(w, err.Error(), http.StatusInternalServerError) return @@ -64,3 +84,28 @@ func ServeArticle(c *b.Config, db *b.DB) http.HandlerFunc { } } } + +func ServeClicks(db *b.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + idString := r.PathValue("id") + id, err := strconv.ParseInt(idString, 10, 64) + if err != nil { + log.Println(err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + article, err := db.GetArticle(id) + if err != nil { + log.Println(err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + if _, err = fmt.Fprint(w, article.Clicks); err != nil { + log.Println(err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + } +} diff --git a/cmd/main.go b/cmd/main.go index 8a43477..c10ae2f 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -65,6 +65,7 @@ func main() { mux.HandleFunc("GET /article/review-rejected/{id}", f.ReviewRejectedArticle(config, db, store)) mux.HandleFunc("GET /article/review-unpublished/{id}", f.ReviewArticle(config, db, store, "publish", "Artikel veröffentlichen", "Veröffentlichen")) mux.HandleFunc("GET /article/serve/{id}", c.ServeArticle(config, db)) + mux.HandleFunc("GET /article/serve/{id}/clicks", c.ServeClicks(db)) mux.HandleFunc("GET /article/write", f.WriteArticle(config, db, store)) mux.HandleFunc("GET /atom/serve", c.ServeAtomFeed(config)) mux.HandleFunc("GET /hub", f.ShowHub(config, db, store))