~evanj/cms

934d7839c31c509d5fa6a1ba06db2913ab5c4c06 — Evan M Jones 8 months ago da76b84
WIP(*): Creating and querying content is working. MVP.
M internal/c/c.go => internal/c/c.go +1 -0
@@ 77,6 77,7 @@ func (c *Controller) Error(w http.ResponseWriter, r *http.Request, code int, str
func (c *Controller) HTML(w http.ResponseWriter, r *http.Request, tmpl *template.Template, data interface{}) {
	buf := bytes.Buffer{}
	if err := tmpl.Execute(&buf, data); err != nil {
		c.log.Println(err)
		c.Error(w, r, http.StatusInternalServerError, "failed to build html response")
		return
	}

M internal/c/contenttype/contenttype.go => internal/c/contenttype/contenttype.go +29 -0
@@ 4,9 4,11 @@ import (
	"fmt"
	"log"
	"net/http"
	"strconv"
	"strings"

	"git.sr.ht/~evanj/cms/internal/c"
	"git.sr.ht/~evanj/cms/internal/m/content"
	"git.sr.ht/~evanj/cms/internal/m/contenttype"
	"git.sr.ht/~evanj/cms/internal/m/space"
	"git.sr.ht/~evanj/cms/internal/m/user"


@@ 29,6 31,7 @@ type dber interface {
	SpaceGet(user user.User, spaceID string) (space.Space, error)
	ContentTypeNew(space space.Space, name string, params []db.ContentTypeNewParam) (contenttype.ContentType, error)
	ContentTypeGet(space space.Space, contenttypeID string) (contenttype.ContentType, error)
	ContentPerContentType(space space.Space, ct contenttype.ContentType, page int) ([]content.Content, error)
}

func New(log *log.Logger, db dber) *ContentType {


@@ 83,6 86,18 @@ func (c *ContentType) create(w http.ResponseWriter, r *http.Request) {
		return
	}

	// Enforce content always has a value for value type of "name"
	hasName := false
	for _, p := range params {
		if p.Name == "name" {
			hasName = true
		}
	}
	if !hasName {
		c.Error(w, r, http.StatusInternalServerError, "must have field of \"name\" for searchability")
		return
	}

	ct, err := c.db.ContentTypeNew(space, name, params)
	if err != nil {
		c.log.Println(err)


@@ 114,10 129,24 @@ func (c *ContentType) serve(w http.ResponseWriter, r *http.Request, spaceID, con
		return
	}

	page, err := strconv.Atoi(r.URL.Query().Get("page"))
	if err != nil || page < 1 {
		page = 1
	}
	page-- // Show one to user but start counting at zero for us.

	list, err := c.db.ContentPerContentType(space, ct, page)
	if err != nil {
		c.log.Println(err)
		c.Error(w, r, http.StatusInternalServerError, "failed to find content for contenttype")
		return
	}

	c.HTML(w, r, contenttypeHTML, map[string]interface{}{
		"User":        user,
		"Space":       space,
		"ContentType": ct,
		"ContentList": list,
	})
}


M internal/c/space/space.go => internal/c/space/space.go +0 -1
@@ 56,7 56,6 @@ func (s *Space) serve(w http.ResponseWriter, r *http.Request, spaceID string) {
	if err != nil || page < 1 {
		page = 1
	}

	page-- // Show one to user but start counting at zero for us.

	cts, err := s.db.ContentTypesPerSpace(space, page)

M internal/m/content/content.go => internal/m/content/content.go +2 -0
@@ 9,4 9,6 @@ type Content interface {
	ID() string
	Type() contenttype.ContentType
	Values() []value.Value
	ValueByName(name string) (value.Value, bool)
	MustValueByName(name string) value.Value
}

M internal/m/value/value.go => internal/m/value/value.go +2 -4
@@ 1,9 1,7 @@
package value

import "git.sr.ht/~evanj/cms/internal/m/valuetype"

type Value interface {
	ID() string
	Type() valuetype.ValueType
	Type() string
	Name() string
	Value() string
}

M internal/s/db/content.go => internal/s/db/content.go +104 -55
@@ 7,7 7,6 @@ import (
	"git.sr.ht/~evanj/cms/internal/m/contenttype"
	"git.sr.ht/~evanj/cms/internal/m/space"
	"git.sr.ht/~evanj/cms/internal/m/value"
	"git.sr.ht/~evanj/cms/internal/m/valuetype"
)

type Content struct {


@@ 19,19 18,53 @@ type Content struct {
}

type ContentValue struct {
	id          string
	valuetypeID string
	value       string

	valuetype valuetype.ValueType
	typ   string // StringSmall
	name  string // Title of a blog post
	value string // "My First Blog Post!"
}

var (
	queryContentNew        = `INSERT INTO cms_content (CONTENTTYPE_ID) VALUES (?);`
	queryContentGetByID    = `SELECT ID, CONTENTTYPE_ID FROM cms_content WHERE ID = ?;`
	queryValueNew          = `INSERT INTO cms_value (CONTENT_ID, VALUETYPE_ID, VALUE) VALUES (?, (SELECT VALUETYPE_ID FROM cms_contenttype_to_valuetype WHERE CONTENTTYPE_ID = ? AND NAME = ? LIMIT 1), ?);`
	queryValueGetByID      = `SELECT ID, VALUETYPE_ID, VALUE FROM cms_value WHERE ID = ?;`
	queryValueGetByContent = `SELECT ID, VALUETYPE_ID, VALUE FROM cms_value WHERE CONTENT_ID = ?;`
const (
	queryContentNew = `
		INSERT INTO cms_content (CONTENTTYPE_ID) 
		VALUES (?);
	`

	queryContentGetByID = `
		SELECT ID, CONTENTTYPE_ID 
		FROM cms_content 
		WHERE ID = ?;
	`

	queryContentListByContentType = `
		SELECT ID, CONTENTTYPE_ID 
		FROM cms_content 
		WHERE CONTENTTYPE_ID = ? LIMIT ? OFFSET ?;
	`

	queryValueNew = `
		INSERT INTO cms_value (CONTENT_ID, CONTENTTYPE_TO_VALUETYPE_ID, VALUE) 
		VALUES (?, (SELECT ID FROM cms_contenttype_to_valuetype WHERE CONTENTTYPE_ID = ? AND NAME = ? LIMIT 1), ?);
	`

	queryValueGetByID = `
		SELECT cms_valuetype.VALUE as TYPE, cms_contenttype_to_valuetype.NAME, cms_value.VALUE
		FROM cms_value 
		JOIN cms_contenttype_to_valuetype 
		ON cms_contenttype_to_valuetype.ID = cms_value.CONTENTTYPE_TO_VALUETYPE_ID
		JOIN cms_valuetype
		ON cms_valuetype.ID = cms_contenttype_to_valuetype.VALUETYPE_ID
		WHERE cms_value.ID = ?;
	`

	queryValueListByContent = `
		SELECT cms_valuetype.VALUE as TYPE, cms_contenttype_to_valuetype.NAME, cms_value.VALUE
		FROM cms_value 
		JOIN cms_contenttype_to_valuetype 
		ON cms_contenttype_to_valuetype.ID = cms_value.CONTENTTYPE_TO_VALUETYPE_ID
		JOIN cms_valuetype
		ON cms_valuetype.ID = cms_contenttype_to_valuetype.VALUETYPE_ID
		WHERE CONTENT_ID = ?;
	`
)

type ContentNewParam struct {


@@ 55,7 88,7 @@ func (db *DB) ContentNew(space space.Space, ct contenttype.ContentType, params [
	var content Content
	if err := db.QueryRow(queryContentGetByID, contentID).Scan(&content.id, &content.contenttypeID); err != nil {
		db.log.Println("db.QueryRow", err)
		return nil, fmt.Errorf("failed to find space created")
		return nil, fmt.Errorf("failed to find content created")
	}

	for _, item := range params {


@@ 73,23 106,10 @@ func (db *DB) ContentNew(space space.Space, ct contenttype.ContentType, params [
		}

		var value ContentValue
		if err := db.QueryRow(queryValueGetByID, valueID).Scan(&value.id, &value.valuetypeID, &value.value); err != nil {
		if err := db.QueryRow(queryValueGetByID, valueID).Scan(&value.typ, &value.name, &value.value); err != nil {
			db.log.Println("db.QueryRow", err)
			return nil, fmt.Errorf("failed to find space created")
		}

		found := false
		for _, field := range ct.Fields() {
			if field.ID() == value.valuetypeID {
				found = true
				value.valuetype = field
			}
		}

		if !found {
			return nil, fmt.Errorf("failed to find value's valuetype")
			return nil, fmt.Errorf("failed to find value created")
		}

		content.values = append(content.values, value)
	}



@@ 103,7 123,36 @@ func (db *DB) ContentNew(space space.Space, ct contenttype.ContentType, params [

func (db *DB) ContentPerContentType(space space.Space, ct contenttype.ContentType, page int) ([]content.Content, error) {
	var ret []content.Content
	return ret, fmt.Errorf("TODO")
	rows, err := db.Query(queryContentListByContentType, ct.ID(), perPage, perPage*page)
	if err != nil {
		db.log.Println(err)
		return nil, err
	}

	for rows.Next() {
		var content Content
		if err := rows.Scan(&content.id, &content.contenttypeID); err != nil {
			return nil, err
		}

		rows, err := db.Query(queryValueListByContent, content.id)
		if err != nil {
			db.log.Println(err)
			return nil, err
		}

		for rows.Next() {
			var value ContentValue
			if err := rows.Scan(&value.typ, &value.name, &value.value); err != nil {
				return nil, err
			}
			content.values = append(content.values, value)
		}

		ret = append(ret, &content)
	}

	return ret, nil
}

func (db *DB) ContentGet(space space.Space, ct contenttype.ContentType, contentID string) (content.Content, error) {


@@ 113,36 162,20 @@ func (db *DB) ContentGet(space space.Space, ct contenttype.ContentType, contentI
		return nil, fmt.Errorf("failed to find space")
	}

	rows, err := db.Query(queryValueGetByContent, content.ID())
	rows, err := db.Query(queryValueListByContent, content.ID())
	if err != nil {
		db.log.Println(err)
		return nil, fmt.Errorf("failed to find values(s)")
	}

	for rows.Next() {
		var value ContentValue
		if err := rows.Scan(&value.id, &value.valuetypeID, &value.value); err != nil {
		if err := rows.Scan(&value.typ, &value.name, &value.value); err != nil {
			return nil, fmt.Errorf("failed to scan values(s)")
		}

		found := false
		for _, field := range ct.Fields() {
			if field.ID() == value.valuetypeID {
				found = true
				value.valuetype = field
			}
		}

		if !found {
			return nil, fmt.Errorf("failed to find value's valuetype")
		}

		content.values = append(content.values, value)
	}

	if len(ct.Fields()) != len(content.values) {
		return nil, fmt.Errorf("failed to find all values")
	}

	content.contenttype = ct
	return &content, nil
}


@@ 159,21 192,37 @@ func (c *Content) Values() []value.Value {
	var ret []value.Value
	for _, item := range c.values {
		ret = append(ret, &ContentValue{
			item.id,
			item.valuetypeID,
			item.typ,
			item.name,
			item.value,
			item.valuetype,
		})
	}
	return ret
}

func (c *ContentValue) ID() string {
	return c.id
func (c *Content) ValueByName(name string) (value.Value, bool) {
	for _, val := range c.Values() {
		if val.Name() == name {
			return val, true
		}
	}
	return nil, false
}

func (c *Content) MustValueByName(name string) (ret value.Value) {
	val, ok := c.ValueByName(name)
	if ok {
		return val
	}
	return
}

func (c *ContentValue) Type() string {
	return c.typ
}

func (c *ContentValue) Type() valuetype.ValueType {
	return c.valuetype
func (c *ContentValue) Name() string {
	return c.name
}

func (c *ContentValue) Value() string {

M internal/s/db/db.go => internal/s/db/db.go +2 -2
@@ 121,8 121,8 @@ func (db *DB) EnsureSetup() error {
			ID INTEGER PRIMARY KEY AUTOINCREMENT,
			VALUE TEXT NOT NULL,
			CONTENT_ID INTEGER NOT NULL,
			VALUETYPE_ID INTEGER NOT NULL,
			FOREIGN KEY(VALUETYPE_ID) REFERENCES cms_valuetype(ID)
			CONTENTTYPE_TO_VALUETYPE_ID INTEGER NOT NULL,
			FOREIGN KEY(CONTENTTYPE_TO_VALUETYPE_ID) REFERENCES cms_contenttype_to_valuetype(ID)
		);
	`)


M internal/s/tmpl/html/content.html => internal/s/tmpl/html/content.html +17 -6
@@ 19,7 19,7 @@
  <main>
    <header>
      <h1>CMS</h1>
      <p>A flexible CMS for everyone.</p>
      <p>An old-school CMS for most.</p>
    </header>
    <hr/>
    <article>


@@ 28,19 28,30 @@
        <a href='/contenttype/{{ .Space.ID }}/{{ .ContentType.ID }}'>Back</a>
      </nav>

      <h2>{{ .Space.Name }}: {{ .ContentType.Name }}: {{ .Content.ID }}</h2>
      <h2>
        {{ .Space.Name }}
        >
        {{ .ContentType.Name }}
        >
        {{ (.Content.MustValueByName "name").Value }}
      </h2>

      <form method=POST action='/content/update' enctype='multipart/form-data'>
        <legend>Create Content:</legend>
        <legend>Update Content:</legend>
        <input required type=hidden name=space value="{{ .Space.ID }}" />
        <input required type=hidden name=contenttype value="{{ .ContentType.ID }}" />
        <input required type=hidden name=content value="{{ .Content.ID }}" />

        <br/>

        {{ range .Content.Values }}

          {{ .Type.Name }}
          {{ .Type.Type }}
          {{ .Value }}
          <strong>type:</strong> {{ .Type }}
          <br/>
          <strong>name:</strong> {{ .Name }}
          <br/>
          <strong>value:</strong> {{ .Value }}
          <br/>
          <br/>

        {{ end }}

M internal/s/tmpl/html/contenttype.html => internal/s/tmpl/html/contenttype.html +18 -6
@@ 19,7 19,7 @@
  <main>
    <header>
      <h1>CMS</h1>
      <p>A flexible CMS for everyone.</p>
      <p>An old-school CMS for most.</p>
    </header>
    <hr/>
    <article>


@@ 28,10 28,14 @@
        <a href='/space/{{ .Space.ID }}'>Back</a>
      </nav>

      <h2>{{ .Space.Name }}: {{ .ContentType.Name }}</h2>
      <h2>
        {{ .Space.Name }}
        > 
        {{ .ContentType.Name }}
      </h2>

      <form method=POST action='/content/new' enctype='multipart/form-data'>
        <legend>Create Content:</legend>
        <legend>Create a <strong>{{ .ContentType.Name }}</strong>:</legend>
        <input required type=hidden name=space value="{{ .Space.ID }}" />
        <input required type=hidden name=contenttype value="{{ .ContentType.ID }}" />



@@ 75,9 79,17 @@
        <input type=submit value=Go />
      </form>

      <p>Browse Content:</p>
      {{ if .Contents }}
        TODO
      <p>Browse <strong>{{ .ContentType.Name }}</strong> Content:</p>
      {{ if .ContentList }}
        <ul>
          {{ range .ContentList }}
            <li> 
              <a href='/content/{{ $.Space.ID }}/{{ $.ContentType.ID }}/{{ .ID }}'>
                {{ (.MustValueByName "name").Value }}
              </a>
            </li>
          {{ end }}
        </ul>
      {{ else }}
        <p>No content has been created with a content type of 
        {{ .ContentType.Name }}</p>

M internal/s/tmpl/html/index.html => internal/s/tmpl/html/index.html +9 -9
@@ 16,7 16,7 @@
  <main>
    <header>
      <h1>CMS</h1>
      <p>A flexible CMS for everyone.</p>
      <p>An old-school CMS for most.</p>
    </header>
    <hr/>
    <article>


@@ 26,8 26,15 @@

        <h2>Welcome back, {{ .User.Name }}.</h2>

        <p>Available Spaces:</p>
        <form method=POST action='/space/new' enctype='multipart/form-data'>
          <legend>Create Space</legend>
          <input required type=text name=name placeholder=name />
          <input required type=text name=desc placeholder=description />
          <input type=submit value=Go />
        </form>


        <p>Available Spaces:</p>
        {{ if .Spaces }}
          <ul>
            {{ range .Spaces }}


@@ 38,13 45,6 @@
          <p>You haven't created any spaces yet.</p>
        {{ end }}

        <form method=POST action='/space/new' enctype='multipart/form-data'>
          <legend>Create Space</legend>
          <input required type=text name=name placeholder=name />
          <input required type=text name=desc placeholder=description />
          <input type=submit value=Go />
        </form>

        <form method=POST action='/user/logout' enctype='multipart/form-data'>
          <legend>Logout</legend>
          <input type=submit value=Go />

M internal/s/tmpl/html/space.html => internal/s/tmpl/html/space.html +3 -3
@@ 18,7 18,7 @@
  <main>
    <header>
      <h1>CMS</h1>
      <p>A flexible CMS for everyone.</p>
      <p>An old-school CMS for most.</p>
    </header>
    <hr/>
    <article>


@@ 34,8 34,8 @@
        <input required type=hidden name=space value="{{ .Space.ID }}" />
        <input required type=text name=name placeholder="content type name" />
        <div id='first-fieldset'>
          <input required type=text name="field_name_1" placeholder="try 'name' (for searchability)" />
          <select required name="field_type_1">
          <input readonly="readonly" required type=text name="field_name_1" value="name" />
          <select readonly="readonly" required name="field_type_1">
            <option disabled value>Field Type</option>
            <option selected value="StringSmall">String Small</option>
            <option disabled value="StringBig">String Big</option>

M internal/s/tmpl/tmpls_embed.go => internal/s/tmpl/tmpls_embed.go +47 -24
@@ 28,7 28,7 @@ func init() {
  <main>
    <header>
      <h1>CMS</h1>
      <p>A flexible CMS for everyone.</p>
      <p>An old-school CMS for most.</p>
    </header>
    <hr/>
    <article>


@@ 37,19 37,30 @@ func init() {
        <a href='/contenttype/{{ .Space.ID }}/{{ .ContentType.ID }}'>Back</a>
      </nav>

      <h2>{{ .Space.Name }}: {{ .ContentType.Name }}: {{ .Content.ID }}</h2>
      <h2>
        {{ .Space.Name }}
        >
        {{ .ContentType.Name }}
        >
        {{ (.Content.MustValueByName "name").Value }}
      </h2>

      <form method=POST action='/content/update' enctype='multipart/form-data'>
        <legend>Create Content:</legend>
        <legend>Update Content:</legend>
        <input required type=hidden name=space value="{{ .Space.ID }}" />
        <input required type=hidden name=contenttype value="{{ .ContentType.ID }}" />
        <input required type=hidden name=content value="{{ .Content.ID }}" />

        <br/>

        {{ range .Content.Values }}

          {{ .Type.Name }}
          {{ .Type.Type }}
          {{ .Value }}
          <strong>type:</strong> {{ .Type }}
          <br/>
          <strong>name:</strong> {{ .Name }}
          <br/>
          <strong>value:</strong> {{ .Value }}
          <br/>
          <br/>

        {{ end }}


@@ 89,7 100,7 @@ func init() {
  <main>
    <header>
      <h1>CMS</h1>
      <p>A flexible CMS for everyone.</p>
      <p>An old-school CMS for most.</p>
    </header>
    <hr/>
    <article>


@@ 98,10 109,14 @@ func init() {
        <a href='/space/{{ .Space.ID }}'>Back</a>
      </nav>

      <h2>{{ .Space.Name }}: {{ .ContentType.Name }}</h2>
      <h2>
        {{ .Space.Name }}
        > 
        {{ .ContentType.Name }}
      </h2>

      <form method=POST action='/content/new' enctype='multipart/form-data'>
        <legend>Create Content:</legend>
        <legend>Create a <strong>{{ .ContentType.Name }}</strong>:</legend>
        <input required type=hidden name=space value="{{ .Space.ID }}" />
        <input required type=hidden name=contenttype value="{{ .ContentType.ID }}" />



@@ 145,9 160,17 @@ func init() {
        <input type=submit value=Go />
      </form>

      <p>Browse Content:</p>
      {{ if .Contents }}
        TODO
      <p>Browse <strong>{{ .ContentType.Name }}</strong> Content:</p>
      {{ if .ContentList }}
        <ul>
          {{ range .ContentList }}
            <li> 
              <a href='/content/{{ $.Space.ID }}/{{ $.ContentType.ID }}/{{ .ID }}'>
                {{ (.MustValueByName "name").Value }}
              </a>
            </li>
          {{ end }}
        </ul>
      {{ else }}
        <p>No content has been created with a content type of 
        {{ .ContentType.Name }}</p>


@@ 182,7 205,7 @@ func init() {
  <main>
    <header>
      <h1>CMS</h1>
      <p>A flexible CMS for everyone.</p>
      <p>An old-school CMS for most.</p>
    </header>
    <hr/>
    <article>


@@ 192,8 215,15 @@ func init() {

        <h2>Welcome back, {{ .User.Name }}.</h2>

        <p>Available Spaces:</p>
        <form method=POST action='/space/new' enctype='multipart/form-data'>
          <legend>Create Space</legend>
          <input required type=text name=name placeholder=name />
          <input required type=text name=desc placeholder=description />
          <input type=submit value=Go />
        </form>


        <p>Available Spaces:</p>
        {{ if .Spaces }}
          <ul>
            {{ range .Spaces }}


@@ 204,13 234,6 @@ func init() {
          <p>You haven't created any spaces yet.</p>
        {{ end }}

        <form method=POST action='/space/new' enctype='multipart/form-data'>
          <legend>Create Space</legend>
          <input required type=text name=name placeholder=name />
          <input required type=text name=desc placeholder=description />
          <input type=submit value=Go />
        </form>

        <form method=POST action='/user/logout' enctype='multipart/form-data'>
          <legend>Logout</legend>
          <input type=submit value=Go />


@@ 266,7 289,7 @@ func init() {
  <main>
    <header>
      <h1>CMS</h1>
      <p>A flexible CMS for everyone.</p>
      <p>An old-school CMS for most.</p>
    </header>
    <hr/>
    <article>


@@ 282,8 305,8 @@ func init() {
        <input required type=hidden name=space value="{{ .Space.ID }}" />
        <input required type=text name=name placeholder="content type name" />
        <div id='first-fieldset'>
          <input required type=text name="field_name_1" placeholder="try 'name' (for searchability)" />
          <select required name="field_type_1">
          <input readonly="readonly" required type=text name="field_name_1" value="name" />
          <select readonly="readonly" required name="field_type_1">
            <option disabled value>Field Type</option>
            <option selected value="StringSmall">String Small</option>
            <option disabled value="StringBig">String Big</option>