first draft of categories #2

This commit is contained in:
Lars Hoogestraat 2018-11-13 18:05:37 +01:00
parent 9bcc96d845
commit f017bd8f40
9 changed files with 137 additions and 41 deletions

View File

@ -53,7 +53,6 @@ TODOs
* Revisit Makefile (v0.3)
* Review async mails
* Review preview of articles and sites / error handling
* Categories?
* Roles?
Licence

View File

@ -91,7 +91,7 @@ func GetArticleByIDHandler(ctx *middleware.AppContext, w http.ResponseWriter, r
func ListArticlesHandler(ctx *middleware.AppContext, w http.ResponseWriter, r *http.Request) *middleware.Template {
page := getPageParam(r)
t, err := ctx.ArticleService.CountArticles(nil, models.OnlyPublished)
t, err := ctx.ArticleService.CountArticles(nil, nil, models.OnlyPublished)
p := &models.Pagination{
Total: t,
@ -108,7 +108,7 @@ func ListArticlesHandler(ctx *middleware.AppContext, w http.ResponseWriter, r *h
}
}
a, err := ctx.ArticleService.ListArticles(nil, p, models.OnlyPublished)
a, err := ctx.ArticleService.ListArticles(nil, nil, p, models.OnlyPublished)
if err != nil {
return &middleware.Template{
@ -139,9 +139,73 @@ func ListArticlesHandler(ctx *middleware.AppContext, w http.ResponseWriter, r *h
}
}
//ListArticlesHandler returns the template which contains all published articles
func ListArticlesCategoryHandler(ctx *middleware.AppContext, w http.ResponseWriter, r *http.Request) *middleware.Template {
page := getPageParam(r)
category := getVar(r, "category")
c, err := ctx.CategoryService.GetCategoryBySlug(category)
if err != nil {
return &middleware.Template{
Name: tplArticles,
Active: "articles",
Err: err,
}
}
t, err := ctx.ArticleService.CountArticles(nil, c, models.OnlyPublished)
p := &models.Pagination{
Total: t,
Limit: ctx.ConfigService.ArticlesPerPage,
CurrentPage: page,
RelURL: "articles/page",
}
if err != nil {
return &middleware.Template{
Name: tplArticles,
Active: "articles",
Err: err,
}
}
a, err := ctx.ArticleService.ListArticles(nil, c, p, models.OnlyPublished)
if err != nil {
return &middleware.Template{
Name: tplArticles,
Active: "articles",
Err: err,
}
}
cs, err := ctx.CategoryService.ListCategories(models.CategoriesWithArticles)
if err != nil {
return &middleware.Template{
Name: tplArticles,
Active: "articles",
Err: err,
}
}
return &middleware.Template{
Name: tplArticles,
Active: "articles",
Data: map[string]interface{}{
"articles": a,
"categories": cs,
"pagination": p,
},
}
}
//IndexArticlesHandler returns the template information for the index page
func IndexArticlesHandler(ctx *middleware.AppContext, w http.ResponseWriter, r *http.Request) *middleware.Template {
a, err := ctx.ArticleService.IndexArticles(nil, nil, models.OnlyPublished)
a, err := ctx.ArticleService.IndexArticles(nil, nil, nil, models.OnlyPublished)
if err != nil {
return &middleware.Template{
@ -183,7 +247,7 @@ func RSSFeed(ctx *middleware.AppContext, w http.ResponseWriter, r *http.Request)
func AdminListArticlesHandler(ctx *middleware.AppContext, w http.ResponseWriter, r *http.Request) *middleware.Template {
u, _ := middleware.User(r)
t, err := ctx.ArticleService.CountArticles(u, models.All)
t, err := ctx.ArticleService.CountArticles(u, nil, models.All)
if err != nil {
return &middleware.Template{
@ -200,7 +264,7 @@ func AdminListArticlesHandler(ctx *middleware.AppContext, w http.ResponseWriter,
RelURL: "admin/articles/page",
}
a, err := ctx.ArticleService.ListArticles(u, p, models.All)
a, err := ctx.ArticleService.ListArticles(u, nil, p, models.All)
if err != nil {
return &middleware.Template{

View File

@ -35,8 +35,8 @@ type Article struct {
//ArticleDatasourceService defines an interface for CRUD operations of articles
type ArticleDatasourceService interface {
Create(a *Article) (int, error)
List(u *User, p *Pagination, pc PublishedCriteria) ([]Article, error)
Count(u *User, pc PublishedCriteria) (int, error)
List(u *User, c *Category, p *Pagination, pc PublishedCriteria) ([]Article, error)
Count(u *User, c *Category, pc PublishedCriteria) (int, error)
Get(articleID int, u *User, pc PublishedCriteria) (*Article, error)
GetBySlug(slug string, u *User, pc PublishedCriteria) (*Article, error)
Publish(a *Article) error
@ -220,14 +220,14 @@ func (as ArticleService) GetArticleByID(id int, u *User, pc PublishedCriteria) (
// CountArticles returns the number of articles.
// The publishedCriteria defines whether the published and/or unpublished articles should be considered
func (as ArticleService) CountArticles(u *User, pc PublishedCriteria) (int, error) {
return as.Datasource.Count(u, pc)
func (as ArticleService) CountArticles(u *User, c *Category, pc PublishedCriteria) (int, error) {
return as.Datasource.Count(u, c, pc)
}
// ListArticles returns all article by the slug.
// The publishedCriteria defines whether the published and/or unpublished articles should be considered
func (as ArticleService) ListArticles(u *User, p *Pagination, pc PublishedCriteria) ([]Article, error) {
return as.Datasource.List(u, p, pc)
func (as ArticleService) ListArticles(u *User, c *Category, p *Pagination, pc PublishedCriteria) ([]Article, error) {
return as.Datasource.List(u, c, p, pc)
}
// RSSFeed receives a specified number of articles in RSS
@ -239,7 +239,8 @@ func (as ArticleService) RSSFeed(p *Pagination, pc PublishedCriteria) (RSS, erro
Language: as.AppConfig.Language,
}
articles, err := as.Datasource.List(nil, p, pc)
//TODO: categories in rss feeds
articles, err := as.Datasource.List(nil, nil, p, pc)
if err != nil {
return RSS{}, err
@ -274,8 +275,8 @@ type IndexArticle struct {
Articles []Article
}
func (as ArticleService) IndexArticles(u *User, p *Pagination, pc PublishedCriteria) ([]IndexArticle, error) {
arts, err := as.Datasource.List(u, p, pc)
func (as ArticleService) IndexArticles(u *User, c *Category, p *Pagination, pc PublishedCriteria) ([]IndexArticle, error) {
arts, err := as.Datasource.List(u, c, p, pc)
if err != nil {
return nil, err

View File

@ -40,8 +40,8 @@ func (rdb SQLiteArticleDatasource) Create(a *Article) (int, error) {
//List returns a slice of articles; if the user is not nil the number of articles for this explcit user is returned
//the PublishedCritera specifies which articles should be considered
func (rdb SQLiteArticleDatasource) List(u *User, p *Pagination, pc PublishedCriteria) ([]Article, error) {
rows, err := selectSQLiteArticlesStmt(rdb.SQLConn, u, p, pc)
func (rdb SQLiteArticleDatasource) List(u *User, c *Category, p *Pagination, pc PublishedCriteria) ([]Article, error) {
rows, err := selectArticlesStmt(rdb.SQLConn, u, c, p, pc)
if err != nil {
return nil, err
@ -53,10 +53,11 @@ func (rdb SQLiteArticleDatasource) List(u *User, p *Pagination, pc PublishedCrit
for rows.Next() {
var a Article
var c Category
var ru User
if err := rows.Scan(&a.ID, &a.Headline, &a.Teaser, &a.Content, &a.Published, &a.PublishedOn, &a.Slug, &a.LastModified, &ru.ID, &ru.DisplayName,
&ru.Email, &ru.Username, &ru.IsAdmin); err != nil {
&ru.Email, &ru.Username, &ru.IsAdmin, &c.ID, &c.Name); err != nil {
return nil, err
}
@ -74,27 +75,40 @@ func (rdb SQLiteArticleDatasource) List(u *User, p *Pagination, pc PublishedCrit
//Count returns the number of article found; if the user is not nil the number of articles for this explcit user is returned
//the PublishedCritera specifies which articles should be considered
func (rdb SQLiteArticleDatasource) Count(u *User, pc PublishedCriteria) (int, error) {
func (rdb SQLiteArticleDatasource) Count(u *User, c *Category, pc PublishedCriteria) (int, error) {
var total int
var args []interface{}
var stmt bytes.Buffer
stmt.WriteString("SELECT count(id) FROM article WHERE ")
stmt.WriteString("SELECT count(a.id) FROM article a ")
if c != nil {
stmt.WriteString("INNER JOIN category c ON (c.id = a.category_id) ")
} else {
stmt.WriteString("LEFT JOIN category c ON (c.id = a.category_id) ")
}
stmt.WriteString("WHERE ")
if c != nil {
stmt.WriteString("c.name = ? AND ")
args = append(args, c.Name)
}
if u != nil {
if !u.IsAdmin {
stmt.WriteString("user_id=? AND ")
stmt.WriteString("a.user_id=? AND ")
args = append(args, u.ID)
}
}
if pc == NotPublished {
stmt.WriteString("published = '0' ")
stmt.WriteString("a.published = '0' ")
} else if pc == All {
stmt.WriteString("(published='0' OR published='1') ")
stmt.WriteString("(a.published='0' OR a.published='1') ")
} else {
stmt.WriteString("published = '1' ")
stmt.WriteString("a.published = '1' ")
}
if err := rdb.SQLConn.QueryRow(stmt.String(), args...).Scan(&total); err != nil {
@ -110,7 +124,7 @@ func (rdb SQLiteArticleDatasource) Get(articleID int, u *User, pc PublishedCrite
var a Article
var ru User
if err := selectSQLiteArticleStmt(rdb.SQLConn, articleID, "", u, pc).Scan(&a.ID, &a.Headline, &a.PublishedOn, &a.Published, &a.Slug, &a.Teaser, &a.Content,
if err := selectArticleStmt(rdb.SQLConn, articleID, "", u, pc).Scan(&a.ID, &a.Headline, &a.PublishedOn, &a.Published, &a.Slug, &a.Teaser, &a.Content,
&a.LastModified, &ru.ID, &ru.DisplayName, &ru.Email, &ru.Username, &ru.IsAdmin); err != nil {
return nil, err
}
@ -126,7 +140,7 @@ func (rdb SQLiteArticleDatasource) GetBySlug(slug string, u *User, pc PublishedC
var a Article
var ru User
if err := selectSQLiteArticleStmt(rdb.SQLConn, -1, slug, u, pc).Scan(&a.ID, &a.Headline, &a.PublishedOn, &a.Published, &a.Slug, &a.Teaser, &a.Content,
if err := selectArticleStmt(rdb.SQLConn, -1, slug, u, pc).Scan(&a.ID, &a.Headline, &a.PublishedOn, &a.Published, &a.Slug, &a.Teaser, &a.Content,
&a.LastModified, &ru.ID, &ru.DisplayName, &ru.Email, &ru.Username, &ru.IsAdmin); err != nil {
return nil, err
}
@ -170,7 +184,7 @@ func (rdb SQLiteArticleDatasource) Delete(articleID int) error {
return nil
}
func selectSQLiteArticleStmt(db *sql.DB, articleID int, slug string, u *User, pc PublishedCriteria) *sql.Row {
func selectArticleStmt(db *sql.DB, articleID int, slug string, u *User, pc PublishedCriteria) *sql.Row {
var stmt bytes.Buffer
var args []interface{}
@ -206,17 +220,30 @@ func selectSQLiteArticleStmt(db *sql.DB, articleID int, slug string, u *User, pc
return db.QueryRow(stmt.String(), args...)
}
func selectSQLiteArticlesStmt(db *sql.DB, u *User, p *Pagination, pc PublishedCriteria) (*sql.Rows, error) {
func selectArticlesStmt(db *sql.DB, u *User, c *Category, p *Pagination, pc PublishedCriteria) (*sql.Rows, error) {
var stmt bytes.Buffer
var args []interface{}
stmt.WriteString("SELECT a.id, a.headline, a.teaser, a.content, a.published, a.published_on, a.slug, a.last_modified, ")
stmt.WriteString("u.id, u.display_name, u.email, u.username, u.is_admin ")
stmt.WriteString("u.id, u.display_name, u.email, u.username, u.is_admin, ")
stmt.WriteString("c.id, c.name ")
stmt.WriteString("FROM article a ")
stmt.WriteString("INNER JOIN user u ON (a.user_id = u.id) ")
if c != nil {
stmt.WriteString("INNER JOIN category c ON (c.id = a.category_id) ")
} else {
stmt.WriteString("LEFT JOIN category c ON (c.id = a.category_id) ")
}
stmt.WriteString("WHERE ")
if c != nil {
stmt.WriteString("c.name = ? AND ")
args = append(args, c.Name)
}
if u != nil {
if !u.IsAdmin {
stmt.WriteString("a.user_id=? AND ")

View File

@ -4,6 +4,7 @@ import (
"database/sql"
"errors"
"fmt"
"net/url"
"strings"
"time"
@ -56,6 +57,11 @@ type CategoryService struct {
Datasource CategoryDatasourceService
}
//SlugEscape escapes the slug for use in URLs
func (c Category) SlugEscape() string {
return url.PathEscape(c.Slug)
}
func (cs CategoryService) GetCategoryBySlug(s string) (*Category, error) {
c, err := cs.Datasource.GetBySlug(s)
@ -116,7 +122,7 @@ func (cs CategoryService) CreateCategory(c *Category) (int, error) {
return cid, nil
}
//UpdateArticle updates a category
//UpdateCategory updates a category
func (cs CategoryService) UpdateCategory(c *Category) error {
if err := c.validate(); err != nil {
return err

View File

@ -141,6 +141,9 @@ func publicRoutes(ctx *m.AppContext, router *mux.Router, chain alice.Chain) {
router.Handle("/", chain.Then(useTemplateHandler(ctx, c.ListArticlesHandler))).Methods("GET")
router.Handle("/index", chain.Then(useTemplateHandler(ctx, c.IndexArticlesHandler))).Methods("GET")
router.Handle("/articles/category/{categorySlug}", chain.Then(useTemplateHandler(ctx, c.ListArticlesCategoryHandler))).Methods("GET")
router.Handle("/articles/category/{categorySlug}/{page}", chain.Then(useTemplateHandler(ctx, c.ListArticlesCategoryHandler))).Methods("GET")
router.Handle("/articles/page/{page}", chain.Then(useTemplateHandler(ctx, c.ListArticlesHandler))).Methods("GET")
router.Handle("/article/{year}/{month}/{slug}", chain.Then(useTemplateHandler(ctx, c.GetArticleHandler))).Methods("GET")
router.Handle("/article/by-id/{articleID}", chain.Then(useTemplateHandler(ctx, c.GetArticleByIDHandler))).Methods("GET")

View File

@ -13,7 +13,7 @@
{{template "front/navigation" .}}
<main>
{{template "skel/flash" .}}
{{template "skel/flash" .}}
<article>
{{with .article}}
<h2 class="article_link">{{.Headline}}</h2>

View File

@ -42,7 +42,7 @@
<ul>
{{range .categories}}
<li>
<a href="#" href="/">{{.Name}}</a>
<a href="/articles/category/{{.SlugEscape}}">{{.Name}}</a>
</li>
{{end}}
</ul>

View File

@ -3,16 +3,12 @@
{{template "front/head" .}}
</head>
<body>
{{template "front/title"}}
<body>
{{template "front/navigation" .}}
<main>
<section class="max--width">
<h2>Error</h2>
{{template "skel/flash" .}}
</section>
<h2>Error</h2>
{{template "skel/flash" .}}
</main>
{{end}}