~evanj/cms

dcecadae58f43a08eb60939c949f21659b4bdc28 — Evan M Jones 1 year, 1 month ago fb49893
WIP(reference list): Getting closer to completing ref list impl.
M cms.go => cms.go +11 -6
@@ 93,6 93,7 @@ func init() {
	if err != nil {
		applogger.Fatal(err)
	}
	_ = cacher

	fs := e3.New(
		os.Getenv("E3_USER"),


@@ 104,26 105,30 @@ func init() {
		log: applogger,
		content: content.New(
			log.New(w, "[cms:content] ", 0),
			cacher,
			// db,
			// cacher,
			db,
			fs,
		),
		contenttype: contenttype.New(
			log.New(w, "[cms:contenttype] ", 0),
			cacher,
			// cacher,
			db,
		),
		space: space.New(
			log.New(w, "[cms:space] ", 0),
			cacher,
			// cacher,
			db,
		),
		user: user.New(
			log.New(w, "[cms:user] ", 0),
			cacher,
			// cacher,
			db,
			os.Getenv("SIGNUP_ENABLE") == "true",
		),
		ping: ping.New(
			log.New(w, "[cms:ping] ", 0),
			cacher,
			// cacher,
			db,
		),
	}
}

M internal/c/content/content.go => internal/c/content/content.go +9 -3
@@ 39,7 39,7 @@ type dber interface {
	ContentGet(space space.Space, ct contenttype.ContentType, contentID string) (content.Content, error)
	ContentUpdate(space space.Space, ct contenttype.ContentType, content content.Content, params []db.ContentUpdateParam) (content.Content, error)
	ContentDelete(space space.Space, ct contenttype.ContentType, content content.Content) error
	ContentSearch(space space.Space, ct contenttype.ContentType, query string, page int) ([]content.Content, error)
	ContentSearch(space space.Space, ct contenttype.ContentType, name, query string, page int) ([]content.Content, error)
	ContentPerContentType(space space.Space, ct contenttype.ContentType, page int) ([]content.Content, error)
}



@@ 275,7 275,8 @@ func (c *Content) update(w http.ResponseWriter, r *http.Request) {

	content, err = c.db.ContentUpdate(space, ct, content, params)
	if err != nil {
		c.Error(w, r, http.StatusInternalServerError, "failed to find update content")
		c.log.Println(err)
		c.Error(w, r, http.StatusInternalServerError, "failed to update content")
		return
	}



@@ 311,6 312,11 @@ func (c *Content) search(w http.ResponseWriter, r *http.Request) {
	spaceID := r.FormValue("space")
	contenttypeID := r.FormValue("contenttype")
	query := r.FormValue("query")
	field := r.FormValue("field")

	if field == "" {
		field = "name"
	}

	page, err := strconv.Atoi(r.URL.Query().Get("page"))
	if err != nil || page < 1 {


@@ 337,7 343,7 @@ func (c *Content) search(w http.ResponseWriter, r *http.Request) {
		c.Error(w, r, http.StatusInternalServerError, "failed to find required contenttype")
	}

	list, err := c.db.ContentSearch(space, ct, query, page)
	list, err := c.db.ContentSearch(space, ct, field, query, page)
	if err != nil {
		c.log.Println(err)
		c.Error(w, r, http.StatusInternalServerError, "failed to find desired content")

M internal/m/value/value.go => internal/m/value/value.go +4 -0
@@ 5,4 5,8 @@ type Value interface {
	Type() string
	Name() string
	Value() string

	// Specific per valuetype
	RefName() string
	RefListNames() string
}

M internal/s/db/content.go => internal/s/db/content.go +112 -18
@@ 63,21 63,6 @@ var (
		WHERE ID = ?;
	`

	queryContentListByNameAndContentType = `
		SELECT DISTINCT cms_content.ID, cms_content.CONTENTTYPE_ID, cms_value_string_small.VALUE as VALUE
		FROM cms_content 

		JOIN cms_value
		ON cms_value.CONTENT_ID = cms_content.ID

		JOIN cms_value_string_small  
		ON cms_value.VALUE_ID = cms_value_string_small.ID

		WHERE cms_content.CONTENTTYPE_ID = ? 
		AND cms_value_string_small.VALUE LIKE ?
		LIMIT ? OFFSET ?;
	`

	queryContentListByContentType = `
		SELECT ID, CONTENTTYPE_ID 
		FROM cms_content 


@@ 290,8 275,76 @@ var (

		ORDER BY ORDER_ID ASC;
	`

	queryContentListByNameAndContentType = `
		SELECT cms_content.ID, cms_content.CONTENTTYPE_ID, cms_value_string_small.VALUE as VALUE
		FROM cms_value

		JOIN cms_content
		ON cms_value.CONTENT_ID = cms_content.ID

		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

		LEFT JOIN cms_value_string_small
		ON cms_value_string_small.ID = cms_value.VALUE_ID

		LEFT JOIN cms_value_string_big
		ON cms_value_string_big.ID = cms_value.VALUE_ID
		
		LEFT JOIN cms_value_date
		ON cms_value_date.ID = cms_value.VALUE_ID

	 	WHERE cms_content.CONTENTTYPE_ID = ? 
	 	AND cms_contenttype_to_valuetype.NAME = ?
	 	AND (
			cms_value_string_small.VALUE LIKE ?
			OR cms_value_string_big.VALUE LIKE ?
			OR cms_value_date.VALUE LIKE ?
		)
	 	LIMIT ? OFFSET ?;
	`
)

func (db *DB) valueReferenceListUpdate(s space.Space, ct contenttype.ContentType, c *Content, t *sql.Tx, valueID string, IDs []string, depth int) error {
	if len(IDs) < 1 {
		return fmt.Errorf("reference list type has no values")
	}

	// wipe out old list values
	// create new list values
	// TODO: Remove these inline queries, please.
	var refListID string
	if err := t.QueryRow("SELECT cms_value_reference_list.ID FROM cms_value JOIN cms_value_reference_list ON cms_value_reference_list.ID = cms_value.VALUE_ID WHERE cms_value.ID = ?", valueID).Scan(&refListID); err != nil {
		db.log.Println("valueID", valueID, "Error", err)
		return fmt.Errorf("failed to query for reference list identifier before reference list update")
	}

	if _, err := t.Exec("DELETE FROM cms_value_reference_list_values WHERE VALUE_ID = ?", refListID); err != nil {
		return fmt.Errorf("failed to remove old reference list values before update")
	}

	var value ContentValue
	value.FieldValue = strings.Join(IDs, "-") // Match what client should send us.
	if err := t.QueryRow(queryValueGetReferenceListByID, valueID).Scan(&value.FieldID, &value.FieldType, &value.FieldName); err != nil {
		return err
	}

	for _, cid := range IDs {
		_, err := t.Exec("INSERT INTO cms_value_reference_list_values (VALUE_ID, CONTENT_ID) VALUES (?, ?);", refListID, cid)
		if err != nil {
			return err
		}
		db.contentValueAttachRefList(t, &value, depth)
	}

	c.ContentValues = append(c.ContentValues, value)
	return nil
}

func (db *DB) valueReferenceListNew(s space.Space, ct contenttype.ContentType, c *Content, t *sql.Tx, fieldName string, IDs []string, depth int) error {
	if len(IDs) < 1 {
		return fmt.Errorf("reference list type has no values")


@@ 426,15 479,44 @@ type ContentUpdateParam struct {
}

func (db *DB) ContentUpdate(space space.Space, ct contenttype.ContentType, content content.Content, params []ContentUpdateParam) (content.Content, error) {
	depth := defaultDepth

	t, err := db.Begin()
	if err != nil {
		return nil, err
	}
	defer t.Rollback()

	for _, item := range params {
		// Special data type. Life is hard.
		if item.Type == valuetype.ReferenceList {
			// TODO: Do we have to do this w/ types? Can you not find a better way?
			// I'm beginning to think we're over using interfaces and it's hurting.
			c, ok := content.(*Content)
			if !ok {
				return nil, fmt.Errorf("failed to type cast value type before attempting to update content")
			}

			if err := db.valueReferenceListUpdate(space, ct, c, t, item.ID, strings.Split(item.Value, "-"), depth); err != nil {
				return nil, err
			}
			continue
		}

		_, _, queryValueUpdate, err := db.valueQuerySetByType(item.Type)
		if err != nil {
			return nil, err
		}
		if _, err := db.Exec(queryValueUpdate, item.Value, item.ID); err != nil {

		if _, err := t.Exec(queryValueUpdate, item.Value, item.ID); err != nil {
			return nil, fmt.Errorf("failed to create update content value '%s'", item.Value)
		}
	}

	if err := t.Commit(); err != nil {
		return nil, err
	}

	return db.ContentGet(space, ct, content.ID())
}



@@ 492,11 574,11 @@ func (db *DB) ContentPerContentType(space space.Space, ct contenttype.ContentTyp
	return ret, nil
}

func (db *DB) ContentSearch(space space.Space, ct contenttype.ContentType, query string, page int) ([]content.Content, error) {
func (db *DB) ContentSearch(space space.Space, ct contenttype.ContentType, name, query string, page int) ([]content.Content, error) {
	s := fmt.Sprintf("%%%s%%", query)

	var ret []content.Content
	rows, err := db.Query(queryContentListByNameAndContentType, ct.ID(), s, perPage, perPage*page)
	rows, err := db.Query(queryContentListByNameAndContentType, ct.ID(), name, s, s, s, perPage, perPage*page)
	if err != nil {
		return nil, err
	}


@@ 695,6 777,18 @@ func (c *ContentValue) Value() string {
	return c.FieldValue
}

func (c *ContentValue) RefName() string {
	return c.FieldReference.MustValueByName("name").Value()
}

func (c *ContentValue) RefListNames() string {
	var names []string
	for _, item := range c.FieldReferenceList {
		names = append(names, item.MustValueByName("name").Value())
	}
	return strings.Join(names, ", ")
}

func (c *ContentValue) MarshalJSON() (ret []byte, err error) {
	var v contentValueJSON


M internal/s/db/space.go => internal/s/db/space.go +1 -1
@@ 57,7 57,7 @@ func (db *DB) SpaceGet(user user.User, spaceID string) (space.Space, error) {
	var id string

	if err := db.QueryRow(queryFindUserToSpace, user.ID(), spaceID).Scan(&id); err != nil {
		db.log.Println("db.QueryRow", err)
		db.log.Println("db.QueryRow", err, "spaceID:", spaceID)
		return nil, fmt.Errorf("failed to find space for user")
	}


M internal/s/tmpl/html/content.html => internal/s/tmpl/html/content.html +4 -4
@@ 53,8 53,8 @@
            {{ end }}

            {{ if eq .Type "Reference" }}
            <input class='output-ref' required type=hidden value="{{ .Value }}" name="{{ .Type }}-{{ .ID}}" />
              <input class='input-ref' type=button value=Open />
              <input class='output-ref' required type=hidden value="{{ .Value }}" name="{{ .Type }}-{{ .ID}}" />
              <input class='input-ref' type=button value="{{ if  .RefName }}{{ .RefName }}{{ else }}Open{{ end}}"/>
              <dialog>
                <menu>
                  <div>


@@ 69,8 69,8 @@
            {{ end }}

            {{ if eq .Type "ReferenceList" }}
              <input class='output-ref' required type=hidden value="{{ .Value }}" name="{{ .Type }}-{{ .Name }}" />
              <input class='input-ref' type=button value=Open />
              <input class='output-ref' required type=hidden value="{{ .Value }}" name="{{ .Type }}-{{ .ID }}" />
              <input class='input-ref' type=button value="{{ if  .RefListNames }}{{ .RefListNames }}{{ else }}Open{{ end}}"/>
              <dialog>
                <menu>
                  <div>

M internal/s/tmpl/js/content.js => internal/s/tmpl/js/content.js +5 -3
@@ 120,8 120,8 @@
        autoselect: true,
        autoselectOnBlur: true, 
        tabAutocomplete: true,
        hint: false,
        // clearOnSelected: true
        // clearOnSelected: true,
        hint: false
      }

      function getopts(url, transform, displayKey) { 


@@ 182,7 182,9 @@
        'FieldValue'
      )

      window.autocomplete(content, opts, [contentOpts]).on('autocomplete:selected', onContentSelected)
      // TODO: Weird behavior here, why do I have to inline this clear on
      // selected? Why can't it exists in contentOpts?
      window.autocomplete(content, Object.assign({}, opts, {clearOnSelected:true}), [contentOpts]).on('autocomplete:selected', onContentSelected)
      function onContentSelected(e, item, dataset, ctx) {
        if (isList) {
          chosenContentIDs.push(item.ContentID)

M internal/s/tmpl/tmpls_embed.go => internal/s/tmpl/tmpls_embed.go +9 -7
@@ 581,8 581,8 @@ blockquote footer {
            {{ end }}

            {{ if eq .Type "Reference" }}
            <input class='output-ref' required type=hidden value="{{ .Value }}" name="{{ .Type }}-{{ .ID}}" />
              <input class='input-ref' type=button value=Open />
              <input class='output-ref' required type=hidden value="{{ .Value }}" name="{{ .Type }}-{{ .ID}}" />
              <input class='input-ref' type=button value="{{ if  .RefName }}{{ .RefName }}{{ else }}Open{{ end}}"/>
              <dialog>
                <menu>
                  <div>


@@ 597,8 597,8 @@ blockquote footer {
            {{ end }}

            {{ if eq .Type "ReferenceList" }}
              <input class='output-ref' required type=hidden value="{{ .Value }}" name="{{ .Type }}-{{ .Name }}" />
              <input class='input-ref' type=button value=Open />
              <input class='output-ref' required type=hidden value="{{ .Value }}" name="{{ .Type }}-{{ .ID }}" />
              <input class='input-ref' type=button value="{{ if  .RefListNames }}{{ .RefListNames }}{{ else }}Open{{ end}}"/>
              <dialog>
                <menu>
                  <div>


@@ 1052,8 1052,8 @@ blockquote footer {
        autoselect: true,
        autoselectOnBlur: true, 
        tabAutocomplete: true,
        hint: false,
        // clearOnSelected: true
        // clearOnSelected: true,
        hint: false
      }

      function getopts(url, transform, displayKey) { 


@@ 1114,7 1114,9 @@ blockquote footer {
        'FieldValue'
      )

      window.autocomplete(content, opts, [contentOpts]).on('autocomplete:selected', onContentSelected)
      // TODO: Weird behavior here, why do I have to inline this clear on
      // selected? Why can't it exists in contentOpts?
      window.autocomplete(content, Object.assign({}, opts, {clearOnSelected:true}), [contentOpts]).on('autocomplete:selected', onContentSelected)
      function onContentSelected(e, item, dataset, ctx) {
        if (isList) {
          chosenContentIDs.push(item.ContentID)

M makefile => makefile +1 -0
@@ 6,6 6,7 @@ BIN=cms
all: vendor gen build

vendor: go.mod go.sum
	go mod tidy
	go mod vendor

build: