feature/id-for-hmtl-headers #3
@@ -9,18 +9,20 @@ import (
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type Article struct {
 | 
			
		||||
	Title       string
 | 
			
		||||
	Created     time.Time
 | 
			
		||||
	Description string
 | 
			
		||||
	Link        string
 | 
			
		||||
	EncURL      string
 | 
			
		||||
	EncLength   int
 | 
			
		||||
	EncType     string
 | 
			
		||||
	Published   bool
 | 
			
		||||
	Rejected    bool
 | 
			
		||||
	ID          int64
 | 
			
		||||
	AuthorID    int64
 | 
			
		||||
	IssueID     int64
 | 
			
		||||
	Title         string
 | 
			
		||||
	Created       time.Time
 | 
			
		||||
	Description   string
 | 
			
		||||
	Link          string
 | 
			
		||||
	EncURL        string
 | 
			
		||||
	EncLength     int
 | 
			
		||||
	EncType       string
 | 
			
		||||
	Published     bool
 | 
			
		||||
	Rejected      bool
 | 
			
		||||
	ID            int64
 | 
			
		||||
	AuthorID      int64
 | 
			
		||||
	IssueID       int64
 | 
			
		||||
	IsInIssue     bool
 | 
			
		||||
	AutoGenerated bool
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (db *DB) AddArticle(a *Article) (int64, error) {
 | 
			
		||||
@@ -29,8 +31,8 @@ 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)
 | 
			
		||||
    VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
 | 
			
		||||
        (title, description, link, enc_url, enc_length, enc_type, published, rejected, author_id, issue_id, is_in_issue, auto_generated)
 | 
			
		||||
    VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
 | 
			
		||||
    `
 | 
			
		||||
 | 
			
		||||
	for i := 0; i < TxMaxRetries; i++ {
 | 
			
		||||
@@ -47,8 +49,10 @@ func (db *DB) AddArticle(a *Article) (int64, error) {
 | 
			
		||||
				return 0, fmt.Errorf("error getting issue ID when adding article to DB: %v", err)
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			fmt.Println(a)
 | 
			
		||||
			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.EncURL, a.EncLength, a.EncType, a.Published, a.Rejected, a.AuthorID, id,
 | 
			
		||||
				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 +86,7 @@ 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
 | 
			
		||||
    SELECT title, created, description, link, enc_url, enc_length, enc_type, published, author_id, issue_id, is_in_issue, auto_generated
 | 
			
		||||
    FROM articles
 | 
			
		||||
    WHERE id = ?
 | 
			
		||||
    `
 | 
			
		||||
@@ -94,7 +98,8 @@ 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); err != nil {
 | 
			
		||||
		&article.Published, &article.AuthorID, &article.IssueID,
 | 
			
		||||
		&article.IsInIssue, &article.AutoGenerated); err != nil {
 | 
			
		||||
		return nil, fmt.Errorf("error scanning article row: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@@ -109,7 +114,7 @@ func (db *DB) GetArticle(id int64) (*Article, error) {
 | 
			
		||||
 | 
			
		||||
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
 | 
			
		||||
    SELECT id, title, created, description, link, enc_url, enc_length, enc_type, author_id, issue_id, is_in_issue, auto_generated
 | 
			
		||||
    FROM articles
 | 
			
		||||
    WHERE published = ?
 | 
			
		||||
    AND rejected = ?
 | 
			
		||||
@@ -126,7 +131,8 @@ 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); err != nil {
 | 
			
		||||
			&article.EncType, &article.AuthorID, &article.IssueID,
 | 
			
		||||
			&article.IsInIssue, &article.AutoGenerated); err != nil {
 | 
			
		||||
			return nil, fmt.Errorf("error scanning article row: %v", err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
@@ -147,9 +153,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
 | 
			
		||||
    SELECT id, title, created, description, link, enc_url, enc_length, enc_type, author_id, auto_generated
 | 
			
		||||
    FROM articles
 | 
			
		||||
    WHERE issue_id = ? AND published = true
 | 
			
		||||
    WHERE issue_id = ? AND published = true AND is_in_issue = true
 | 
			
		||||
    `
 | 
			
		||||
 | 
			
		||||
	for i := 0; i < TxMaxRetries; i++ {
 | 
			
		||||
@@ -182,7 +188,7 @@ func (db *DB) GetCurrentIssueArticles() ([]*Article, error) {
 | 
			
		||||
 | 
			
		||||
				if err = rows.Scan(&article.ID, &article.Title, &created,
 | 
			
		||||
					&article.Description, &article.Link, &article.EncURL, &article.EncLength,
 | 
			
		||||
					&article.EncType, &article.AuthorID); err != nil {
 | 
			
		||||
					&article.EncType, &article.AuthorID, &article.AutoGenerated); err != nil {
 | 
			
		||||
					if rollbackErr := tx.Rollback(); rollbackErr != nil {
 | 
			
		||||
						log.Fatalf("transaction error: %v, rollback error: %v", err, rollbackErr)
 | 
			
		||||
					}
 | 
			
		||||
 
 | 
			
		||||
@@ -31,7 +31,10 @@ func GenerateRSS(c *Config, db *DB) (*string, error) {
 | 
			
		||||
		for _, tag := range tags {
 | 
			
		||||
			tagNames = append(tagNames, tag.Name)
 | 
			
		||||
		}
 | 
			
		||||
		tagNames = append(tagNames, fmt.Sprint("Orient Express ", article.IssueID))
 | 
			
		||||
 | 
			
		||||
		if article.IsInIssue {
 | 
			
		||||
			tagNames = append(tagNames, fmt.Sprint("Orient Express ", article.IssueID))
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		user, err := db.GetUser(article.AuthorID)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
@@ -57,9 +60,8 @@ func GenerateRSS(c *Config, db *DB) (*string, error) {
 | 
			
		||||
			PubDate:     article.Created.Format(time.RFC1123Z),
 | 
			
		||||
			Title:       articleTitle,
 | 
			
		||||
		}
 | 
			
		||||
		fmt.Println(article.Link, ": ", len(article.Link))
 | 
			
		||||
 | 
			
		||||
		if article.Title == "Autogenerated cpolis Issue Article" {
 | 
			
		||||
		if article.AutoGenerated {
 | 
			
		||||
			item.Enclosure = &rss.Enclosure{
 | 
			
		||||
				Url:    article.EncURL,
 | 
			
		||||
				Lenght: article.EncLength,
 | 
			
		||||
 
 | 
			
		||||
@@ -73,11 +73,13 @@ func SubmitArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		article := &b.Article{
 | 
			
		||||
			Title:       r.PostFormValue("article-title"),
 | 
			
		||||
			Description: r.PostFormValue("article-description"),
 | 
			
		||||
			Published:   false,
 | 
			
		||||
			Rejected:    false,
 | 
			
		||||
			AuthorID:    session.Values["id"].(int64),
 | 
			
		||||
			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,
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		article.ID, err = db.AddArticle(article)
 | 
			
		||||
@@ -153,6 +155,7 @@ func ResubmitArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
 | 
			
		||||
			&b.Attribute{Table: "articles", ID: id, AttName: "title", Value: title},
 | 
			
		||||
			&b.Attribute{Table: "articles", ID: id, AttName: "description", Value: description},
 | 
			
		||||
			&b.Attribute{Table: "articles", ID: id, AttName: "rejected", Value: false},
 | 
			
		||||
			&b.Attribute{Table: "articles", ID: id, AttName: "is_in_issue", Value: r.PostFormValue("issue") == "on"},
 | 
			
		||||
		); err != nil {
 | 
			
		||||
			log.Println(err)
 | 
			
		||||
			http.Error(w, err.Error(), http.StatusInternalServerError)
 | 
			
		||||
@@ -240,46 +243,27 @@ func ReviewUnpublishedArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.Hand
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		type htmlData struct {
 | 
			
		||||
			Title       string
 | 
			
		||||
			Description string
 | 
			
		||||
			Content     template.HTML
 | 
			
		||||
			Tags        []*b.Tag
 | 
			
		||||
			ID          int64
 | 
			
		||||
		}
 | 
			
		||||
		data := new(struct {
 | 
			
		||||
			Article *b.Article
 | 
			
		||||
			Content template.HTML
 | 
			
		||||
			Tags    []*b.Tag
 | 
			
		||||
		})
 | 
			
		||||
 | 
			
		||||
		var err error
 | 
			
		||||
		data := new(htmlData)
 | 
			
		||||
 | 
			
		||||
		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)
 | 
			
		||||
		data.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)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Println(err)
 | 
			
		||||
			http.Error(w, err.Error(), http.StatusInternalServerError)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		data.Description, err = b.ConvertToPlain(article.Description)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Println(err)
 | 
			
		||||
			http.Error(w, err.Error(), http.StatusInternalServerError)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		articleAbsName := fmt.Sprint(c.ArticleDir, "/", article.ID, ".md")
 | 
			
		||||
		articleAbsName := fmt.Sprint(c.ArticleDir, "/", data.Article.ID, ".md")
 | 
			
		||||
		contentBytes, err := os.ReadFile(articleAbsName)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Println(err)
 | 
			
		||||
@@ -295,7 +279,7 @@ func ReviewUnpublishedArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.Hand
 | 
			
		||||
		}
 | 
			
		||||
		data.Content = template.HTML(content)
 | 
			
		||||
 | 
			
		||||
		data.Tags, err = db.GetArticleTags(data.ID)
 | 
			
		||||
		data.Tags, err = db.GetArticleTags(data.Article.ID)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Println(err)
 | 
			
		||||
			http.Error(w, err.Error(), http.StatusInternalServerError)
 | 
			
		||||
@@ -314,13 +298,12 @@ func ReviewRejectedArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.Handler
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		type htmlData struct {
 | 
			
		||||
		data := new(struct {
 | 
			
		||||
			Selected map[int64]bool
 | 
			
		||||
			Article  *b.Article
 | 
			
		||||
			Content  string
 | 
			
		||||
			Tags     []*b.Tag
 | 
			
		||||
		}
 | 
			
		||||
		data := new(htmlData)
 | 
			
		||||
		})
 | 
			
		||||
 | 
			
		||||
		id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
@@ -378,13 +361,20 @@ func PublishArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Println(err)
 | 
			
		||||
			http.Error(w, err.Error(), http.StatusBadRequest)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		article, err := db.GetArticle(id)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Println(err)
 | 
			
		||||
			http.Error(w, err.Error(), http.StatusInternalServerError)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if err = db.AddArticleToCurrentIssue(id); err != nil {
 | 
			
		||||
		if err = db.AddArticleToCurrentIssue(article.ID); err != nil {
 | 
			
		||||
			log.Println(err)
 | 
			
		||||
			http.Error(w, err.Error(), http.StatusInternalServerError)
 | 
			
		||||
			return
 | 
			
		||||
@@ -523,7 +513,7 @@ func ShowPublishedArticles(c *b.Config, db *b.DB, s *b.CookieStore) http.Handler
 | 
			
		||||
 | 
			
		||||
		filteredArticles := make([]*b.Article, 0)
 | 
			
		||||
		for _, article := range publishedArticles {
 | 
			
		||||
			if article.Title != "Autogenerated cpolis Issue Article" {
 | 
			
		||||
			if !article.AutoGenerated {
 | 
			
		||||
				filteredArticles = append(filteredArticles, article)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 
 | 
			
		||||
@@ -59,14 +59,15 @@ func PublishLatestIssue(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFun
 | 
			
		||||
		mimeType := mime.TypeByExtension(filepath.Ext(imgAbsName))
 | 
			
		||||
 | 
			
		||||
		article := &b.Article{
 | 
			
		||||
			Title:     "Autogenerated cpolis Issue Article",
 | 
			
		||||
			EncURL:    fmt.Sprint(c.Domain, "/image/serve/", imgFileName),
 | 
			
		||||
			EncLength: int(imgSize),
 | 
			
		||||
			EncType:   mimeType,
 | 
			
		||||
			Published: true,
 | 
			
		||||
			Rejected:  false,
 | 
			
		||||
			Created:   time.Now(),
 | 
			
		||||
			AuthorID:  session.Values["id"].(int64),
 | 
			
		||||
			Title:         r.PostFormValue("issue-title"),
 | 
			
		||||
			EncURL:        fmt.Sprint(c.Domain, "/image/serve/", imgFileName),
 | 
			
		||||
			EncLength:     int(imgSize),
 | 
			
		||||
			EncType:       mimeType,
 | 
			
		||||
			Published:     true,
 | 
			
		||||
			Rejected:      false,
 | 
			
		||||
			Created:       time.Now(),
 | 
			
		||||
			AuthorID:      session.Values["id"].(int64),
 | 
			
		||||
			AutoGenerated: true,
 | 
			
		||||
		}
 | 
			
		||||
		fmt.Println(article.Link)
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										66
									
								
								cmd/frontend/pdf.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										66
									
								
								cmd/frontend/pdf.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,66 @@
 | 
			
		||||
package frontend
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"io"
 | 
			
		||||
	"log"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"os"
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
 | 
			
		||||
	"github.com/google/uuid"
 | 
			
		||||
	b "streifling.com/jason/cpolis/cmd/backend"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func UploadPDF(c *b.Config, s *b.CookieStore) http.HandlerFunc {
 | 
			
		||||
	return func(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
		fmt.Println("Content-Type:", r.Header.Get("Content-Type"))
 | 
			
		||||
		if _, err := getSession(w, r, c, s); err != nil {
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		fmt.Println(1)
 | 
			
		||||
		if err := r.ParseMultipartForm(10 << 20); err != nil {
 | 
			
		||||
			log.Println(err)
 | 
			
		||||
			http.Error(w, err.Error(), http.StatusBadRequest)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		fmt.Println(2)
 | 
			
		||||
		file, _, err := r.FormFile("pdf-upload")
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Println(err)
 | 
			
		||||
			http.Error(w, err.Error(), http.StatusInternalServerError)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		defer file.Close()
 | 
			
		||||
 | 
			
		||||
		fmt.Println(3)
 | 
			
		||||
		filename := fmt.Sprint(uuid.New(), ".pdf")
 | 
			
		||||
		absFilepath, err := filepath.Abs(fmt.Sprint(c.PDFDir, "/", filename))
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Println(err)
 | 
			
		||||
			http.Error(w, err.Error(), http.StatusInternalServerError)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		fmt.Println(4)
 | 
			
		||||
		pdf, err := os.Create(absFilepath)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Println(err)
 | 
			
		||||
			http.Error(w, err.Error(), http.StatusInternalServerError)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		defer pdf.Close()
 | 
			
		||||
 | 
			
		||||
		fmt.Println(5)
 | 
			
		||||
		if _, err = io.Copy(pdf, file); err != nil {
 | 
			
		||||
			log.Println(err)
 | 
			
		||||
			http.Error(w, err.Error(), http.StatusInternalServerError)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		fmt.Println(6)
 | 
			
		||||
		w.WriteHeader(http.StatusOK)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@@ -81,6 +81,7 @@ func main() {
 | 
			
		||||
	mux.HandleFunc("POST /issue/publish", f.PublishLatestIssue(config, db, store))
 | 
			
		||||
	mux.HandleFunc("POST /issue/upload-image", f.UploadIssueImage(config, store))
 | 
			
		||||
	mux.HandleFunc("POST /login", f.Login(config, db, store))
 | 
			
		||||
	mux.HandleFunc("POST /pdf/upload", f.UploadPDF(config, store))
 | 
			
		||||
	mux.HandleFunc("POST /tag/add", f.AddTag(config, db, store))
 | 
			
		||||
	mux.HandleFunc("POST /user/add", f.AddUser(config, db, store))
 | 
			
		||||
	mux.HandleFunc("POST /user/add-first", f.AddFirstUser(config, db, store))
 | 
			
		||||
 
 | 
			
		||||
@@ -21,18 +21,20 @@ CREATE TABLE issues (
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
CREATE TABLE articles (
 | 
			
		||||
    id          INT             AUTO_INCREMENT,
 | 
			
		||||
    title       VARCHAR(255)    NOT NULL,
 | 
			
		||||
    created     TIMESTAMP       DEFAULT CURRENT_TIMESTAMP,
 | 
			
		||||
    description TEXT            NOT NULL,
 | 
			
		||||
    link        VARCHAR(255),
 | 
			
		||||
    enc_url     VARCHAR(255),
 | 
			
		||||
    enc_length  INT,
 | 
			
		||||
    enc_type    VARCHAR(255),
 | 
			
		||||
    published   BOOL            NOT NULL,
 | 
			
		||||
    rejected    BOOL            NOT NULL,
 | 
			
		||||
    author_id   INT             NOT NULL,
 | 
			
		||||
    issue_id    INT             NOT NULL,
 | 
			
		||||
    id              INT             AUTO_INCREMENT,
 | 
			
		||||
    title           VARCHAR(255)    NOT NULL,
 | 
			
		||||
    created         TIMESTAMP       DEFAULT CURRENT_TIMESTAMP,
 | 
			
		||||
    description     TEXT            NOT NULL,
 | 
			
		||||
    link            VARCHAR(255),
 | 
			
		||||
    enc_url         VARCHAR(255),
 | 
			
		||||
    enc_length      INT,
 | 
			
		||||
    enc_type        VARCHAR(255),
 | 
			
		||||
    published       BOOL            NOT NULL,
 | 
			
		||||
    rejected        BOOL            NOT NULL,
 | 
			
		||||
    author_id       INT             NOT NULL,
 | 
			
		||||
    issue_id        INT             NOT NULL,
 | 
			
		||||
    is_in_issue     BOOL            NOT NULL,
 | 
			
		||||
    auto_generated  BOOL            NOT NULL,
 | 
			
		||||
    PRIMARY KEY (id),
 | 
			
		||||
    FOREIGN KEY (author_id) REFERENCES users (id),
 | 
			
		||||
    FOREIGN KEY (issue_id) REFERENCES issues (id)
 | 
			
		||||
 
 | 
			
		||||
@@ -17,7 +17,18 @@
 | 
			
		||||
 | 
			
		||||
        <div>
 | 
			
		||||
            <h3>Cover</h3>
 | 
			
		||||
            <input id="image-upload" name="issue-image" type="file" required hx-post="/issue/upload-image" />
 | 
			
		||||
            <div class="flex">
 | 
			
		||||
                <label class="btn text-center" for="image-upload">Bild hochladen</label>
 | 
			
		||||
                <input class="hidden" id="image-upload" name="issue-image" type="file" required
 | 
			
		||||
                    hx-post="/issue/upload-image" />
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <div>
 | 
			
		||||
            <h3>Titel</h3>
 | 
			
		||||
            <div class="flex flex-col gap-y-1">
 | 
			
		||||
                <input name="issue-title" type="text" />
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <div>
 | 
			
		||||
 
 | 
			
		||||
@@ -21,6 +21,11 @@
 | 
			
		||||
    <div>
 | 
			
		||||
        <span>Tags</span>
 | 
			
		||||
        <div class="flex flex-wrap gap-x-4">
 | 
			
		||||
            <div>
 | 
			
		||||
                <input id="issue" name="issue" type="checkbox" />
 | 
			
		||||
                <label for="issue">Orient Express</label>
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
            {{range .Tags}}
 | 
			
		||||
            <div>
 | 
			
		||||
                <input id="{{.Name}}" name="tags" type="checkbox" value="{{.ID}}" />
 | 
			
		||||
 
 | 
			
		||||
@@ -16,7 +16,7 @@
 | 
			
		||||
    {{if lt . 3}}
 | 
			
		||||
    <div class="mb-3">
 | 
			
		||||
        <h2>Redakteur</h2>
 | 
			
		||||
        <div class="grid grid-cols-2 gap-4">
 | 
			
		||||
        <div class="grid grid-cols-2 gap-x-4 gap-y-2">
 | 
			
		||||
            <button class="btn" hx-get="/article/all-unpublished" hx-target="#page-content">
 | 
			
		||||
                Unveröffentlichte Artikel
 | 
			
		||||
            </button>
 | 
			
		||||
@@ -28,9 +28,14 @@
 | 
			
		||||
    {{if lt . 2}}
 | 
			
		||||
    <div class="mb-3">
 | 
			
		||||
        <h2>Herausgeber</h2>
 | 
			
		||||
        <div class="grid grid-cols-2 gap-4">
 | 
			
		||||
        <div class="grid grid-cols-2 gap-x-4 gap-y-2">
 | 
			
		||||
            <button class="btn" hx-get="/issue/this" hx-target="#page-content">Diese Ausgabe</button>
 | 
			
		||||
            <button class="btn" hx-get="/article/all-published" hx-target="#page-content">Artikel löschen</button>
 | 
			
		||||
            <form class="flex" hx-encoding="multipart/form-data">
 | 
			
		||||
                <label class="btn text-center" for="pdf-upload">PDF hochladen</label>
 | 
			
		||||
                <input accept=".pdf" class="hidden" id="pdf-upload" name="pdf-upload" type="file"
 | 
			
		||||
                    hx-post="/pdf/upload" />
 | 
			
		||||
            </form>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
    {{end}}
 | 
			
		||||
 
 | 
			
		||||
@@ -21,6 +21,11 @@
 | 
			
		||||
    <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
 | 
			
		||||
 
 | 
			
		||||
@@ -4,12 +4,12 @@
 | 
			
		||||
<div>
 | 
			
		||||
    <span>Titel</span>
 | 
			
		||||
    <div class="bg-white border mb-3 px-2 py-2 rounded-md w-full">
 | 
			
		||||
        {{.Title}}
 | 
			
		||||
        {{.Article.Title}}
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <span>Beschreibung</span>
 | 
			
		||||
    <div class="bg-white border mb-3 px-2 py-2 rounded-md w-full">
 | 
			
		||||
        {{.Description}}
 | 
			
		||||
        {{.Article.Description}}
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <span>Artikel</span>
 | 
			
		||||
@@ -21,16 +21,21 @@
 | 
			
		||||
 | 
			
		||||
    <span>Tags</span>
 | 
			
		||||
    <div class="bg-white border mb-3 px-2 py-2 rounded-md w-full">
 | 
			
		||||
        {{if .Article.IsInIssue}}
 | 
			
		||||
        <span>Orient Express</span>
 | 
			
		||||
        <br>
 | 
			
		||||
        {{end}}
 | 
			
		||||
        {{range .Tags}}
 | 
			
		||||
        {{.Name}}
 | 
			
		||||
        <span>{{.Name}}</span>
 | 
			
		||||
        <br>
 | 
			
		||||
        {{end}}
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <div class="btn-area">
 | 
			
		||||
        <input class="action-btn" type="submit" value="Veröffentlichen" hx-get="/article/publish/{{.ID}}"
 | 
			
		||||
        <input class="action-btn" type="submit" value="Veröffentlichen" hx-get="/article/publish/{{.Article.ID}}"
 | 
			
		||||
            hx-target="#page-content" />
 | 
			
		||||
        <input class="btn" type="submit" value="Ablehnen" hx-get="/article/reject/{{.Article.ID}}"
 | 
			
		||||
            hx-target="#page-content" />
 | 
			
		||||
        <input class="btn" type="submit" value="Ablehnen" hx-get="/article/reject/{{.ID}}" hx-target="#page-content" />
 | 
			
		||||
        <button class="btn" hx-get="/hub" hx-target="#page-content">Zurück</button>
 | 
			
		||||
    </div>
 | 
			
		||||
</div>
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user