diff --git a/.gitignore b/.gitignore
index 61ab71b9..b0b7a129 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,7 @@
go/
+example/monsti-example-module/go
+example/monsti-example-module/monsti-example-module
static/lib/
dist/
-example/data/example/cache/
\ No newline at end of file
+templates/example
+example/data/localhost/cache/
\ No newline at end of file
diff --git a/CHANGES b/CHANGES
index f5f711d1..363df456 100644
--- a/CHANGES
+++ b/CHANGES
@@ -1,3 +1,17 @@
+* 0.13.0 - released 2015/07/22
+ - New features:
+ + Fields of contact forms are configurable.
+ + Added RenderNode signal (replaces NodeContext signal).
+ + Added GUI for list fields.
+ - Changes:
+ + Deprecated NodeContext signal (use RenderNode signal!).
+ + Moved contact form to separate module.
+ + Changed fields (Method, FormData, Form, PostForm) of service.Request.
+ + Refactored service.Field.
+ - Fixed:
+ + Now calling FinishSignal in WaitSignal if an error occurs.
+ + Stop waiting for signal responses if an error occured.
+
* 0.12.0 - released 2015/06/05
- New features:
+ Migrations to new releases will be as pleasant as possible and
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
new file mode 100644
index 00000000..b55d77b2
--- /dev/null
+++ b/CODE_OF_CONDUCT.md
@@ -0,0 +1,15 @@
+# Contributor Code of Conduct
+
+As contributors and maintainers of this project, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities.
+
+We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, or religion.
+
+Examples of unacceptable behavior by participants include the use of sexual language or imagery, derogatory comments or personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct.
+
+Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. Project maintainers who do not follow the Code of Conduct may be removed from the project team.
+
+This code of conduct applies both within project spaces and in public spaces when an individual is representing the project or its community.
+
+Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers.
+
+This Code of Conduct is adapted from the [Contributor Covenant](http:contributor-covenant.org), version 1.0.0, available at [http://contributor-covenant.org/version/1/0/0/](http://contributor-covenant.org/version/1/0/0/)
diff --git a/Makefile b/Makefile
index f84f8f45..81767091 100644
--- a/Makefile
+++ b/Makefile
@@ -5,11 +5,11 @@ GO_GET=$(GO) get $(GO_COMMON_OPTS)
GO_BUILD=$(GO) build $(GO_COMMON_OPTS)
GO_TEST=$(GO) test $(GO_COMMON_OPTS)
-MODULES=daemon
+MODULES=daemon base
LOCALES=de
-MONSTI_VERSION=0.12.0
+MONSTI_VERSION=0.13.0
DEB_VERSION=1
DIST_PATH=dist/monsti-$(MONSTI_VERSION)
@@ -143,7 +143,7 @@ locale/monsti-daemon.pot:
%.po: locale/monsti-daemon.pot
msgmerge -s -U $@ $<
-doc: doc/manual.html
+doc: doc/manual.html doc/release_notes.html
doc/%.html: doc/%.adoc
asciidoc $<
@@ -156,6 +156,6 @@ example/monsti-example-module/monsti-example-module:
example-module: go/bin/monsti-example-module
go/bin/monsti-example-module: example/monsti-example-module/monsti-example-module
- cp example/monsti-example-module/monsti-example-module $(GOPATH)/bin
+ cp -f example/monsti-example-module/monsti-example-module $(GOPATH)/bin
rm -f templates/example
ln -sf ../example/monsti-example-module/templates templates/example
diff --git a/README.md b/README.md
index 7bea1986..21a59a1b 100644
--- a/README.md
+++ b/README.md
@@ -53,6 +53,13 @@ Project
Website: http://www.monsti.org/
Code: http://www.gitorious.org/monsti | http://github.com/monsti
+Contributions
+-------------
+
+Please note that this project is released with a
+[Contributor Code of Conduct](CODE_OF_CONDUCT.md).
+By participating in this project you agree to abide by its terms.
+
Acknowledgements
----------------
diff --git a/api/service/field.go b/api/service/field.go
index 92abc752..0d1dbc03 100644
--- a/api/service/field.go
+++ b/api/service/field.go
@@ -93,18 +93,9 @@ type Field interface {
// The dumped value must be something that can be marshalled into
// JSON by encoding/json.
Dump() interface{}
- // Adds a form field to the given form.
- //
- // The nested map stores the field values used by the form. Locale
- // is used for translations.
- ToFormField(form *htmlwidgets.Form, values NestedMap, field *FieldConfig,
- locale string)
- // Load values from the form submission
- FromFormField(NestedMap, *FieldConfig)
-
- // TODO Replace ToFormField and FromFormField using the Form* methods.
+
// Needed for rendering of nested fields (see ListField).
- FormWidget() htmlwidgets.Widget
+ FormWidget(locale string, field *FieldConfig) htmlwidgets.Widget
FormData() interface{}
FromFormData(data interface{})
}
@@ -138,32 +129,20 @@ func (t BoolField) Dump() interface{} {
return t
}
-func (t BoolField) ToFormField(form *htmlwidgets.Form, data NestedMap,
- field *FieldConfig, locale string) {
- data.Set(field.Id, t)
- form.AddWidget(new(htmlwidgets.BoolWidget), "Fields."+field.Id,
- field.Name.Get(locale), "")
-}
-
-func (t *BoolField) FromFormField(data NestedMap, field *FieldConfig) {
- *t = BoolField(data.Get(field.Id).(bool))
-}
-
func (t *BoolField) Bool() bool {
return bool(*t)
}
func (f BoolField) FormData() interface{} {
- panic("Not implemented")
+ return f
}
func (f *BoolField) FromFormData(data interface{}) {
- panic("Not implemented")
+ *f = BoolField(data.(bool))
}
-func (f BoolField) FormWidget() htmlwidgets.Widget {
- panic("Not implemented")
- return nil
+func (f BoolField) FormWidget(locale string, field *FieldConfig) htmlwidgets.Widget {
+ return new(htmlwidgets.BoolWidget)
}
type CombinedFieldType struct {
@@ -236,18 +215,11 @@ func (f *CombinedField) FromFormData(data interface{}) {
panic("Not implemented")
}
-func (f CombinedField) FormWidget() htmlwidgets.Widget {
+func (f CombinedField) FormWidget(locale string, field *FieldConfig) htmlwidgets.Widget {
panic("Not implemented")
return nil
}
-func (t CombinedField) ToFormField(form *htmlwidgets.Form, data NestedMap,
- field *FieldConfig, locale string) {
-}
-
-func (t *CombinedField) FromFormField(data NestedMap, field *FieldConfig) {
-}
-
type DummyFieldType int
func (_ DummyFieldType) Field() Field {
@@ -277,13 +249,6 @@ func (t DummyField) Dump() interface{} {
return nil
}
-func (t DummyField) ToFormField(form *htmlwidgets.Form, data NestedMap,
- field *FieldConfig, locale string) {
-}
-
-func (t *DummyField) FromFormField(data NestedMap, field *FieldConfig) {
-}
-
func (f DummyField) FormData() interface{} {
return nil
}
@@ -291,7 +256,7 @@ func (f DummyField) FormData() interface{} {
func (f *DummyField) FromFormData(data interface{}) {
}
-func (f DummyField) FormWidget() htmlwidgets.Widget {
+func (f DummyField) FormWidget(locale string, field *FieldConfig) htmlwidgets.Widget {
return nil
}
@@ -380,18 +345,11 @@ func (f *DynamicTypeField) FromFormData(data interface{}) {
panic("Not implemented")
}
-func (f DynamicTypeField) FormWidget() htmlwidgets.Widget {
+func (f DynamicTypeField) FormWidget(locale string, field *FieldConfig) htmlwidgets.Widget {
panic("Not implemented")
return nil
}
-func (t DynamicTypeField) ToFormField(form *htmlwidgets.Form, data NestedMap,
- field *FieldConfig, locale string) {
-}
-
-func (t *DynamicTypeField) FromFormField(data NestedMap, field *FieldConfig) {
-}
-
type RefFieldType int
func (_ RefFieldType) Field() Field {
@@ -421,33 +379,22 @@ func (t RefField) Dump() interface{} {
return string(t)
}
-func (t RefField) ToFormField(form *htmlwidgets.Form, data NestedMap,
- field *FieldConfig, locale string) {
- data.Set(field.Id, string(t))
- G, _, _, _ := gettext.DefaultLocales.Use("", locale)
- widget := new(htmlwidgets.TextWidget)
- if field.Required {
- widget.MinLength = 1
- widget.ValidationError = G("Required.")
- }
- form.AddWidget(widget, "Fields."+field.Id, field.Name.Get(locale), "")
-}
-
-func (t *RefField) FromFormField(data NestedMap, field *FieldConfig) {
- *t = RefField(data.Get(field.Id).(string))
-}
-
func (f RefField) FormData() interface{} {
- panic("Not implemented")
+ return string(f)
}
func (f *RefField) FromFormData(data interface{}) {
- panic("Not implemented")
+ *f = RefField(data.(string))
}
-func (f RefField) FormWidget() htmlwidgets.Widget {
- panic("Not implemented")
- return nil
+func (f RefField) FormWidget(locale string, field *FieldConfig) htmlwidgets.Widget {
+ G, _, _, _ := gettext.DefaultLocales.Use("", locale)
+ widget := new(htmlwidgets.TextWidget)
+ if field.Required {
+ widget.MinLength = 1
+ widget.ValidationError = G("Required.")
+ }
+ return widget
}
type IntegerFieldType int
@@ -479,33 +426,22 @@ func (t IntegerField) Dump() interface{} {
return int(t)
}
-func (t IntegerField) ToFormField(form *htmlwidgets.Form, data NestedMap,
- field *FieldConfig, locale string) {
- data.Set(field.Id, int(t))
- G, _, _, _ := gettext.DefaultLocales.Use("", locale)
- widget := new(htmlwidgets.TextWidget)
- if field.Required {
- widget.MinLength = 1
- widget.ValidationError = G("Required.")
- }
- form.AddWidget(widget, "Fields."+field.Id, field.Name.Get(locale), "")
-}
-
-func (t *IntegerField) FromFormField(data NestedMap, field *FieldConfig) {
- *t = IntegerField(data.Get(field.Id).(int))
-}
-
func (f IntegerField) FormData() interface{} {
- panic("Not implemented")
+ return int(f)
}
func (f *IntegerField) FromFormData(data interface{}) {
- panic("Not implemented")
+ *f = IntegerField(data.(int))
}
-func (f IntegerField) FormWidget() htmlwidgets.Widget {
- panic("Not implemented")
- return nil
+func (f IntegerField) FormWidget(locale string, field *FieldConfig) htmlwidgets.Widget {
+ G, _, _, _ := gettext.DefaultLocales.Use("", locale)
+ widget := new(htmlwidgets.TextWidget)
+ if field.Required {
+ widget.MinLength = 1
+ widget.ValidationError = G("Required.")
+ }
+ return widget
}
type TextFieldType int
@@ -537,33 +473,22 @@ func (t TextField) Dump() interface{} {
return string(t)
}
-func (t TextField) ToFormField(form *htmlwidgets.Form, data NestedMap,
- field *FieldConfig, locale string) {
- data.Set(field.Id, string(t))
- G, _, _, _ := gettext.DefaultLocales.Use("", locale)
- widget := new(htmlwidgets.TextWidget)
- if field.Required {
- widget.MinLength = 1
- widget.ValidationError = G("Required.")
- }
- form.AddWidget(widget, "Fields."+field.Id, field.Name.Get(locale), "")
-}
-
-func (t *TextField) FromFormField(data NestedMap, field *FieldConfig) {
- *t = TextField(data.Get(field.Id).(string))
-}
-
func (f TextField) FormData() interface{} {
- panic("Not implemented")
+ return string(f)
}
func (f *TextField) FromFormData(data interface{}) {
- panic("Not implemented")
+ *f = TextField(data.(string))
}
-func (f TextField) FormWidget() htmlwidgets.Widget {
- panic("Not implemented")
- return nil
+func (f TextField) FormWidget(locale string, field *FieldConfig) htmlwidgets.Widget {
+ G, _, _, _ := gettext.DefaultLocales.Use("", locale)
+ widget := new(htmlwidgets.TextWidget)
+ if field.Required {
+ widget.MinLength = 1
+ widget.ValidationError = G("Required.")
+ }
+ return widget
}
type HTMLFieldType int
@@ -595,30 +520,18 @@ func (t HTMLField) Dump() interface{} {
return string(t)
}
-func (t HTMLField) ToFormField(form *htmlwidgets.Form, data NestedMap,
- field *FieldConfig, locale string) {
- //G, _, _, _ := gettext.DefaultLocales.Use("", locale)
- data.Set(field.Id, string(t))
- widget := form.AddWidget(new(htmlwidgets.TextAreaWidget), "Fields."+field.Id,
- field.Name.Get(locale), "")
- widget.Base().Classes = []string{"html-field"}
-}
-
-func (t *HTMLField) FromFormField(data NestedMap, field *FieldConfig) {
- *t = HTMLField(data.Get(field.Id).(string))
-}
-
func (f HTMLField) FormData() interface{} {
- panic("Not implemented")
+ return string(f)
}
func (f *HTMLField) FromFormData(data interface{}) {
- panic("Not implemented")
+ *f = HTMLField(data.(string))
}
-func (f HTMLField) FormWidget() htmlwidgets.Widget {
- panic("Not implemented")
- return nil
+func (f HTMLField) FormWidget(locale string, field *FieldConfig) htmlwidgets.Widget {
+ widget := new(htmlwidgets.TextAreaWidget)
+ widget.Base().Classes = []string{"html-field"}
+ return widget
}
type FileFieldType int
@@ -649,28 +562,16 @@ func (t FileField) Dump() interface{} {
return ""
}
-func (t FileField) ToFormField(form *htmlwidgets.Form, data NestedMap,
- field *FieldConfig, locale string) {
- data.Set(field.Id, "")
- form.AddWidget(new(htmlwidgets.FileWidget), "Fields."+field.Id,
- field.Name.Get(locale), "")
-}
-
-func (t *FileField) FromFormField(data NestedMap, field *FieldConfig) {
- *t = FileField(data.Get(field.Id).(string))
-}
-
func (f FileField) FormData() interface{} {
- panic("Not implemented")
+ return ""
}
func (f *FileField) FromFormData(data interface{}) {
- panic("Not implemented")
+ *f = FileField(data.(string))
}
-func (f FileField) FormWidget() htmlwidgets.Widget {
- panic("Not implemented")
- return nil
+func (f FileField) FormWidget(locale string, field *FieldConfig) htmlwidgets.Widget {
+ return new(htmlwidgets.FileWidget)
}
type DateTimeFieldType int
@@ -721,29 +622,16 @@ func (t DateTimeField) Dump() interface{} {
return t.Time.UTC().Format(time.RFC3339)
}
-func (t DateTimeField) ToFormField(form *htmlwidgets.Form, data NestedMap,
- field *FieldConfig, locale string) {
- data.Set(field.Id, t.Time)
- form.AddWidget(&htmlwidgets.TimeWidget{Location: t.Location},
- "Fields."+field.Id, field.Name.Get(locale), "")
-}
-
-func (t *DateTimeField) FromFormField(data NestedMap, field *FieldConfig) {
- time := data.Get(field.Id).(time.Time)
- *t = DateTimeField{Time: time}
-}
-
func (f DateTimeField) FormData() interface{} {
- panic("Not implemented")
+ return f.Time
}
func (f *DateTimeField) FromFormData(data interface{}) {
- panic("Not implemented")
+ *f = DateTimeField{Time: data.(time.Time)}
}
-func (f DateTimeField) FormWidget() htmlwidgets.Widget {
- panic("Not implemented")
- return nil
+func (f DateTimeField) FormWidget(locale string, field *FieldConfig) htmlwidgets.Widget {
+ return &htmlwidgets.TimeWidget{Location: f.Location}
}
func initFields(fields map[string]Field, configs []*FieldConfig,
@@ -760,7 +648,8 @@ func initFields(fields map[string]Field, configs []*FieldConfig,
}
type ListFieldType struct {
- ElementType FieldType
+ ElementType FieldType
+ AddLabel, RemoveLabel i18n.LanguageMap
}
func (t ListFieldType) Field() Field {
@@ -769,7 +658,7 @@ func (t ListFieldType) Field() Field {
type ListField struct {
Fields []Field
- fieldType FieldType
+ fieldType *ListFieldType
monsti *MonstiClient
site string
}
@@ -797,7 +686,7 @@ func (f *ListField) Load(dataFnc func(interface{}) error) error {
if err := dataFnc(&data); err != nil {
return err
}
- elementType := f.fieldType.(*ListFieldType).ElementType
+ elementType := f.fieldType.ElementType
for _, msg := range data {
fieldDataFnc := func(in interface{}) error {
return json.Unmarshal(msg, in)
@@ -830,38 +719,21 @@ func (f ListField) FormData() interface{} {
func (f *ListField) FromFormData(data interface{}) {
dataList := data.([]interface{})
- if len(dataList) != len(f.Fields) {
- panic("Implement me!")
- }
- for idx, field := range f.Fields {
- field.FromFormData(dataList[idx])
- }
-}
-
-func (f ListField) FormWidget() htmlwidgets.Widget {
- panic("Not implemented")
- return nil
- /*
- return &htmlwidgets.ListWidget{
- InnerWidget: &htmlwidgets.TextWidget{},
+ for idx, data := range dataList {
+ if idx == len(f.Fields) {
+ f.Fields = append(f.Fields, f.fieldType.ElementType.Field())
}
- */
-}
-
-func (t ListField) ToFormField(form *htmlwidgets.Form, data NestedMap,
- field *FieldConfig, locale string) {
- /*
- data.Set(field.Id, t.Time)
- form.AddWidget(&htmlwidgets.TimeWidget{Location: t.Location},
- "Fields."+field.Id, field.Name.Get(locale), "")
- */
+ f.Fields[idx].FromFormData(data)
+ }
+ f.Fields = f.Fields[:len(dataList)]
}
-func (t *ListField) FromFormField(data NestedMap, field *FieldConfig) {
- /*
- time := data.Get(field.Id).(time.Time)
- *t = DateTimeField{Time: time}
- */
+func (f ListField) FormWidget(locale string, field *FieldConfig) htmlwidgets.Widget {
+ return &htmlwidgets.ListWidget{
+ InnerWidget: f.fieldType.ElementType.Field().FormWidget(locale, field),
+ AddLabel: f.fieldType.AddLabel.Get(locale),
+ RemoveLabel: f.fieldType.RemoveLabel.Get(locale),
+ }
}
type MapFieldType struct {
@@ -935,18 +807,11 @@ func (f *MapField) FromFormData(data interface{}) {
panic("Not implemented")
}
-func (f MapField) FormWidget() htmlwidgets.Widget {
+func (f MapField) FormWidget(locale string, field *FieldConfig) htmlwidgets.Widget {
panic("Not implemented")
return nil
}
-func (t MapField) ToFormField(form *htmlwidgets.Form, data NestedMap,
- field *FieldConfig, locale string) {
-}
-
-func (t *MapField) FromFormField(data NestedMap, field *FieldConfig) {
-}
-
type FieldType interface {
// Field returns a new field for the type.
Field() Field
diff --git a/api/service/monsti.go b/api/service/monsti.go
index 07ac0a89..a218918d 100644
--- a/api/service/monsti.go
+++ b/api/service/monsti.go
@@ -192,7 +192,7 @@ func (s *MonstiClient) LoadSiteSettings(site string) (*Settings, error) {
Hidden: true,
Type: &MapFieldType{
&ListFieldType{
- &CombinedFieldType{map[string]FieldConfig{
+ ElementType: &CombinedFieldType{map[string]FieldConfig{
"id": {Type: new(TextFieldType)}}}}},
},
}
@@ -312,7 +312,10 @@ func restoreFields(fields map[string]map[string]*json.RawMessage,
f := func(in interface{}) error {
return json.Unmarshal(*value, in)
}
- out[field.Id].Load(f)
+ if err := out[field.Id].Load(f); err != nil {
+ return err
+ }
+
}
}
return nil
@@ -572,13 +575,6 @@ func (r RequestFile) ReadFile() ([]byte, error) {
}
*/
-type RequestMethod uint
-
-const (
- GetRequest = iota
- PostRequest
-)
-
type Action uint
const (
@@ -604,13 +600,17 @@ type Request struct {
// The query values of the request URL.
Query url.Values
// Method of the request (GET,POST,...).
- Method RequestMethod
+ Method string
// User session
Session *UserSession
// Action to perform (e.g. "edit").
Action Action
- // FormData stores the requests form data.
- FormData url.Values
+ // Form stores the requests form data.
+ // See net/http's Request.Form
+ Form url.Values
+ // PostForm stores the requests POST/PUT form data.
+ // See net/http's Request.PostForm
+ PostForm url.Values
/*
// The requested node.
Node *Node
@@ -849,6 +849,13 @@ func (s *MonstiClient) WaitSignal() error {
enc := gob.NewEncoder(buffer)
err = enc.Encode(argWrap{ret})
if err != nil {
+ signalRet.Err = fmt.Sprintf("service: Could not encode signal return value: %v", err)
+ finishErr := s.RPCClient.Call("Monsti.FinishSignal", signalRet, new(int))
+ if finishErr != nil {
+ return fmt.Errorf(
+ "service: Monsti.FinishSignal error after signal encode error: %v",
+ finishErr)
+ }
return fmt.Errorf("service: Could not encode signal return value: %v", err)
}
}
diff --git a/api/service/signals.go b/api/service/signals.go
index 0c46a0ba..2ebc5731 100644
--- a/api/service/signals.go
+++ b/api/service/signals.go
@@ -19,22 +19,18 @@ package service
import (
"encoding/gob"
"fmt"
-)
-
-type NodeContextArgs struct {
- Request uint
- NodeType string
- EmbedNode *EmbedNode
-}
+ "html/template"
-type NodeContextRet struct {
- Context map[string][]byte
- Mods *CacheMods
-}
+ "github.com/chrneumann/htmlwidgets"
+)
func init() {
gob.RegisterName("monsti.NodeContextArgs", NodeContextArgs{})
gob.RegisterName("monsti.NodeContextRet", NodeContextRet{})
+ gob.RegisterName("monsti.RenderNodeArgs", RenderNodeArgs{})
+ gob.RegisterName("monsti.RenderNodeRet", RenderNodeRet{})
+ gob.Register(new(template.HTML))
+ gob.Register(new(htmlwidgets.RenderData))
}
// SignalHandler wraps a handler for a specific signal.
@@ -45,6 +41,17 @@ type SignalHandler interface {
Handle(args interface{}) (interface{}, error)
}
+type NodeContextArgs struct {
+ Request uint
+ NodeType string
+ EmbedNode *EmbedNode
+}
+
+type NodeContextRet struct {
+ Context map[string][]byte
+ Mods *CacheMods
+}
+
type nodeContextHandler struct {
f func(request uint, session *Session, nodeType string, embedNode *EmbedNode) (
map[string][]byte, *CacheMods, error)
@@ -69,9 +76,61 @@ func (r *nodeContextHandler) Handle(args interface{}) (interface{}, error) {
// NewNodeContextHandler consructs a signal handler that adds some
// template context for rendering a node.
+//
+// DEPRECATED: Use the more powerful RenderNode signal.
func NewNodeContextHandler(
sessions *SessionPool,
cb func(request uint, session *Session, nodeType string,
embedNode *EmbedNode) (map[string][]byte, *CacheMods, error)) SignalHandler {
return &nodeContextHandler{cb, sessions}
}
+
+type RenderNodeArgs struct {
+ Request uint
+ NodeType string
+ EmbedNode *EmbedNode
+}
+
+// Redirect configures a HTTP redirect with the given URL and HTTP status.
+type Redirect struct {
+ URL string
+ Status int
+}
+
+type RenderNodeRet struct {
+ Context map[string]interface{}
+ Redirect *Redirect
+ Mods *CacheMods
+}
+
+type renderNodeHandler struct {
+ f func(signal *RenderNodeArgs, session *Session) (*RenderNodeRet, error)
+ sessions *SessionPool
+}
+
+func (r *renderNodeHandler) Name() string {
+ return "monsti.RenderNode"
+}
+
+func (r *renderNodeHandler) Handle(args interface{}) (interface{}, error) {
+ session, err := r.sessions.New()
+ if err != nil {
+ return nil, fmt.Errorf("service: Could not get session: %v", err)
+ }
+ defer r.sessions.Free(session)
+ args_ := args.(RenderNodeArgs)
+ ret, err := r.f(&args_, session)
+ if ret == nil {
+ ret = new(RenderNodeRet)
+ }
+ return ret, err
+}
+
+// NewRenderNodeHandler consructs a signal handler that may add some
+// template context for rendering a node and issue redirects.
+func NewRenderNodeHandler(
+ sessions *SessionPool,
+ cb func(args *RenderNodeArgs, session *Session) (
+ *RenderNodeRet, error)) SignalHandler {
+ return &renderNodeHandler{cb, sessions}
+}
diff --git a/api/util/template/template.go b/api/util/template/template.go
index 27e1e235..bee52440 100644
--- a/api/util/template/template.go
+++ b/api/util/template/template.go
@@ -113,6 +113,9 @@ func (r Renderer) Render(name string, context interface{},
"GN": GN,
"GD": GD,
"GDN": GDN,
+ "Interface": func(in interface{}) interface{} {
+ return in
+ },
"RawHTML": func(in interface{}) template.HTML {
return template.HTML(fmt.Sprintf("%s", in))
},
diff --git a/core/monsti-base/monsti-base.go b/core/monsti-base/monsti-base.go
new file mode 100644
index 00000000..ba1bf46a
--- /dev/null
+++ b/core/monsti-base/monsti-base.go
@@ -0,0 +1,205 @@
+// This file is part of Monsti, a web content management system.
+// Copyright 2012-2015 Christian Neumann
+//
+// Monsti is free software: you can redistribute it and/or modify it under the
+// terms of the GNU Affero General Public License as published by the Free
+// Software Foundation, either version 3 of the License, or (at your option) any
+// later version.
+//
+// Monsti is distributed in the hope that it will be useful, but WITHOUT ANY
+// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+// A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
+// details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with Monsti. If not, see .
+
+package main
+
+import (
+ "fmt"
+ "net/http"
+
+ "path"
+ "github.com/chrneumann/htmlwidgets"
+ gomail "gopkg.in/gomail.v1"
+ "pkg.monsti.org/gettext"
+ "pkg.monsti.org/monsti/api/service"
+ "pkg.monsti.org/monsti/api/util/i18n"
+ "pkg.monsti.org/monsti/api/util/module"
+)
+
+var availableLocales = []string{"de", "en"}
+
+func setup(c *module.ModuleContext) error {
+ gettext.DefaultLocales.Domain = "monsti-daemon"
+ G := func(in string) string { return in }
+ m := c.Session.Monsti()
+
+ nodeType := &service.NodeType{
+ Id: "core.ContactForm",
+ AddableTo: []string{"."},
+ Name: i18n.GenLanguageMap(G("Contact form"), availableLocales),
+ Fields: append(service.CoreFields, []*service.FieldConfig{
+ {
+ Id: "core.ContactFormFields",
+ Hidden: true,
+ Type: &service.ListFieldType{
+ ElementType: &service.CombinedFieldType{map[string]service.FieldConfig{
+ "Name": {Type: new(service.TextFieldType)},
+ "Required": {Type: new(service.BoolFieldType)},
+ "Field": {Type: &service.DynamicTypeFieldType{
+ Fields: []service.FieldConfig{
+ {
+ Id: "text",
+ Type: new(service.DummyFieldType),
+ },
+ {
+ Id: "textarea",
+ Type: new(service.DummyFieldType),
+ },
+ }}}}}}}}...)}
+ if err := m.RegisterNodeType(nodeType); err != nil {
+ c.Logger.Fatalf("Could not register %q node type: %v", nodeType.Id, err)
+ }
+
+ handler := service.NewRenderNodeHandler(c.Sessions,
+ func(args *service.RenderNodeArgs, session *service.Session) (
+ *service.RenderNodeRet, error) {
+ if args.NodeType == "core.ContactForm" {
+ req, err := session.Monsti().GetRequest(args.Request)
+ if err != nil || req == nil {
+ return nil, fmt.Errorf("Could not get request: %v", err)
+ }
+ return renderContactForm(req, session)
+ }
+ return nil, nil
+ })
+ if err := m.AddSignalHandler(handler); err != nil {
+ c.Logger.Fatalf("Could not add signal handler: %v", err)
+ }
+
+ return nil
+}
+
+type dataField struct {
+ Id, Name string
+}
+
+func renderContactForm(req *service.Request, session *service.Session) (
+ *service.RenderNodeRet, error) {
+
+ m := session.Monsti()
+ siteSettings, err := m.LoadSiteSettings(req.Site)
+ if err != nil {
+ return nil, fmt.Errorf("Could not get site settings: %v", err)
+ }
+ G, _, _, _ := gettext.DefaultLocales.Use("",
+ siteSettings.Fields["core.Locale"].Value().(string))
+
+ node, err := m.GetNode(req.Site, req.NodePath)
+ if err != nil {
+ return nil, fmt.Errorf("Could not get contact form node: %v", err)
+ }
+
+ data := make(service.NestedMap)
+ var dataFields []dataField
+ form := htmlwidgets.NewForm(data)
+
+ formFields := node.Fields["core.ContactFormFields"].(*service.ListField)
+
+ for i, field := range formFields.Fields {
+ combinedField := field.(*service.CombinedField)
+ name := combinedField.Fields["Name"].Value().(string)
+ required := combinedField.Fields["Required"].Value().(bool)
+ fieldId := fmt.Sprintf("field_%d", i)
+ dataFields = append(dataFields, dataField{fieldId, name})
+ data[fieldId] = ""
+ innerFieldType := combinedField.Fields["Field"].(*service.DynamicTypeField).DynamicType
+ var widget htmlwidgets.Widget
+ switch innerFieldType {
+ case "text":
+ textWidget := &htmlwidgets.TextWidget{ValidationError: G("Required.")}
+ if required {
+ textWidget.MinLength = 1
+ }
+ widget = textWidget
+ case "textarea":
+ areaWidget := &htmlwidgets.TextAreaWidget{ValidationError: G("Required.")}
+ if required {
+ areaWidget.MinLength = 1
+ }
+ widget = areaWidget
+ default:
+ panic(fmt.Sprintf("Unknow inner field type <%v>", innerFieldType))
+ }
+ form.AddWidget(widget, fieldId, name, "")
+ }
+
+ if len(formFields.Fields) == 0 {
+ // Add default fields for backward compatibility
+ data["Name"] = ""
+ data["Email"] = ""
+ data["Subject"] = ""
+ data["Message"] = ""
+ dataFields = []dataField{
+ {"Name", G("Name")},
+ {"Email", G("Email")},
+ {"Subject", G("Subject")},
+ {"Message", G("Message")},
+ }
+ form.AddWidget(&htmlwidgets.TextWidget{MinLength: 1,
+ ValidationError: G("Required.")}, "Name", G("Name"), "")
+ form.AddWidget(&htmlwidgets.TextWidget{MinLength: 1,
+ ValidationError: G("Required.")}, "Email", G("Email"), "")
+ form.AddWidget(&htmlwidgets.TextWidget{MinLength: 1,
+ ValidationError: G("Required.")}, "Subject", G("Subject"), "")
+ form.AddWidget(&htmlwidgets.TextAreaWidget{MinLength: 1,
+ ValidationError: G("Required.")}, "Message", G("Message"), "")
+ }
+
+ context := make(map[string]interface{})
+ switch req.Method {
+ case "GET":
+ if _, submitted := req.Form["submitted"]; submitted {
+ context["Submitted"] = 1
+ }
+ case "POST":
+ if form.Fill(req.PostForm) {
+ mail := gomail.NewMessage()
+ mail.SetAddressHeader("From",
+ siteSettings.StringValue("core.EmailAddress"),
+ siteSettings.StringValue("core.EmailName"))
+ mail.SetAddressHeader("To",
+ siteSettings.StringValue("core.OwnerEmail"),
+ siteSettings.StringValue("core.OwnerName"))
+ // mail.SetAddressHeader("Reply-To", data.Email, data.Name)
+ mail.SetHeader("Subject", "Contact form submission")
+ var fieldValues string
+ for _, v := range dataFields {
+ fieldValues += fmt.Sprintf("%v: %v\n", v.Name, data[v.Id])
+ }
+ body := fmt.Sprintf("%v\n\n%v",
+ fmt.Sprintf(G("Received from contact form at %v"),
+ siteSettings.StringValue("core.Title")), fieldValues)
+ mail.SetBody("text/plain", body)
+ mailer := gomail.NewCustomMailer("", nil, gomail.SetSendMail(
+ m.SendMailFunc()))
+ err := mailer.Send(mail)
+ if err != nil {
+ return nil, fmt.Errorf("Could not send mail: %v", err)
+ }
+ return &service.RenderNodeRet{
+ Redirect: &service.Redirect{
+ path.Dir(node.Path) + "/?submitted", http.StatusSeeOther}}, nil
+ }
+ default:
+ return nil, fmt.Errorf("Request method not supported: %v", req.Method)
+ }
+ context["Form"] = form.RenderData()
+ return &service.RenderNodeRet{Context: context}, err
+}
+
+func main() {
+ module.StartModule("base", setup)
+}
diff --git a/core/monsti-daemon/daemon.go b/core/monsti-daemon/daemon.go
index 8db00796..3067d79d 100644
--- a/core/monsti-daemon/daemon.go
+++ b/core/monsti-daemon/daemon.go
@@ -40,7 +40,7 @@ import (
"pkg.monsti.org/monsti/api/util/template"
)
-const monstiVersion = "0.12.0"
+const monstiVersion = "0.13.0"
// Settings for the application and the sites.
type settings struct {
@@ -155,10 +155,11 @@ func main() {
// Start modules
monsti.moduleInit = make(map[string]chan bool)
- for _, module := range settings.Modules {
+ modules := append([]string{"base"}, settings.Modules...)
+ for _, module := range modules {
monsti.moduleInit[module] = make(chan bool, 1)
}
- for _, module := range settings.Modules {
+ for _, module := range modules {
executable := "monsti-" + module
cmd := exec.Command(executable, cfgPath)
cmd.Stderr = moduleLog{module, logger}
@@ -169,7 +170,7 @@ func main() {
}(module)
}
logger.Println("Waiting for modules to finish initialization...")
- for _, module := range settings.Modules {
+ for _, module := range modules {
logger.Printf("Waiting for %q...", module)
<-monsti.moduleInit[module]
}
diff --git a/core/monsti-daemon/node.go b/core/monsti-daemon/node.go
index 2a82ae4b..63b80d25 100644
--- a/core/monsti-daemon/node.go
+++ b/core/monsti-daemon/node.go
@@ -351,6 +351,12 @@ func (h *nodeHandler) viewImage(c *reqContext) error {
return nil
}
+type errRedirect service.Redirect
+
+func (e errRedirect) Error() string {
+ return "Redirect"
+}
+
// ViewNode handles node views.
func (h *nodeHandler) View(c *reqContext) error {
// Redirect if trailing slash is missing and if this is not a file
@@ -392,6 +398,10 @@ func (h *nodeHandler) View(c *reqContext) error {
if rendered == nil {
rendered, mods, err = h.RenderNode(c, nil)
if err != nil {
+ if e, ok := err.(errRedirect); ok {
+ http.Redirect(c.Res, c.Req, e.URL, e.Status)
+ return nil
+ }
return fmt.Errorf("Could not render node: %v", err)
}
if c.UserSession.User == nil && len(c.Req.Form) == 0 {
@@ -448,6 +458,23 @@ func (h *nodeHandler) RenderNode(c *reqContext, embedNode *service.EmbedNode) (
}
}
context := make(mtemplate.Context)
+
+ var renderNodeRet []service.RenderNodeRet
+ err := c.Serv.Monsti().EmitSignal("monsti.RenderNode",
+ service.RenderNodeArgs{c.Id, reqNode.Type.Id, embedNode}, &renderNodeRet)
+ if err != nil {
+ return nil, nil, fmt.Errorf("Could not emit signal: %v", err)
+ }
+ for i, _ := range renderNodeRet {
+ if renderNodeRet[i].Redirect != nil {
+ return nil, nil, errRedirect(*renderNodeRet[i].Redirect)
+ }
+ mods.Join(renderNodeRet[i].Mods)
+ for key, value := range renderNodeRet[i].Context {
+ context[key] = value
+ }
+ }
+
context["Embed"] = make(map[string]template.HTML)
// Embed nodes
embedNodes := append(reqNode.Type.Embed, reqNode.Embed...)
@@ -461,24 +488,18 @@ func (h *nodeHandler) RenderNode(c *reqContext, embedNode *service.EmbedNode) (
template.HTML(rendered)
}
context["Node"] = reqNode
- switch reqNode.Type.Id {
- case "core.ContactForm":
- if err := renderContactForm(c, context, c.Req.Form, h); err != nil {
- return nil, nil, fmt.Errorf("Could not render contact form: %v", err)
- }
- }
context["Embedded"] = embedNode != nil
- var ret []service.NodeContextRet
- err := c.Serv.Monsti().EmitSignal("monsti.NodeContext",
- service.NodeContextArgs{c.Id, reqNode.Type.Id, embedNode}, &ret)
+ var nodeContextRet []service.NodeContextRet
+ err = c.Serv.Monsti().EmitSignal("monsti.NodeContext",
+ service.NodeContextArgs{c.Id, reqNode.Type.Id, embedNode}, &nodeContextRet)
if err != nil {
return nil, nil, fmt.Errorf("Could not emit signal: %v", err)
}
- for i, _ := range ret {
- mods.Join(ret[i].Mods)
- for key, value := range ret[i].Context {
- context[key] = template.HTML(value)
+ for i, _ := range nodeContextRet {
+ mods.Join(nodeContextRet[i].Mods)
+ for key, value := range nodeContextRet[i].Context {
+ context[key] = template.HTML(string(value))
}
}
@@ -577,8 +598,11 @@ func (h *nodeHandler) Edit(c *reqContext) error {
if field.Hidden {
continue
}
- formData.Node.Fields[field.Id].ToFormField(form, formData.Fields,
- field, c.UserSession.Locale)
+ formData.Fields.Set(field.Id, formData.Node.Fields[field.Id].FormData())
+ widget := formData.Node.Fields[field.Id].FormWidget(
+ c.UserSession.Locale, field)
+ form.AddWidget(widget, "Fields."+field.Id,
+ field.Name.Get(c.UserSession.Locale), "")
if _, ok := field.Type.(*service.FileFieldType); ok {
fileFields = append(fileFields, field.Id)
}
@@ -639,7 +663,7 @@ func (h *nodeHandler) Edit(c *reqContext) error {
}
for _, field := range nodeType.Fields {
if !field.Hidden {
- node.Fields[field.Id].FromFormField(formData.Fields, field)
+ node.Fields[field.Id].FromFormData(formData.Fields.Get(field.Id))
}
}
err := c.Serv.Monsti().WriteNode(c.Site, node.Path, &node)
diff --git a/core/monsti-daemon/nodetypes.go b/core/monsti-daemon/nodetypes.go
index 0bc44951..b1ad8649 100644
--- a/core/monsti-daemon/nodetypes.go
+++ b/core/monsti-daemon/nodetypes.go
@@ -19,19 +19,11 @@ package main
import (
"fmt"
"log"
- "net/http"
- "net/url"
-
- "path"
- "github.com/chrneumann/htmlwidgets"
- gomail "gopkg.in/gomail.v1"
- "pkg.monsti.org/gettext"
"pkg.monsti.org/monsti/api/util/i18n"
- "pkg.monsti.org/monsti/api/util/template"
)
import "pkg.monsti.org/monsti/api/service"
-var availableLocales = []string{"en", "de"}
+var availableLocales = []string{"en", "de", "nl"}
func initNodeTypes(settings *settings, session *service.Session, logger *log.Logger) error {
G := func(in string) string { return in }
@@ -109,72 +101,5 @@ func initNodeTypes(settings *settings, session *service.Session, logger *log.Log
return fmt.Errorf("Could not register image node type: %v", err)
}
- contactFormType := service.NodeType{
- Id: "core.ContactForm",
- AddableTo: []string{"."},
- Name: i18n.GenLanguageMap(G("Contact form"), availableLocales),
- Fields: service.CoreFields,
- }
- if err := session.Monsti().RegisterNodeType(&contactFormType); err != nil {
- return fmt.Errorf("Could not register contactform node type: %v", err)
- }
- return nil
-}
-
-type contactFormData struct {
- Name, Email, Subject, Message string
-}
-
-func renderContactForm(c *reqContext, context template.Context,
- formValues url.Values, h *nodeHandler) error {
- G, _, _, _ := gettext.DefaultLocales.Use("",
- c.SiteSettings.Fields["core.Locale"].Value().(string))
- m := c.Serv.Monsti()
- data := contactFormData{}
- form := htmlwidgets.NewForm(&data)
- form.AddWidget(&htmlwidgets.TextWidget{MinLength: 1,
- ValidationError: G("Required.")}, "Name", G("Name"), "")
- form.AddWidget(&htmlwidgets.TextWidget{MinLength: 1,
- ValidationError: G("Required.")}, "Email", G("Email"), "")
- form.AddWidget(&htmlwidgets.TextWidget{MinLength: 1,
- ValidationError: G("Required.")}, "Subject", G("Subject"), "")
- form.AddWidget(&htmlwidgets.TextAreaWidget{MinLength: 1,
- ValidationError: G("Required.")}, "Message", G("Message"), "")
-
- switch c.Req.Method {
- case "GET":
- if _, submitted := formValues["submitted"]; submitted {
- context["Submitted"] = 1
- }
- case "POST":
- if form.Fill(formValues) {
- mail := gomail.NewMessage()
- mail.SetAddressHeader("From",
- c.SiteSettings.StringValue("core.EmailAddress"),
- c.SiteSettings.StringValue("core.EmailName"))
- mail.SetAddressHeader("To",
- c.SiteSettings.StringValue("core.OwnerEmail"),
- c.SiteSettings.StringValue("core.OwnerName"))
- mail.SetAddressHeader("Reply-To", data.Email, data.Name)
- mail.SetHeader("Subject", data.Subject)
- body := fmt.Sprintf("%v\n%v\n\n%v",
- fmt.Sprintf(G("Received from contact form at %v"),
- c.SiteSettings.StringValue("core.Title")),
- fmt.Sprintf(G("Name: %v | Email: %v"), data.Name, data.Email),
- data.Message)
- mail.SetBody("text/plain", body)
- mailer := gomail.NewCustomMailer("", nil, gomail.SetSendMail(
- m.SendMailFunc()))
- err := mailer.Send(mail)
- if err != nil {
- return fmt.Errorf("Could not send mail: %v", err)
- }
- http.Redirect(c.Res, c.Req, path.Dir(c.Node.Path)+"/?submitted", http.StatusSeeOther)
- return nil
- }
- default:
- return fmt.Errorf("Request method not supported: %v", c.Req.Method)
- }
- context["Form"] = form.RenderData()
return nil
}
diff --git a/core/monsti-daemon/render.go b/core/monsti-daemon/render.go
index a688ded0..eb0f6f94 100644
--- a/core/monsti-daemon/render.go
+++ b/core/monsti-daemon/render.go
@@ -75,7 +75,7 @@ func renderInMaster(r template.Renderer, content []byte, env masterTmplEnv,
"Session": env.Session}, userLocale,
settings.Monsti.GetSiteTemplatesPath(site))
if err != nil {
- panic("Can't render: " + err.Error())
+ panic("Can't render admin template: " + err.Error())
}
return ret, nil
}
@@ -146,7 +146,9 @@ func renderInMaster(r template.Renderer, content []byte, env masterTmplEnv,
"Session": env.Session}, userLocale,
settings.Monsti.GetSiteTemplatesPath(site))
if err != nil {
- panic("Can't render: " + err.Error())
+ panic(fmt.Sprintf(
+ "Can't render master template for site %v: %v",
+ site, err))
}
return ret, mods
}
diff --git a/core/monsti-daemon/serve.go b/core/monsti-daemon/serve.go
index e3b675c4..d9cc0c21 100644
--- a/core/monsti-daemon/serve.go
+++ b/core/monsti-daemon/serve.go
@@ -74,10 +74,11 @@ func (n *nodeHandler) GetRequest(id uint) *service.Request {
NodePath: req.Node.Path,
Site: req.Site,
Query: req.Req.URL.Query(),
- // TODO add method
+ Method: req.Req.Method,
Session: req.UserSession,
Action: req.Action,
- FormData: req.Req.PostForm,
+ Form: req.Req.Form,
+ PostForm: req.Req.PostForm,
/*
Node: req.Node,
*/
diff --git a/core/monsti-daemon/service.go b/core/monsti-daemon/service.go
index 0feaaad5..2f4820bd 100644
--- a/core/monsti-daemon/service.go
+++ b/core/monsti-daemon/service.go
@@ -81,7 +81,7 @@ func (i *MonstiService) InitSite(host *string, reply *bool) error {
}
if strings.Trim(string(version), " ") != monstiVersion {
return fmt.Errorf("Wrong database version for %v: %v, expected %v",
- *host, version, monstiVersion)
+ *host, string(version), monstiVersion)
}
i.siteMutexes[*host] = new(sync.RWMutex)
*reply = true
@@ -182,6 +182,7 @@ func (m *MonstiService) EmitSignal(args *Receive, ret *[][]byte) error {
m.subscriber[id] <- &signal{args.Name, args.Args, retChan}
emitRet := <-retChan
if len(emitRet.Error) > 0 {
+ done = true
return fmt.Errorf("Received error as signal response: %v", emitRet.Error)
}
(*ret)[i] = emitRet.Ret
diff --git a/core/monsti-daemon/session.go b/core/monsti-daemon/session.go
index 0b33c28a..d09057e0 100644
--- a/core/monsti-daemon/session.go
+++ b/core/monsti-daemon/session.go
@@ -71,6 +71,7 @@ func (h *nodeHandler) Login(c *reqContext) error {
return fmt.Errorf("Request method not supported: %v", c.Req.Method)
}
data.Password = ""
+
body, err := h.Renderer.Render("actions/loginform", template.Context{
"Form": form.RenderData()}, c.UserSession.Locale,
h.Settings.Monsti.GetSiteTemplatesPath(c.Site))
diff --git a/core/monsti-daemon/settings.go b/core/monsti-daemon/settings.go
index ea6c9189..aa8b7834 100644
--- a/core/monsti-daemon/settings.go
+++ b/core/monsti-daemon/settings.go
@@ -49,8 +49,11 @@ func (h *nodeHandler) SettingsAction(c *reqContext) error {
if field.Hidden {
continue
}
- settings.Fields[field.Id].ToFormField(form, formData.Fields,
- field, c.UserSession.Locale)
+ formData.Fields.Set(field.Id, settings.Fields[field.Id].FormData())
+ widget := settings.Fields[field.Id].FormWidget(
+ c.UserSession.Locale, field)
+ form.AddWidget(widget, "Fields."+field.Id,
+ field.Name.Get(c.UserSession.Locale), "")
}
switch c.Req.Method {
@@ -59,7 +62,7 @@ func (h *nodeHandler) SettingsAction(c *reqContext) error {
if form.Fill(c.Req.Form) {
for _, field := range settings.FieldConfigs {
if !field.Hidden {
- settings.Fields[field.Id].FromFormField(formData.Fields, field)
+ settings.Fields[field.Id].FromFormData(formData.Fields.Get(field.Id))
}
}
if err := m.WriteSiteSettings(c.Site, settings); err != nil {
diff --git a/doc/manual.adoc b/doc/manual.adoc
index 3d51bae6..e9536f47 100644
--- a/doc/manual.adoc
+++ b/doc/manual.adoc
@@ -89,6 +89,14 @@ You will need following tools to build the documentation:
- http://www.gnu.org/software/src-highlite/[source-highlight]
+== Typical usage
+
+- Copy the example site's data to a new directory in the data
+ directory and name it matching the hostname of the new site.
+- Alter the templates according to you needs.
+- Create a new module to implement new functionality and custom node
+ types.
+
== Architecture [[sec-architecture]]
Monsti's master daemon provides the core functionality of Monsti. It
@@ -149,6 +157,16 @@ Map fields map string keys to fields of a configurable type.
=== Core Node Types
+==== core.ContactForm
+
+The contact form node type allows to realize contact forms with custom
+fields on your website. Currently, fields can only be configured
+directly in the node's json file. Have a look at the contact form in
+the example.
+
+For backward compatibility, contact forms without configured form
+fields will be provided with some default fields.
+
==== core.Path
The Path node type is not written to the database. It will be returned
@@ -243,6 +261,15 @@ Have a look at the documented example module
`monsti-example-module`. It shows how to setup a module and call
Monsti's API, including use of signals.
+=== Signals
+
+Monsti includes a signal mechanism to alter functionality. For
+example, when a node gets visited and rendered, the `RenderNode`
+signal will be called which can be used for various tasks as to add
+content, process form submits, or redirect to another node. Have a
+look at the example how to use signals and refer to the service API
+documentation for a list of available signals.
+
== Configuration
=== `monsti.yaml`
diff --git a/doc/manual.html b/doc/manual.html
index 6348a836..bb3521e8 100644
--- a/doc/manual.html
+++ b/doc/manual.html
@@ -845,6 +845,30 @@
Documentation
+
Typical usage
+
+
+-
+
+Copy the example site’s data to a new directory in the data
+ directory and name it matching the hostname of the new site.
+
+
+-
+
+Alter the templates according to you needs.
+
+
+-
+
+Create a new module to implement new functionality and custom node
+ types.
+
+
+
+
+
+
Architecture
Monsti’s master daemon provides the core functionality of Monsti. It
@@ -910,6 +934,15 @@
Node types
Core Node Types
+
+
The contact form node type allows to realize contact forms with custom
+fields on your website. Currently, fields can only be configured
+directly in the node’s json file. Have a look at the contact form in
+the example.
+
For backward compatibility, contact forms without configured form
+fields will be provided with some default fields.
+
+
core.Path
The Path node type is not written to the database. It will be returned
by monsti.GetChildren
and monsti.GetNode
to represent a
@@ -1116,6 +1149,15 @@
Writing modules
monsti-example-module
. It shows how to setup a module and call
Monsti’s API, including use of signals.
+
+
Signals
+
Monsti includes a signal mechanism to alter functionality. For
+example, when a node gets visited and rendered, the RenderNode
+signal will be called which can be used for various tasks as to add
+content, process form submits, or redirect to another node. Have a
+look at the example how to use signals and refer to the service API
+documentation for a list of available signals.
+
@@ -1218,7 +1260,7 @@
Include Files
+
+
+
+
Notable Changes
+
+
+
New base module
+
A new module named monsti-base
was added that implements base
+functionality like core node types. The module will always be
+automatically started.
+
+
+
+
Fields of the contact form may be configured using the
+core.ContactFormFields
field. Have a look at the example.
+
+
+
New signal RenderNode
+
This signal was added to replace the NodeContext
signal. It’s more
+powerful, e.g. it allows for redirects.
+
+
+
GUI for list fields
+
A GUI has been added for editing list fields.
+
+
+
+
+
Upgrade from 0.12.0
+
+
Modules and sites should be able to run and compile without changes.
+
+
Deprecation of NodeContext signal
+
Please use the RenderNode instead of the NodeContext signal.
+
+
+
service.Request struct changes
+
The Form
member now contains not only PUT/POST form values, but also
+URL parameters. The PostForm
contains only the PUT/POST form
+values.
+
+
+
service.Field struct changes
+
The ToFormField and FromFormField methods have been replaced by the
+Form* methods.
+
+
+
+
+
+
+
diff --git a/doc/release_notes.adoc b/doc/release_notes.adoc
new file mode 100644
index 00000000..f29ff1a2
--- /dev/null
+++ b/doc/release_notes.adoc
@@ -0,0 +1,47 @@
+= Monsti 0.13.0 Release Notes
+:imagesdir: static/img
+:data-uri:
+:icons:
+:toc:
+:homepage: http://www.monsti.org
+
+== Notable Changes
+
+=== New base module
+
+A new module named `monsti-base` was added that implements base
+functionality like core node types. The module will always be
+automatically started.
+
+=== Configurable contact form fields
+
+Fields of the contact form may be configured using the
+`core.ContactFormFields` field. Have a look at the example.
+
+=== New signal `RenderNode`
+
+This signal was added to replace the `NodeContext` signal. It's more
+powerful, e.g. it allows for redirects.
+
+=== GUI for list fields
+
+A GUI has been added for editing list fields.
+
+== Upgrade from 0.12.0
+
+Modules and sites should be able to run and compile without changes.
+
+=== Deprecation of NodeContext signal
+
+Please use the RenderNode instead of the NodeContext signal.
+
+=== service.Request struct changes
+
+The `Form` member now contains not only PUT/POST form values, but also
+URL parameters. The `PostForm` contains only the PUT/POST form
+values.
+
+=== service.Field struct changes
+
+The ToFormField and FromFormField methods have been replaced by the
+Form* methods.
\ No newline at end of file
diff --git a/doc/release_notes.html b/doc/release_notes.html
new file mode 100644
index 00000000..aa810dc1
--- /dev/null
+++ b/doc/release_notes.html
@@ -0,0 +1,798 @@
+
+
+