feature/id-for-hmtl-headers #3
@@ -18,6 +18,7 @@ type Article struct {
 | 
			
		||||
	ID            int64
 | 
			
		||||
	AuthorID      int64
 | 
			
		||||
	IssueID       int64
 | 
			
		||||
	EditedID      int64
 | 
			
		||||
	EncLength     int
 | 
			
		||||
	Published     bool
 | 
			
		||||
	Rejected      bool
 | 
			
		||||
@@ -31,8 +32,9 @@ func (db *DB) AddArticle(a *Article) (int64, error) {
 | 
			
		||||
	selectQuery := "SELECT id FROM issues WHERE published = false"
 | 
			
		||||
	insertQuery := `
 | 
			
		||||
    INSERT INTO articles
 | 
			
		||||
        (title, description, link, enc_url, enc_length, enc_type, published, rejected, author_id, issue_id, is_in_issue, auto_generated)
 | 
			
		||||
    VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
 | 
			
		||||
        (title, description, link, enc_url, enc_length, enc_type, published,
 | 
			
		||||
        rejected, author_id, issue_id, edited_id, is_in_issue, auto_generated)
 | 
			
		||||
    VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
 | 
			
		||||
    `
 | 
			
		||||
 | 
			
		||||
	for i := 0; i < TxMaxRetries; i++ {
 | 
			
		||||
@@ -51,7 +53,7 @@ func (db *DB) AddArticle(a *Article) (int64, error) {
 | 
			
		||||
 | 
			
		||||
			result, err := tx.Exec(insertQuery, a.Title, a.Description, a.Link,
 | 
			
		||||
				a.EncURL, a.EncLength, a.EncType, a.Published, a.Rejected, a.AuthorID, id,
 | 
			
		||||
				a.IsInIssue, a.AutoGenerated)
 | 
			
		||||
				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)
 | 
			
		||||
@@ -85,7 +87,9 @@ func (db *DB) AddArticle(a *Article) (int64, error) {
 | 
			
		||||
 | 
			
		||||
func (db *DB) GetArticle(id int64) (*Article, error) {
 | 
			
		||||
	query := `
 | 
			
		||||
    SELECT title, created, description, link, enc_url, enc_length, enc_type, published, author_id, issue_id, is_in_issue, auto_generated
 | 
			
		||||
    SELECT
 | 
			
		||||
        title, created, description, link, enc_url, enc_length, enc_type,
 | 
			
		||||
        published, author_id, issue_id, edited_id, is_in_issue, auto_generated
 | 
			
		||||
    FROM articles
 | 
			
		||||
    WHERE id = ?
 | 
			
		||||
    `
 | 
			
		||||
@@ -97,7 +101,7 @@ func (db *DB) GetArticle(id int64) (*Article, error) {
 | 
			
		||||
 | 
			
		||||
	if err := row.Scan(&article.Title, &created, &article.Description,
 | 
			
		||||
		&article.Link, &article.EncURL, &article.EncLength, &article.EncType,
 | 
			
		||||
		&article.Published, &article.AuthorID, &article.IssueID,
 | 
			
		||||
		&article.Published, &article.AuthorID, &article.IssueID, &article.EditedID,
 | 
			
		||||
		&article.IsInIssue, &article.AutoGenerated); err != nil {
 | 
			
		||||
		return nil, fmt.Errorf("error scanning article row: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
@@ -111,14 +115,15 @@ func (db *DB) GetArticle(id int64) (*Article, error) {
 | 
			
		||||
	return article, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (db *DB) GetCertainArticles(published, rejected bool) ([]*Article, error) {
 | 
			
		||||
	query := `
 | 
			
		||||
    SELECT id, title, created, description, link, enc_url, enc_length, enc_type, author_id, issue_id, is_in_issue, auto_generated
 | 
			
		||||
func (db *DB) GetCertainArticles(attribute string, value bool) ([]*Article, error) {
 | 
			
		||||
	query := fmt.Sprintf(`
 | 
			
		||||
    SELECT
 | 
			
		||||
        id, title, created, description, link, enc_url, enc_length, enc_type,
 | 
			
		||||
        author_id, issue_id, published, rejected, is_in_issue, auto_generated
 | 
			
		||||
    FROM articles
 | 
			
		||||
    WHERE published = ?
 | 
			
		||||
    AND rejected = ?
 | 
			
		||||
    `
 | 
			
		||||
	rows, err := db.Query(query, published, rejected)
 | 
			
		||||
    WHERE %s = ?
 | 
			
		||||
    `, attribute)
 | 
			
		||||
	rows, err := db.Query(query, value)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, fmt.Errorf("error querying articles: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
@@ -130,12 +135,11 @@ func (db *DB) GetCertainArticles(published, rejected bool) ([]*Article, error) {
 | 
			
		||||
 | 
			
		||||
		if err = rows.Scan(&article.ID, &article.Title, &created,
 | 
			
		||||
			&article.Description, &article.Link, &article.EncURL, &article.EncLength,
 | 
			
		||||
			&article.EncType, &article.AuthorID, &article.IssueID,
 | 
			
		||||
			&article.IsInIssue, &article.AutoGenerated); err != nil {
 | 
			
		||||
			&article.EncType, &article.AuthorID, &article.IssueID, &article.Published,
 | 
			
		||||
			&article.Rejected, &article.IsInIssue, &article.AutoGenerated); err != nil {
 | 
			
		||||
			return nil, fmt.Errorf("error scanning article row: %v", err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		article.Published = false
 | 
			
		||||
		article.Created, err = time.Parse("2006-01-02 15:04:05", string(created))
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, fmt.Errorf("error parsing created: %v", err)
 | 
			
		||||
@@ -152,7 +156,9 @@ 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, description, link, enc_url, enc_length, enc_type, author_id, auto_generated
 | 
			
		||||
    SELECT
 | 
			
		||||
        id, title, created, description, link, enc_url, enc_length, enc_type,
 | 
			
		||||
        author_id, auto_generated
 | 
			
		||||
    FROM articles
 | 
			
		||||
    WHERE issue_id = ? AND published = true AND is_in_issue = true
 | 
			
		||||
    `
 | 
			
		||||
@@ -269,14 +275,13 @@ func (db *DB) AddArticleToCurrentIssue(id int64) error {
 | 
			
		||||
 | 
			
		||||
func (db *DB) DeleteArticle(id int64) error {
 | 
			
		||||
	articlesTagsQuery := "DELETE FROM articles_tags 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)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	articlesQuery := "DELETE FROM articles WHERE id = ?"
 | 
			
		||||
 | 
			
		||||
	_, err = db.Exec(articlesQuery, id)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return fmt.Errorf("error deleting article %v from DB: %v", id, err)
 | 
			
		||||
 
 | 
			
		||||
@@ -17,7 +17,7 @@ func GenerateRSS(c *Config, db *DB) (*string, error) {
 | 
			
		||||
		Items:       make([]*rss.Item, 0),
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	articles, err := db.GetCertainArticles(true, false)
 | 
			
		||||
	articles, err := db.GetCertainArticles("published", true)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, fmt.Errorf("error getting published articles for RSS feed: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
 
 | 
			
		||||
@@ -22,6 +22,38 @@ const (
 | 
			
		||||
	PreviewMode
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type EditorHTMLData struct {
 | 
			
		||||
	Selected     map[int64]bool
 | 
			
		||||
	Content      string
 | 
			
		||||
	Action       string
 | 
			
		||||
	ActionTitle  string
 | 
			
		||||
	ActionButton string
 | 
			
		||||
	HTMLContent  template.HTML
 | 
			
		||||
	Article      *b.Article
 | 
			
		||||
	Tags         []*b.Tag
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func copyFile(src, dst string) error {
 | 
			
		||||
	srcFile, err := os.Open(src)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return fmt.Errorf("error opening source file: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
	defer srcFile.Close()
 | 
			
		||||
 | 
			
		||||
	dstFile, err := os.Create(dst)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return fmt.Errorf("error opening destination file: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
	defer dstFile.Close()
 | 
			
		||||
 | 
			
		||||
	_, err = io.Copy(dstFile, srcFile)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return fmt.Errorf("error copying file: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return dstFile.Sync()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func WriteArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
 | 
			
		||||
	return func(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
		session, err := getSession(w, r, c, s)
 | 
			
		||||
@@ -29,22 +61,14 @@ func WriteArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		type htmlData struct {
 | 
			
		||||
			Title       string
 | 
			
		||||
			Description string
 | 
			
		||||
			Content     string
 | 
			
		||||
			HTMLContent template.HTML
 | 
			
		||||
			Tags        []*b.Tag
 | 
			
		||||
			Mode        int
 | 
			
		||||
		}
 | 
			
		||||
		var data htmlData
 | 
			
		||||
		data := new(EditorHTMLData)
 | 
			
		||||
 | 
			
		||||
		if session.Values["article"] == nil {
 | 
			
		||||
			data = htmlData{}
 | 
			
		||||
			data = &EditorHTMLData{Article: new(b.Article)}
 | 
			
		||||
		} else {
 | 
			
		||||
			data = session.Values["article"].(htmlData)
 | 
			
		||||
			data = session.Values["article"].(*EditorHTMLData)
 | 
			
		||||
		}
 | 
			
		||||
		data.Mode = EditMode
 | 
			
		||||
		// data.Mode = EditMode
 | 
			
		||||
 | 
			
		||||
		data.Tags, err = db.GetTagList()
 | 
			
		||||
		if err != nil {
 | 
			
		||||
@@ -53,6 +77,8 @@ func WriteArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		data.Action = "submit"
 | 
			
		||||
 | 
			
		||||
		tmpl, err := template.ParseFiles(c.WebDir + "/templates/editor.html")
 | 
			
		||||
		template.Must(tmpl, err).ExecuteTemplate(w, "page-content", data)
 | 
			
		||||
	}
 | 
			
		||||
@@ -185,22 +211,41 @@ func ResubmitArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func ShowUnpublishedArticles(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
 | 
			
		||||
func ShowUnpublishedUnrejectedAndPublishedRejectedArticles(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 {
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		unpublishedArticles, err := db.GetCertainArticles(false, false)
 | 
			
		||||
		rejectedArticles, err := db.GetCertainArticles("rejected", true)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Println(err)
 | 
			
		||||
			http.Error(w, err.Error(), http.StatusInternalServerError)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		articles := make([]*b.Article, 0)
 | 
			
		||||
		for _, article := range rejectedArticles {
 | 
			
		||||
			if article.Published {
 | 
			
		||||
				articles = append(articles, article)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		unpublishedArticles, err := db.GetCertainArticles("published", false)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Println(err)
 | 
			
		||||
			http.Error(w, err.Error(), http.StatusInternalServerError)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		for _, article := range unpublishedArticles {
 | 
			
		||||
			if !article.Rejected {
 | 
			
		||||
				articles = append(articles, article)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		tmpl, err := template.ParseFiles(c.WebDir + "/templates/unpublished-articles.html")
 | 
			
		||||
		tmpl = template.Must(tmpl, err)
 | 
			
		||||
		tmpl.ExecuteTemplate(w, "page-content", unpublishedArticles)
 | 
			
		||||
		template.Must(tmpl, err).ExecuteTemplate(w, "page-content", articles)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -216,7 +261,7 @@ func ShowRejectedArticles(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerF
 | 
			
		||||
			RejectedArticles []*b.Article
 | 
			
		||||
		})
 | 
			
		||||
 | 
			
		||||
		data.RejectedArticles, err = db.GetCertainArticles(false, true)
 | 
			
		||||
		data.RejectedArticles, err = db.GetCertainArticles("rejected", true)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Println(err)
 | 
			
		||||
			http.Error(w, err.Error(), http.StatusInternalServerError)
 | 
			
		||||
@@ -236,74 +281,12 @@ func ShowRejectedArticles(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerF
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func ReviewUnpublishedArticle(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 {
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		data := new(struct {
 | 
			
		||||
			Article *b.Article
 | 
			
		||||
			Content template.HTML
 | 
			
		||||
			Tags    []*b.Tag
 | 
			
		||||
		})
 | 
			
		||||
 | 
			
		||||
		id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Println(err)
 | 
			
		||||
			http.Error(w, err.Error(), http.StatusInternalServerError)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		data.Article, err = db.GetArticle(id)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Println(err)
 | 
			
		||||
			http.Error(w, err.Error(), http.StatusInternalServerError)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		articleAbsName := fmt.Sprint(c.ArticleDir, "/", data.Article.ID, ".md")
 | 
			
		||||
		contentBytes, err := os.ReadFile(articleAbsName)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Println(err)
 | 
			
		||||
			http.Error(w, err.Error(), http.StatusInternalServerError)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		content, err := b.ConvertToHTML(string(contentBytes))
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Println(err)
 | 
			
		||||
			http.Error(w, err.Error(), http.StatusInternalServerError)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		data.Content = template.HTML(content)
 | 
			
		||||
 | 
			
		||||
		data.Tags, err = db.GetArticleTags(data.Article.ID)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Println(err)
 | 
			
		||||
			http.Error(w, err.Error(), http.StatusInternalServerError)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		tmpl, err := template.ParseFiles(c.WebDir + "/templates/to-be-published.html")
 | 
			
		||||
		tmpl = template.Must(tmpl, err)
 | 
			
		||||
		tmpl.ExecuteTemplate(w, "page-content", data)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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 {
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		data := new(struct {
 | 
			
		||||
			Selected map[int64]bool
 | 
			
		||||
			Article  *b.Article
 | 
			
		||||
			Content  string
 | 
			
		||||
			Tags     []*b.Tag
 | 
			
		||||
		})
 | 
			
		||||
 | 
			
		||||
		id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Println(err)
 | 
			
		||||
@@ -311,6 +294,7 @@ func ReviewRejectedArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.Handler
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		data := new(EditorHTMLData)
 | 
			
		||||
		data.Article, err = db.GetArticle(id)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Println(err)
 | 
			
		||||
@@ -319,14 +303,13 @@ func ReviewRejectedArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.Handler
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		articleAbsName := fmt.Sprint(c.ArticleDir, "/", data.Article.ID, ".md")
 | 
			
		||||
		contentBytes, err := os.ReadFile(articleAbsName)
 | 
			
		||||
		content, err := os.ReadFile(articleAbsName)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Println(err)
 | 
			
		||||
			http.Error(w, err.Error(), http.StatusInternalServerError)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		data.Content = string(contentBytes)
 | 
			
		||||
		data.Content = string(content)
 | 
			
		||||
 | 
			
		||||
		data.Tags, err = db.GetTagList()
 | 
			
		||||
		if err != nil {
 | 
			
		||||
@@ -346,7 +329,9 @@ func ReviewRejectedArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.Handler
 | 
			
		||||
			data.Selected[tag.ID] = true
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		tmpl, err := template.ParseFiles(c.WebDir + "/templates/rework-article.html")
 | 
			
		||||
		data.Action = fmt.Sprint("resubmit/", data.Article.ID)
 | 
			
		||||
 | 
			
		||||
		tmpl, err := template.ParseFiles(c.WebDir + "/templates/editor.html")
 | 
			
		||||
		tmpl = template.Must(tmpl, err)
 | 
			
		||||
		tmpl.ExecuteTemplate(w, "page-content", data)
 | 
			
		||||
	}
 | 
			
		||||
@@ -389,6 +374,36 @@ func PublishArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if article.EditedID != 0 {
 | 
			
		||||
			oldArticle, err := db.GetArticle(article.EditedID)
 | 
			
		||||
			if 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)
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if err = os.Remove(fmt.Sprint(c.ArticleDir, "/", oldArticle.ID, ".md")); err != nil {
 | 
			
		||||
				log.Println(err)
 | 
			
		||||
				http.Error(w, err.Error(), http.StatusInternalServerError)
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if err = db.UpdateAttributes(
 | 
			
		||||
				&b.Attribute{Table: "articles", ID: id, AttName: "link", Value: fmt.Sprint(c.Domain, "/article/serve/", article.ID)},
 | 
			
		||||
				&b.Attribute{Table: "articles", ID: id, AttName: "edited_id", Value: 0},
 | 
			
		||||
			); err != nil {
 | 
			
		||||
				log.Println(err)
 | 
			
		||||
				http.Error(w, err.Error(), http.StatusInternalServerError)
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		feed, err := b.GenerateRSS(c, db)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Println(err)
 | 
			
		||||
@@ -421,9 +436,7 @@ func RejectArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if err = db.UpdateAttributes(
 | 
			
		||||
			&b.Attribute{Table: "articles", ID: id, AttName: "rejected", Value: true},
 | 
			
		||||
		); err != nil {
 | 
			
		||||
		if err = db.UpdateAttributes(&b.Attribute{Table: "articles", ID: id, AttName: "rejected", Value: true}); err != nil {
 | 
			
		||||
			log.Println(err)
 | 
			
		||||
			http.Error(w, err.Error(), http.StatusInternalServerError)
 | 
			
		||||
			return
 | 
			
		||||
@@ -497,69 +510,75 @@ func UploadArticleImage(c *b.Config, s *b.CookieStore) http.HandlerFunc {
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func ShowPublishedArticles(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
 | 
			
		||||
func ShowPublishedArticles(c *b.Config, db *b.DB, s *b.CookieStore, action string) http.HandlerFunc {
 | 
			
		||||
	return func(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
		if _, err := getSession(w, r, c, s); err != nil {
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		publishedArticles, err := db.GetCertainArticles(true, false)
 | 
			
		||||
		data := new(struct {
 | 
			
		||||
			Action   string
 | 
			
		||||
			Articles []*b.Article
 | 
			
		||||
		})
 | 
			
		||||
		data.Action = action
 | 
			
		||||
 | 
			
		||||
		publishedArticles, err := db.GetCertainArticles("published", true)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Println(err)
 | 
			
		||||
			http.Error(w, err.Error(), http.StatusInternalServerError)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		filteredArticles := make([]*b.Article, 0)
 | 
			
		||||
		for _, article := range publishedArticles {
 | 
			
		||||
			if !article.AutoGenerated {
 | 
			
		||||
				filteredArticles = append(filteredArticles, article)
 | 
			
		||||
				data.Articles = append(data.Articles, article)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		tmpl, err := template.ParseFiles(c.WebDir + "/templates/published-articles.html")
 | 
			
		||||
		tmpl = template.Must(tmpl, err)
 | 
			
		||||
		tmpl.ExecuteTemplate(w, "page-content", filteredArticles)
 | 
			
		||||
		tmpl.ExecuteTemplate(w, "page-content", data)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func ReviewArticleForDeletion(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
 | 
			
		||||
func ReviewArticle(c *b.Config, db *b.DB, s *b.CookieStore, action, title, button string) http.HandlerFunc {
 | 
			
		||||
	return func(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
		if _, err := getSession(w, r, c, s); err != nil {
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		var err error
 | 
			
		||||
		data := new(struct {
 | 
			
		||||
			Title       string
 | 
			
		||||
			Description string
 | 
			
		||||
			Content     template.HTML
 | 
			
		||||
			Tags        []*b.Tag
 | 
			
		||||
			ID          int64
 | 
			
		||||
		})
 | 
			
		||||
 | 
			
		||||
		data.ID, err = strconv.ParseInt(r.PathValue("id"), 10, 64)
 | 
			
		||||
		id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Println(err)
 | 
			
		||||
			http.Error(w, err.Error(), http.StatusInternalServerError)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		article, err := db.GetArticle(data.ID)
 | 
			
		||||
		article, err := db.GetArticle(id)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Println(err)
 | 
			
		||||
			http.Error(w, err.Error(), http.StatusInternalServerError)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		data.Title, err = b.ConvertToPlain(article.Title)
 | 
			
		||||
		data := &EditorHTMLData{
 | 
			
		||||
			Article: &b.Article{
 | 
			
		||||
				ID:        id,
 | 
			
		||||
				IsInIssue: article.IsInIssue,
 | 
			
		||||
			},
 | 
			
		||||
			Action:       action,
 | 
			
		||||
			ActionTitle:  title,
 | 
			
		||||
			ActionButton: button,
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		data.Article.Title, err = b.ConvertToPlain(article.Title)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Println(err)
 | 
			
		||||
			http.Error(w, err.Error(), http.StatusInternalServerError)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		data.Description, err = b.ConvertToPlain(article.Description)
 | 
			
		||||
		data.Article.Description, err = b.ConvertToPlain(article.Description)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Println(err)
 | 
			
		||||
			http.Error(w, err.Error(), http.StatusInternalServerError)
 | 
			
		||||
@@ -567,31 +586,29 @@ func ReviewArticleForDeletion(c *b.Config, db *b.DB, s *b.CookieStore) http.Hand
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		articleAbsName := fmt.Sprint(c.ArticleDir, "/", article.ID, ".md")
 | 
			
		||||
		contentBytes, err := os.ReadFile(articleAbsName)
 | 
			
		||||
		content, err := os.ReadFile(articleAbsName)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Println(err)
 | 
			
		||||
			http.Error(w, err.Error(), http.StatusInternalServerError)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		data.Content, err = b.ConvertToHTML(string(content))
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Println(err)
 | 
			
		||||
			http.Error(w, err.Error(), http.StatusInternalServerError)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		data.HTMLContent = template.HTML(data.Content)
 | 
			
		||||
 | 
			
		||||
		data.Tags, err = db.GetArticleTags(id)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Println(err)
 | 
			
		||||
			http.Error(w, err.Error(), http.StatusInternalServerError)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		content, err := b.ConvertToHTML(string(contentBytes))
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Println(err)
 | 
			
		||||
			http.Error(w, err.Error(), http.StatusInternalServerError)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		data.Content = template.HTML(content)
 | 
			
		||||
 | 
			
		||||
		data.Tags, err = db.GetArticleTags(data.ID)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Println(err)
 | 
			
		||||
			http.Error(w, err.Error(), http.StatusInternalServerError)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		tmpl, err := template.ParseFiles(c.WebDir + "/templates/to-be-deleted.html")
 | 
			
		||||
		tmpl = template.Must(tmpl, err)
 | 
			
		||||
		tmpl.ExecuteTemplate(w, "page-content", data)
 | 
			
		||||
		tmpl, err := template.ParseFiles(c.WebDir + "/templates/review-article.html")
 | 
			
		||||
		template.Must(tmpl, err).ExecuteTemplate(w, "page-content", data)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -638,3 +655,201 @@ func DeleteArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
 | 
			
		||||
		tmpl.ExecuteTemplate(w, "page-content", session.Values["role"].(int))
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func AllowEditArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
 | 
			
		||||
	return func(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
		session, err := getSession(w, r, c, s)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		oldID, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Println(err)
 | 
			
		||||
			http.Error(w, err.Error(), http.StatusInternalServerError)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		fmt.Println(oldID)
 | 
			
		||||
 | 
			
		||||
		oldArticle, err := db.GetArticle(oldID)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Println(err)
 | 
			
		||||
			http.Error(w, err.Error(), http.StatusInternalServerError)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		newArticle := oldArticle
 | 
			
		||||
		newArticle.Published = false
 | 
			
		||||
		newArticle.Rejected = true
 | 
			
		||||
		newArticle.EditedID = oldArticle.ID
 | 
			
		||||
 | 
			
		||||
		newID, 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 {
 | 
			
		||||
			log.Println(err)
 | 
			
		||||
			http.Error(w, err.Error(), http.StatusInternalServerError)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if err = copyFile(fmt.Sprint(c.ArticleDir, "/", oldID, ".md"), fmt.Sprint(c.ArticleDir, "/", newID, ".md")); err != nil {
 | 
			
		||||
			log.Println(err)
 | 
			
		||||
			http.Error(w, err.Error(), http.StatusInternalServerError)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		tmpl, err := template.ParseFiles(c.WebDir + "/templates/hub.html")
 | 
			
		||||
		tmpl = template.Must(tmpl, err)
 | 
			
		||||
		tmpl.ExecuteTemplate(w, "page-content", session.Values["role"].(int))
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func EditArticle(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 {
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Println(err)
 | 
			
		||||
			http.Error(w, err.Error(), http.StatusInternalServerError)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		data := new(EditorHTMLData)
 | 
			
		||||
 | 
			
		||||
		data.Article, err = db.GetArticle(id)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Println(err)
 | 
			
		||||
			http.Error(w, err.Error(), http.StatusInternalServerError)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		content, err := os.ReadFile(fmt.Sprint(c.ArticleDir, "/", data.Article.ID, ".md"))
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Println(err)
 | 
			
		||||
			http.Error(w, err.Error(), http.StatusInternalServerError)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		data.Content = string(content)
 | 
			
		||||
 | 
			
		||||
		data.Tags, err = db.GetArticleTags(data.Article.ID)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Println(err)
 | 
			
		||||
			http.Error(w, err.Error(), http.StatusInternalServerError)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		selectedTags, err := db.GetArticleTags(id)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Println(err)
 | 
			
		||||
			http.Error(w, err.Error(), http.StatusInternalServerError)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		data.Selected = make(map[int64]bool)
 | 
			
		||||
		for _, tag := range selectedTags {
 | 
			
		||||
			data.Selected[tag.ID] = true
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		data.Action = fmt.Sprint("save/", data.Article.ID)
 | 
			
		||||
 | 
			
		||||
		tmpl, err := template.ParseFiles(c.WebDir + "/templates/editor.html")
 | 
			
		||||
		template.Must(tmpl, err).ExecuteTemplate(w, "page-content", data)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func SaveArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
 | 
			
		||||
	return func(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
		session, err := getSession(w, r, c, s)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		session.Values["article"] = nil
 | 
			
		||||
		if err = session.Save(r, w); err != nil {
 | 
			
		||||
			log.Println(err)
 | 
			
		||||
			http.Error(w, err.Error(), http.StatusInternalServerError)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		article := &b.Article{
 | 
			
		||||
			Title:         r.PostFormValue("article-title"),
 | 
			
		||||
			Description:   r.PostFormValue("article-description"),
 | 
			
		||||
			Published:     false,
 | 
			
		||||
			Rejected:      false,
 | 
			
		||||
			AuthorID:      session.Values["id"].(int64),
 | 
			
		||||
			IsInIssue:     r.PostFormValue("issue") == "on",
 | 
			
		||||
			AutoGenerated: false,
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		oldID, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Println(err)
 | 
			
		||||
			http.Error(w, err.Error(), http.StatusInternalServerError)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		fmt.Println(oldID)
 | 
			
		||||
		if oldID != 0 {
 | 
			
		||||
			if err = db.DeleteArticle(oldID); err != nil {
 | 
			
		||||
				log.Println(err)
 | 
			
		||||
				http.Error(w, err.Error(), http.StatusInternalServerError)
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if err = os.Remove(fmt.Sprint(c.ArticleDir, "/", oldID, ".md")); err != nil {
 | 
			
		||||
				log.Println(err)
 | 
			
		||||
				http.Error(w, err.Error(), http.StatusInternalServerError)
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		article.ID, err = db.AddArticle(article)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Println(err)
 | 
			
		||||
			http.Error(w, err.Error(), http.StatusInternalServerError)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		articleAbsName := fmt.Sprint(c.ArticleDir, "/", article.ID, ".md")
 | 
			
		||||
		if err = os.WriteFile(articleAbsName, []byte(r.PostFormValue("article-content")), 0644); err != nil {
 | 
			
		||||
			log.Println(err)
 | 
			
		||||
			http.Error(w, err.Error(), http.StatusInternalServerError)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		article.Link = fmt.Sprint(c.Domain, "/article/serve/", article.ID)
 | 
			
		||||
		if err = db.UpdateAttributes(&b.Attribute{Table: "articles", ID: article.ID, AttName: "link", Value: article.Link}); err != nil {
 | 
			
		||||
			log.Println(err)
 | 
			
		||||
			http.Error(w, err.Error(), http.StatusInternalServerError)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		r.ParseForm()
 | 
			
		||||
		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)
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
			tags = append(tags, tagID)
 | 
			
		||||
		}
 | 
			
		||||
		if err = db.WriteArticleTags(article.ID, tags); err != nil {
 | 
			
		||||
			log.Println(err)
 | 
			
		||||
			http.Error(w, err.Error(), http.StatusInternalServerError)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		tmpl, err := template.ParseFiles(c.WebDir + "/templates/hub.html")
 | 
			
		||||
		tmpl = template.Must(tmpl, err)
 | 
			
		||||
		tmpl.ExecuteTemplate(w, "page-content", session.Values["role"])
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -30,6 +30,14 @@ func PublishLatestIssue(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFun
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		title := r.PostFormValue("issue-title")
 | 
			
		||||
		if len(title) == 0 {
 | 
			
		||||
			err = fmt.Errorf("error: no title for issue specified")
 | 
			
		||||
			log.Println(err)
 | 
			
		||||
			http.Error(w, err.Error(), http.StatusInternalServerError)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if session.Values["issue-image"] == nil {
 | 
			
		||||
			err := "error: Image required"
 | 
			
		||||
			log.Println(err)
 | 
			
		||||
@@ -55,14 +63,11 @@ func PublishLatestIssue(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFun
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		imgSize := imgInfo.Size()
 | 
			
		||||
		mimeType := mime.TypeByExtension(filepath.Ext(imgAbsName))
 | 
			
		||||
 | 
			
		||||
		article := &b.Article{
 | 
			
		||||
			Title:         r.PostFormValue("issue-title"),
 | 
			
		||||
			Title:         title,
 | 
			
		||||
			EncURL:        fmt.Sprint(c.Domain, "/image/serve/", imgFileName),
 | 
			
		||||
			EncLength:     int(imgSize),
 | 
			
		||||
			EncType:       mimeType,
 | 
			
		||||
			EncLength:     int(imgInfo.Size()),
 | 
			
		||||
			EncType:       mime.TypeByExtension(filepath.Ext(imgAbsName)),
 | 
			
		||||
			Published:     true,
 | 
			
		||||
			Rejected:      false,
 | 
			
		||||
			Created:       time.Now(),
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										13
									
								
								cmd/main.go
									
									
									
									
									
								
							
							
						
						
									
										13
									
								
								cmd/main.go
									
									
									
									
									
								
							@@ -49,15 +49,20 @@ func main() {
 | 
			
		||||
		http.FileServer(http.Dir(config.WebDir+"/static/"))))
 | 
			
		||||
	mux.HandleFunc("/", f.HomePage(config, db, store))
 | 
			
		||||
 | 
			
		||||
	mux.HandleFunc("GET /article/all-published", f.ShowPublishedArticles(config, db, store))
 | 
			
		||||
	mux.HandleFunc("GET /article/allow-edit/{id}", f.AllowEditArticle(config, db, store))
 | 
			
		||||
	mux.HandleFunc("GET /article/all-published/review-edit", f.ShowPublishedArticles(config, db, store, "review-edit"))
 | 
			
		||||
	mux.HandleFunc("GET /article/all-published/delete", f.ShowPublishedArticles(config, db, store, "review-delete"))
 | 
			
		||||
	mux.HandleFunc("GET /article/all-rejected", f.ShowRejectedArticles(config, db, store))
 | 
			
		||||
	mux.HandleFunc("GET /article/all-unpublished", f.ShowUnpublishedArticles(config, db, store))
 | 
			
		||||
	mux.HandleFunc("GET /article/all-unpublished-unrejected-and-published-rejected", f.ShowUnpublishedUnrejectedAndPublishedRejectedArticles(config, db, store))
 | 
			
		||||
	mux.HandleFunc("GET /article/delete/{id}", f.DeleteArticle(config, db, store))
 | 
			
		||||
	mux.HandleFunc("GET /article/edit/{id}", f.EditArticle(config, db, store))
 | 
			
		||||
	mux.HandleFunc("GET /article/publish/{id}", f.PublishArticle(config, db, store))
 | 
			
		||||
	mux.HandleFunc("GET /article/reject/{id}", f.RejectArticle(config, db, store))
 | 
			
		||||
	mux.HandleFunc("GET /article/review-deletion/{id}", f.ReviewArticleForDeletion(config, db, store))
 | 
			
		||||
	mux.HandleFunc("GET /article/review-delete/{id}", f.ReviewArticle(config, db, store, "delete", "Artikel löschen", "Löschen"))
 | 
			
		||||
	mux.HandleFunc("GET /article/review-edit/{id}", f.ReviewArticle(config, db, store, "allow-edit", "Artikel bearbeiten", "Bearbeiten erlauben"))
 | 
			
		||||
	mux.HandleFunc("GET /article/review-rejected/{id}", f.ReviewRejectedArticle(config, db, store))
 | 
			
		||||
	mux.HandleFunc("GET /article/review-unpublished/{id}", f.ReviewUnpublishedArticle(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/save/{id}", f.SaveArticle(config, db, store))
 | 
			
		||||
	mux.HandleFunc("GET /article/serve/{id}", c.ServeArticle(config, db))
 | 
			
		||||
	mux.HandleFunc("GET /article/write", f.WriteArticle(config, db, store))
 | 
			
		||||
	mux.HandleFunc("GET /hub", f.ShowHub(config, db, store))
 | 
			
		||||
 
 | 
			
		||||
@@ -33,6 +33,7 @@ CREATE TABLE articles (
 | 
			
		||||
    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,
 | 
			
		||||
    PRIMARY KEY (id),
 | 
			
		||||
 
 | 
			
		||||
@@ -18,7 +18,7 @@
 | 
			
		||||
        <div>
 | 
			
		||||
            <h3>Titelseite</h3>
 | 
			
		||||
            <div class="grid grid-cols-2 gap-4 items-center">
 | 
			
		||||
                <input class="h-full" name="issue-title" placeholder="Titel" type="text" />
 | 
			
		||||
                <input class="h-full" name="issue-title" placeholder="Titel" required type="text" />
 | 
			
		||||
                <label class="btn text-center" for="image-upload">Bild hochladen</label>
 | 
			
		||||
                <input class="hidden" id="image-upload" name="issue-image" type="file" required
 | 
			
		||||
                    hx-post="/issue/upload-image" />
 | 
			
		||||
 
 | 
			
		||||
@@ -4,39 +4,41 @@
 | 
			
		||||
<form id="edit-area">
 | 
			
		||||
    <div class="flex flex-col gap-y-1">
 | 
			
		||||
        <label for="article-title">Titel</label>
 | 
			
		||||
        <input name="article-title" type="text" value="{{.Title}}" />
 | 
			
		||||
        <input name="article-title" type="text" value="{{.Article.Title}}" />
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <div class="flex flex-col gap-y-1">
 | 
			
		||||
        <label for="article-description">Beschreibung</label>
 | 
			
		||||
        <textarea name="article-description">{{.Description}}</textarea>
 | 
			
		||||
        <textarea name="article-description">{{.Article.Description}}</textarea>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <div class="flex flex-col gap-y-1">
 | 
			
		||||
        <label for="easyMDE">Artikel</label>
 | 
			
		||||
        <textarea id="easyMDE">{{.Content}}</textarea>
 | 
			
		||||
        <input id="article-content" name="article-content" type="hidden" />
 | 
			
		||||
        <input id="article-content" name="article-content" type="hidden" value="{{.Content}}" />
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <div>
 | 
			
		||||
        <span>Tags</span>
 | 
			
		||||
        <div class="flex flex-wrap gap-x-4">
 | 
			
		||||
            <div>
 | 
			
		||||
                <input id="issue" name="issue" type="checkbox" />
 | 
			
		||||
                <input id="issue" name="issue" type="checkbox" {{if .Article.IsInIssue}}checked{{end}} />
 | 
			
		||||
                <label for="issue">Orient Express</label>
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
            {{range .Tags}}
 | 
			
		||||
            <div>
 | 
			
		||||
                <input id="{{.Name}}" name="tags" type="checkbox" value="{{.ID}}" />
 | 
			
		||||
                <label for="{{.Name}}">{{.Name}}</label>
 | 
			
		||||
                <input id="tag-{{.Name}}" name="tags" type="checkbox" value="{{.ID}}" {{if index $.Selected
 | 
			
		||||
                    .ID}}checked{{end}} />
 | 
			
		||||
                <label for="tag-{{.Name}}">{{.Name}}</label>
 | 
			
		||||
            </div>
 | 
			
		||||
            {{end}}
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <div class="btn-area">
 | 
			
		||||
        <input class="action-btn" type="submit" value="Senden" hx-post="/article/submit" hx-target="#page-content" />
 | 
			
		||||
        <input class="action-btn" type="submit" value="Senden" hx-post="/article/{{.Action}}"
 | 
			
		||||
            hx-target="#page-content" />
 | 
			
		||||
        <button class="btn" hx-get="/hub" hx-target="#page-content">Abbrechen</button>
 | 
			
		||||
    </div>
 | 
			
		||||
</form>
 | 
			
		||||
 
 | 
			
		||||
@@ -7,13 +7,13 @@
 | 
			
		||||
        <h2>Artikel</h2>
 | 
			
		||||
        <div class="grid grid-cols-1 md:grid-cols-2 gap-x-4 gap-y-2">
 | 
			
		||||
            <button class="btn" hx-get="/article/write" hx-target="#page-content">Artikel schreiben</button>
 | 
			
		||||
            <button class="btn" hx-get="/article/all-rejected" hx-target="#page-content">Abgelehnte Artikel</button>
 | 
			
		||||
            {{if lt . 3}}<button class="btn" hx-get="/article/all-unpublished" hx-target="#page-content">
 | 
			
		||||
                Unveröffentlichte Artikel
 | 
			
		||||
            </button>{{end}}
 | 
			
		||||
            {{if lt . 2}}<button class="btn" hx-get="/article/all-published" hx-target="#page-content">
 | 
			
		||||
                Artikel löschen
 | 
			
		||||
            </button>{{end}}
 | 
			
		||||
            <button class="btn" hx-get="/article/all-rejected" hx-target="#page-content">Artikel bearbeiten</button>
 | 
			
		||||
            {{if lt . 3}}<button class="btn" hx-get="/article/all-unpublished-unrejected-and-published-rejected"
 | 
			
		||||
                hx-target="#page-content">Artikel veröffentlichen</button>{{end}}
 | 
			
		||||
            {{if lt . 2}}<button class="btn" hx-get="/article/all-published/delete" hx-target="#page-content">Artikel
 | 
			
		||||
                löschen</button>{{end}}
 | 
			
		||||
            {{if lt . 2}}<button class="btn" hx-get="/article/all-published/review-edit"
 | 
			
		||||
                hx-target="#page-content">Artikel bearbeiten lassen</button>{{end}}
 | 
			
		||||
            {{if lt . 3}}<button class="btn" hx-get="/tag/create" hx-target="#page-content">Neuer Tag</button>{{end}}
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
@@ -38,15 +38,12 @@
 | 
			
		||||
        <h2>Benutzer</h2>
 | 
			
		||||
        <div class="grid grid-cols-1 md:grid-cols-2 gap-x-4 gap-y-2">
 | 
			
		||||
            <button class="btn" hx-get="/user/edit/self" hx-target="#page-content">Mein Profil bearbeiten</button>
 | 
			
		||||
            {{if eq . 0}}<button class="btn" hx-get="/user/create" hx-target="#page-content">
 | 
			
		||||
                Benutzer hinzufügen
 | 
			
		||||
            </button>{{end}}
 | 
			
		||||
            {{if eq . 0}}<button class="btn" hx-get="/user/show-all/edit" hx-target="#page-content">
 | 
			
		||||
                Benutzer bearbeiten
 | 
			
		||||
            </button>{{end}}
 | 
			
		||||
            {{if eq . 0}}<button class="btn" hx-get="/user/show-all/delete" hx-target="#page-content">
 | 
			
		||||
                Benutzer löschen
 | 
			
		||||
            </button>{{end}}
 | 
			
		||||
            {{if eq . 0}}<button class="btn" hx-get="/user/create" hx-target="#page-content">Benutzer
 | 
			
		||||
                hinzufügen</button>{{end}}
 | 
			
		||||
            {{if eq . 0}}<button class="btn" hx-get="/user/show-all/edit" hx-target="#page-content">Benutzer
 | 
			
		||||
                bearbeiten</button>{{end}}
 | 
			
		||||
            {{if eq . 0}}<button class="btn" hx-get="/user/show-all/delete" hx-target="#page-content">Benutzer
 | 
			
		||||
                löschen</button>{{end}}
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
    {{end}}
 | 
			
		||||
 
 | 
			
		||||
@@ -34,13 +34,8 @@
 | 
			
		||||
    </main>
 | 
			
		||||
 | 
			
		||||
    <footer class="text-center text-gray-500 my-8">
 | 
			
		||||
        <p>
 | 
			
		||||
            © 2024 Jason Streifling. Alle Rechte vorbehalten.
 | 
			
		||||
        </p>
 | 
			
		||||
        <p>
 | 
			
		||||
            v0.10.3 - <strong>Hinweis:</strong> Diese Software befindet sich noch in der Entwicklung und kann Fehler
 | 
			
		||||
            enthalten.
 | 
			
		||||
        </p>
 | 
			
		||||
        <p>© 2024 Jason Streifling. Alle Rechte vorbehalten.</p>
 | 
			
		||||
        <p>v0.11.0 - <strong>Alpha: Drastische Änderungen und Fehler vorbehalten.</strong></p>
 | 
			
		||||
    </footer>
 | 
			
		||||
 | 
			
		||||
    <script src="https://unpkg.com/htmx.org@2.0.2"></script>
 | 
			
		||||
 
 | 
			
		||||
@@ -2,12 +2,13 @@
 | 
			
		||||
<h2>Artikel löschen</h2>
 | 
			
		||||
 | 
			
		||||
<div class="flex flex-col gap-4">
 | 
			
		||||
    {{range .}}
 | 
			
		||||
    <button class="btn" hx-get="/article/review-deletion/{{.ID}}" hx-target="#page-content">
 | 
			
		||||
    {{range .Articles}}
 | 
			
		||||
    <button class="btn" hx-get="/article/{{$.Action}}/{{.ID}}" hx-target="#page-content">
 | 
			
		||||
        <h1 class="font-bold text-2xl">{{.Title}}</h1>
 | 
			
		||||
        <p>{{.Description}}</p>
 | 
			
		||||
    </button>
 | 
			
		||||
    {{end}}
 | 
			
		||||
 | 
			
		||||
    <button class="action-btn" hx-get="/hub" hx-target="#page-content">Zurück</button>
 | 
			
		||||
</div>
 | 
			
		||||
{{end}}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
{{define "page-content"}}
 | 
			
		||||
<h2>Abgelehnte Artikel</h2>
 | 
			
		||||
<h2>Artikel bearbeiten</h2>
 | 
			
		||||
 | 
			
		||||
<div class="flex flex-col gap-4">
 | 
			
		||||
    {{range .RejectedArticles}}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
{{define "page-content"}}
 | 
			
		||||
<h2>Artikel veröffentlichen</h2>
 | 
			
		||||
<h2>{{.ActionTitle}}</h2>
 | 
			
		||||
 | 
			
		||||
<div>
 | 
			
		||||
    <span>Titel</span>
 | 
			
		||||
@@ -14,8 +14,8 @@
 | 
			
		||||
 | 
			
		||||
    <span>Artikel</span>
 | 
			
		||||
    <div class="border border-slate-200 dark:border-slate-800 mb-3 px-2 py-2 rounded-md w-full">
 | 
			
		||||
        <div class="prose">
 | 
			
		||||
            {{.Content}}
 | 
			
		||||
        <div class="prose text-slate-900 dark:text-slate-100">
 | 
			
		||||
            {{.HTMLContent}}
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
@@ -31,12 +31,20 @@
 | 
			
		||||
        {{end}}
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    {{if eq .Action "publish"}}
 | 
			
		||||
    <div class="btn-area-3">
 | 
			
		||||
        <input class="action-btn" type="submit" value="Veröffentlichen" hx-get="/article/publish/{{.Article.ID}}"
 | 
			
		||||
        <input class="action-btn" type="submit" value="{{.ActionButton}}" hx-get="/article/{{.Action}}/{{.Article.ID}}"
 | 
			
		||||
            hx-target="#page-content" />
 | 
			
		||||
        <input class="btn" type="submit" value="Ablehnen" hx-get="/article/reject/{{.Article.ID}}"
 | 
			
		||||
            hx-target="#page-content" />
 | 
			
		||||
        <button class="btn" hx-get="/hub" hx-target="#page-content">Zurück</button>
 | 
			
		||||
        <button class="btn" hx-get="/hub" hx-target="#page-content">Abbrechen</button>
 | 
			
		||||
    </div>
 | 
			
		||||
    {{else}}
 | 
			
		||||
    <div class="btn-area">
 | 
			
		||||
        <input class="action-btn" type="submit" value="{{.ActionButton}}" hx-get="/article/{{.Action}}/{{.Article.ID}}"
 | 
			
		||||
            hx-target="#page-content" />
 | 
			
		||||
        <button class="btn" hx-get="/hub" hx-target="#page-content">Abbrechen</button>
 | 
			
		||||
    </div>
 | 
			
		||||
    {{end}}
 | 
			
		||||
</div>
 | 
			
		||||
{{end}}
 | 
			
		||||
@@ -1,76 +0,0 @@
 | 
			
		||||
{{define "page-content"}}
 | 
			
		||||
<h2>Editor</h2>
 | 
			
		||||
 | 
			
		||||
<form>
 | 
			
		||||
    <div class="flex flex-col gap-y-1">
 | 
			
		||||
        <label for="article-title">Titel</label>
 | 
			
		||||
        <input name="article-title" type="text" value="{{.Article.Title}}" />
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <div class="flex flex-col gap-y-1">
 | 
			
		||||
        <label for="article-description">Beschreibung</label>
 | 
			
		||||
        <textarea name="article-description">{{.Article.Description}}</textarea>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <div class="flex flex-col gap-y-1">
 | 
			
		||||
        <label for="easyMDE">Artikel</label>
 | 
			
		||||
        <textarea id="easyMDE">{{.Content}}</textarea>
 | 
			
		||||
        <input id="article-content" name="article-content" type="hidden" value="{{.Content}}" />
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <div>
 | 
			
		||||
        <span>Tags</span>
 | 
			
		||||
        <div class="flex flex-wrap gap-x-4">
 | 
			
		||||
            <div>
 | 
			
		||||
                <input id="issue" name="issue" type="checkbox" {{if .Article.IsInIssue}}checked{{end}} />
 | 
			
		||||
                <label for="issue">Orient Express</label>
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
            {{range .Tags}}
 | 
			
		||||
            <div>
 | 
			
		||||
                <input id="tag-{{.Name}}" name="tags" type="checkbox" value="{{.ID}}" {{if index $.Selected
 | 
			
		||||
                    .ID}}checked{{end}} />
 | 
			
		||||
                <label for="tag-{{.Name}}">{{.Name}}</label>
 | 
			
		||||
            </div>
 | 
			
		||||
            {{end}}
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <div class="btn-area">
 | 
			
		||||
        <input class="action-btn" type="submit" value="Senden" hx-post="/article/resubmit/{{.Article.ID}}"
 | 
			
		||||
            hx-target="#page-content" />
 | 
			
		||||
        <button class="btn" hx-get="/hub" hx-target="#page-content">Zurück</button>
 | 
			
		||||
    </div>
 | 
			
		||||
</form>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
    var easyMDE = new EasyMDE({
 | 
			
		||||
        element: document.getElementById('easyMDE'),
 | 
			
		||||
        hideIcons: ['image'],
 | 
			
		||||
        imageTexts: {sbInit: ''},
 | 
			
		||||
        showIcons: ["code", "table", "upload-image"],
 | 
			
		||||
        uploadImage: true,
 | 
			
		||||
 | 
			
		||||
        imageUploadFunction: function (file, onSuccess, onError) {
 | 
			
		||||
            var formData = new FormData();
 | 
			
		||||
            formData.append('article-image', file);
 | 
			
		||||
 | 
			
		||||
            fetch('/article/upload-image', {
 | 
			
		||||
                method: 'POST',
 | 
			
		||||
                body: formData
 | 
			
		||||
            })
 | 
			
		||||
                .then(response => response.json())
 | 
			
		||||
                .then(data => {
 | 
			
		||||
                    onSuccess(data);
 | 
			
		||||
                })
 | 
			
		||||
                .catch(error => {
 | 
			
		||||
                    onError(error);
 | 
			
		||||
                });
 | 
			
		||||
        },
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    easyMDE.codemirror.on("change", () => {
 | 
			
		||||
        document.getElementById('article-content').value = easyMDE.value();
 | 
			
		||||
    });
 | 
			
		||||
</script>
 | 
			
		||||
{{end}}
 | 
			
		||||
@@ -1,36 +0,0 @@
 | 
			
		||||
{{define "page-content"}}
 | 
			
		||||
<h2>Artikel löschen</h2>
 | 
			
		||||
 | 
			
		||||
<div>
 | 
			
		||||
    <span>Titel</span>
 | 
			
		||||
    <div class="border border-slate-200 dark:border-slate-800 mb-3 px-2 py-2 rounded-md w-full">
 | 
			
		||||
        {{.Title}}
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <span>Beschreibung</span>
 | 
			
		||||
    <div class="border border-slate-200 dark:border-slate-800 mb-3 px-2 py-2 rounded-md w-full">
 | 
			
		||||
        {{.Description}}
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <span>Artikel</span>
 | 
			
		||||
    <div class="border border-slate-200 dark:border-slate-800 mb-3 px-2 py-2 rounded-md w-full">
 | 
			
		||||
        <div class="prose text-slate-900 dark:text-slate-100">
 | 
			
		||||
            {{.Content}}
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <span>Tags</span>
 | 
			
		||||
    <div class="border border-slate-200 dark:border-slate-800 mb-3 px-2 py-2 rounded-md w-full">
 | 
			
		||||
        {{range .Tags}}
 | 
			
		||||
        {{.Name}}
 | 
			
		||||
        <br>
 | 
			
		||||
        {{end}}
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <div class="btn-area">
 | 
			
		||||
        <input class="action-btn" type="submit" value="Löschen" hx-get="/article/delete/{{.ID}}"
 | 
			
		||||
            hx-target="#page-content" />
 | 
			
		||||
        <button class="btn" hx-get="/hub" hx-target="#page-content">Zurück</button>
 | 
			
		||||
    </div>
 | 
			
		||||
</div>
 | 
			
		||||
{{end}}
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
{{define "page-content"}}
 | 
			
		||||
<h2>Unveröffentlichte Artikel</h2>
 | 
			
		||||
<h2>Artikel veröffentlichen</h2>
 | 
			
		||||
 | 
			
		||||
<div class="flex flex-col gap-4">
 | 
			
		||||
    {{range .}}
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user