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

+

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 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

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 @@ + + + + + +Monsti 0.13.0 Release Notes + + + + + +
+
+

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.

+
+
+
+
+

+ + + diff --git a/example/data/localhost/nodes/types/contact-form/node.json b/example/data/localhost/nodes/types/contact-form/node.json index bc28706d..c1e06e31 100644 --- a/example/data/localhost/nodes/types/contact-form/node.json +++ b/example/data/localhost/nodes/types/contact-form/node.json @@ -3,13 +3,46 @@ "Hide": false, "Embed": null, "Public": true, - "PublishTime": "2014-11-12T15:14:31.198106103+01:00", - "Changed": "2014-11-12T15:14:31.198106133+01:00", + "PublishTime": "2014-11-12T15:14:00+01:00", + "Changed": "2015-06-04T06:51:24.396971265Z", "Type": "core.ContactForm", "Fields": { "core": { - "Body": "A contact form! Possibly non functional, as it requires a configured SMTP daemon.\u003cbr style=\"\" class=\"aloha-cleanme\"\u003e", - "Title": "Contact Form" + "Body": "\u003cp\u003eA contact form! Possibly non functional, as it requires a configured SMTP daemon.\u003c/p\u003e\r\n", + "ContactFormFields": null, + "Description": "", + "Thumbnail": "", + "Title": "Contact Form", + "ContactFormFields": [ + { + "Name": "Name", + "Required": true, + "Field": { + "Type": "text" + } + }, + { + "Name": "Email", + "Required": true, + "Field": { + "Type": "text" + } + }, + { + "Name": "Subject", + "Required": true, + "Field": { + "Type": "text" + } + }, + { + "Name": "Message", + "Required": true, + "Field": { + "Type": "textarea" + } + } + ] } } } diff --git a/example/data/localhost/scss/config.rb b/example/data/localhost/scss/config.rb deleted file mode 100644 index 1ba486ad..00000000 --- a/example/data/localhost/scss/config.rb +++ /dev/null @@ -1,23 +0,0 @@ -######### -# 1. Set this to the root of your project when deployed: -http_path = "/" - -# 2. probably don't need to touch these -css_dir = "../site-static/css" -sass_dir = "./" -images_dir = "../site-static/img" -javascripts_dir = "../site-static/js" -environment = :development -relative_assets = true - -# 3. You can select your preferred output style here (can be overridden via the command line): -#output_style = :expanded - -# 4. When you are ready to launch your WP theme comment out (3) and uncomment the line below -output_style = :compressed - -# To disable debugging comments that display the original location of your selectors. Uncomment: -# line_comments = false - -# don't touch this -preferred_syntax = :scss diff --git a/example/data/localhost/scss/site.scss b/example/data/localhost/scss/site.scss deleted file mode 100644 index 3dd5f29b..00000000 --- a/example/data/localhost/scss/site.scss +++ /dev/null @@ -1,69 +0,0 @@ -@import "compass"; - -html { - font-family: sans-serif; -} - -#site-wrap { - margin: 0 auto; - max-width: 800px; - padding: 0 20px; -} - -#bottom-wrap { - margin-top:3em; -} - -#sidebar { - margin-top:2em; -} - -#header { - margin-top:3em; -} - - -#site-title a { - display:block; - width:301px; - height:71px; - text-indent: -999999em; - background:url("/static/img/logo.png"); -} - -#primary-nav { - ul { - background:#eee; - border:thin solid #777; - border-radius:5px; - @include box-shadow(0 0 2px 0 rgba(0,0,0,0.5)); - li { - &.active-below.child, &.active { - background:#ddd; - } - } - @include border-radius(5px); - overflow: hidden; - } - a { - color: #555; - border-right:thin solid gray; - border-left:thin solid white; - text-decoration: none; - &:hover { - text-decoration: underline; - } - } -} - -h1, h2, h3, h4 { - color: #444; -} - -strong, b { - color: #444; -} - -a { - color: #DD8403; -} diff --git a/example/data/localhost/site-static/css/site.css b/example/data/localhost/site-static/css/site.css deleted file mode 100644 index e69de29b..00000000 diff --git a/example/data/localhost/version b/example/data/localhost/version index d33c3a21..51de3305 100644 --- a/example/data/localhost/version +++ b/example/data/localhost/version @@ -1 +1 @@ -0.12.0 \ No newline at end of file +0.13.0 \ No newline at end of file diff --git a/example/monsti-example-module/monsti-example-module.go b/example/monsti-example-module/monsti-example-module.go index 49073da2..81e1cfbb 100644 --- a/example/monsti-example-module/monsti-example-module.go +++ b/example/monsti-example-module/monsti-example-module.go @@ -24,7 +24,7 @@ import ( "pkg.monsti.org/monsti/api/util/module" ) -var availableLocales = []string{"de", "en"} +var availableLocales = []string{"de", "en", "nl"} func setup(c *module.ModuleContext) error { G := func(in string) string { return in } @@ -106,16 +106,22 @@ func setup(c *module.ModuleContext) error { { Id: "example.TextList", Name: i18n.GenLanguageMap(G("Text list"), availableLocales), - Type: &service.ListFieldType{new(service.TextFieldType)}, + Type: &service.ListFieldType{ + ElementType: new(service.TextFieldType), + AddLabel: i18n.GenLanguageMap(G("Add text entry"), availableLocales), + RemoveLabel: i18n.GenLanguageMap(G("Remove entry"), availableLocales), + }, }, { - Id: "example.Map", - Name: i18n.GenLanguageMap(G("Map"), availableLocales), - Type: &service.MapFieldType{new(service.TextFieldType)}, + Id: "example.Map", + Hidden: true, + Name: i18n.GenLanguageMap(G("Map"), availableLocales), + Type: &service.MapFieldType{new(service.TextFieldType)}, }, { - Id: "example.Combined", - Name: i18n.GenLanguageMap(G("Combined"), availableLocales), + Id: "example.Combined", + Hidden: true, + Name: i18n.GenLanguageMap(G("Combined"), availableLocales), Type: &service.CombinedFieldType{ map[string]service.FieldConfig{ "Text": { diff --git a/example/start.sh b/example/start.sh index d21d0969..0385a5ea 100755 --- a/example/start.sh +++ b/example/start.sh @@ -1,4 +1,4 @@ #!/bin/sh # Might help: killall -r monsti- -PATH=$PATH:../go/bin monsti-daemon config +PATH=$PATH:../go/bin exec monsti-daemon config diff --git a/scss/_base.scss b/scss/_base.scss index 7570b152..134e5df4 100644 --- a/scss/_base.scss +++ b/scss/_base.scss @@ -1,8 +1,156 @@ @import "compass"; @import "htmlarea"; +@mixin breakpoint($point) { + @if $point == large { + /* Large desktop */ + @media (min-width: 1200px) { @content; } + } + @else if $point == medium { + /* Portrait tablet to landscape and desktop */ + @media (min-width: 768px) { @content; } + } + @else if $point == small { + /* Landscape phone to portrait tablet */ + @media (min-width: 480px) { @content; } + } + /* At least 320px */ +} + +* { + box-sizing: border-box; +} + $primary-color: #274661; $secondary-color: #F89B16; $border-color: #CCC; $widget-background: #FAFAFA; -$widget-hover-background: #EEE; \ No newline at end of file +$widget-hover-background: #EEE; + +html { + font: 16px/23.3667px arial, sans-serif; + background: desaturate(lighten($primary-color, 70), 30); + position: relative; +} + +html, body { + height: 100%; +} + +body { + padding:0; + margin:0; + color: #666; +} + +fieldset { + border:0; + padding:0; + margin:0; +} + +form { + .field { + margin: 15px 0 10px 0; + label { + color: $primary-color; + } + } + .help { + display: block; + font-size: 80%; + } + .errors { + padding: 0; + li { + list-style-type: none; + color: #AA0000; + } + } +} + +input[type=text], input[type=password], input[type=datetime-local], select, +textarea, button, .button { + @include border-radius; + border: 1px solid $primary-color; + background: rgba($secondary-color, 0.05); + padding: 5px; + color: black; + width: 100%; + margin: 5px 0; +} + +button { + background: $primary-color; + width: auto; + color: white; + padding: 5px 15px; + &:hover { + background: darken($primary-color, 10); + text-decoration: none; + } +} + +textarea { + height: 150px; +} + +h1, h2, h3, h4, h5 { + color: $primary-color; + font-weight: bold; +} + +h1, h2, h3, h4 { + margin: 20px 0 10px; +} + +h1 { + font-size: 120%; +} + +h2 { + font-size: 110%; + font-weight: normal; +} + +h3 { + font-size: 105%; +} + +h4 { + font-size: 102%; +} + +p { + margin: 10px 0; +} + +strong, b { + font-weight: bold; + color:#444; +} + +a { + color:#DD8403 +} + +.main > article { + padding-top: 5px; + &>h1, .page-title { + font-size: 130%; + border-bottom: 1px solid $border-color; + padding-bottom: 10px; + } +} + +table { + margin: 20px 0; + th { + font-weight: bold; + background: #FAFAFA; + } + th, td { + padding: 3px 10px; + } + margin-left: -10px; +} \ No newline at end of file diff --git a/scss/_layout.scss b/scss/_layout.scss index 8aa38b74..8f461369 100644 --- a/scss/_layout.scss +++ b/scss/_layout.scss @@ -1,183 +1,3 @@ @import "compass/reset"; @import "base"; -html { - font: 16px/23.3667px arial, sans-serif; - background: desaturate(lighten($primary-color, 70), 30); - position: relative; -} - -html, body { - height: 100%; -} - -body { - padding:0; - margin:0; - color: #666; -} - -.site-wrap { - box-sizing: border-box; - max-width: 1200px; - min-width: 900px; - padding: 0 20px; - margin: 0 auto; - &> article { - padding: 70px 0 30px 0; - } -} - -.main, .sidebar, .footer { - background: white; - border: 1px solid $border-color; - @include border-radius(3px); - padding: 20px 50px; - -} - -.bottom-wrap { - margin-top:3em; -} - -.sidebar{ - margin-top:2em; -} - -.header{ - margin-top:3em; -} - -.site-title a{ - display:block; - width:301px; - height:71px; - text-indent:-999999em; - background:url("/static/img/logo.png"); - margin-bottom: 30px; -} - -.top-wrap, .bottom-wrap { - max-width:960px; - margin:0 auto; - @include clearfix; -} - -.footer { - margin-top: 30px; - @include box-shadow(#DDD 0 -20px 15px -15px); - border-top: 1px solid $border-color; -} - -fieldset { - border:0; - padding:0; - margin:0; -} - -form { - .field { - margin: 15px 0 10px 0; - label { - color: $primary-color; - } - } - .help { - display: block; - font-size: 80%; - } - .errors { - padding: 0; - li { - list-style-type: none; - color: #AA0000; - } - } -} - -input[type=text], input[type=password], input[type=datetime-local], select, -textarea, button, .button { - @include border-radius; - border: 1px solid $primary-color; - background: rgba($secondary-color, 0.05); - padding: 5px; - color: black; - width: 100%; - box-sizing: border-box; - margin: 5px 0; -} - -button { - background: $primary-color; - width: auto; - color: white; - padding: 5px 15px; - &:hover { - background: darken($primary-color, 10); - text-decoration: none; - } -} - -textarea { - height: 150px; -} - -h1, h2, h3, h4, h5 { - color: $primary-color; - font-weight: bold; -} - -h1, h2, h3, h4 { - margin: 20px 0 10px; -} - -h1 { - font-size: 120%; -} - -h2 { - font-size: 110%; - font-weight: normal; -} - -h3 { - font-size: 105%; -} - -h4 { - font-size: 102%; -} - -p { - margin: 10px 0; -} - -strong, b { - font-weight: bold; - color:#444; -} - -a { - color:#DD8403 -} - -.main > article { - padding-top: 5px; - &>h1, .page-title { - font-size: 130%; - border-bottom: 1px solid $border-color; - padding-bottom: 10px; - } -} - -table { - margin: 20px 0; - th { - font-weight: bold; - background: #FAFAFA; - } - th, td { - padding: 3px 10px; - } - margin-left: -10px; -} diff --git a/scss/admin.scss b/scss/admin.scss index cff013ec..a073e6ae 100644 --- a/scss/admin.scss +++ b/scss/admin.scss @@ -3,6 +3,59 @@ $default-border-radius: 2px; + +.site-wrap { + box-sizing: border-box; + max-width: 1200px; + min-width: 900px; + padding: 0 20px; + margin: 0 auto; + &> article { + padding: 70px 0 30px 0; + } +} + +.main, .sidebar, .footer { + background: white; + border: 1px solid $border-color; + @include border-radius(3px); + padding: 20px 50px; + +} + +.bottom-wrap { + margin-top:3em; +} + +.sidebar{ + margin-top:2em; +} + +.header{ + margin-top:3em; +} + +.site-title a{ + display:block; + width:301px; + height:71px; + text-indent:-999999em; + background:url("/static/img/logo.png"); + margin-bottom: 30px; +} + +.top-wrap, .bottom-wrap { + max-width:960px; + margin:0 auto; + @include clearfix; +} + +.footer { + margin-top: 30px; + @include box-shadow(#DDD 0 -20px 15px -15px); + border-top: 1px solid $border-color; +} + .site-wrap { background: white; padding: 0 50px 25px 50px; @@ -165,4 +218,4 @@ $default-border-radius: 2px; .chooser-image-name { display: block; -} \ No newline at end of file +} diff --git a/scss/monsti.scss b/scss/monsti.scss index f16f83bc..6465bcf9 100644 --- a/scss/monsti.scss +++ b/scss/monsti.scss @@ -1,60 +1,92 @@ -@import "layout"; +@import "base"; + +.header-wrap { + background: #EEE; + border-bottom: 3px solid #DDD; +} + +header { + padding-top: 10px; + @include breakpoint(medium) { + padding-top: 20px; + } +} + + +.site-logo { + display: block; + margin: 0 auto; + img { + display: block; + max-width: 100%; + } + @include breakpoint(medium) { + margin-bottom: 30px; + } +} .primary-nav { ul { + margin-left: -15px; + padding: 0; + margin-top: 10px; list-style:none; - background: #EEE; - border: thin solid $border-color; - overflow:hidden; - margin-bottom: 20px; - padding-left: 35px; + margin-bottom: -3px; } li { @include inline-block; - padding:0; - margin:0; + padding: 0; + margin: 0; + margin-right: 4px; &.active-below.child, &.active{ - background:#ddd; + a { + border-bottom: 3px solid $secondary-color; + } } &:first-child a { - border-left:thin solid gray; } } a { - color: #333; - padding:0.5em 1em; + padding: 0 15px; + color: $primary-color; + font-weight: bold; display:block; - border-right:thin solid gray; text-decoration:none; + border-bottom: 3px solid rgba($primary-color, 0.2); &:hover { - background: $secondary-color; - color: white; + border-bottom: 3px solid $primary-color; } } } -.content-wrap { - display: table; - width: 100%; -} - -.sidebar, .main { - vertical-align: top; - padding-bottom: 75px; +body { + header, .main-and-aside-wrap, footer { + max-width: 1000px; + width: 90%; + margin: 0 auto; + } } -.sidebar { - border-left: none; - box-sizing: border-box; - width: 33.33%; - display: table-cell; - background: #EEE; - padding-top: 50px; +footer { + border-top: 1px solid #DDD; } -.main { - display: table-cell; - width: 66.66%; - padding-right: 50px; - box-sizing: border-box; +@include breakpoint(medium) { + .main-and-aside-wrap { + max-width: 1000px; + display: table; + width: 100%; + padding-bottom: 30px; + } + main, .sidebar { + display: table-cell; + vertical-align: top; + padding-top: 20px; + } + main { + width: 66.6%; + } + .sidebar { + width: 33.4%; + } } \ No newline at end of file diff --git a/static/css/admin.css b/static/css/admin.css index 362eecb9..6b348b30 100644 --- a/static/css/admin.css +++ b/static/css/admin.css @@ -1 +1 @@ -html,body,div,span,applet,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,big,cite,code,del,dfn,em,img,ins,kbd,q,s,samp,small,strike,strong,sub,sup,tt,var,b,u,i,center,dl,dt,dd,ol,ul,li,fieldset,form,label,legend,table,caption,tbody,tfoot,thead,tr,th,td,article,aside,canvas,details,embed,figure,figcaption,footer,header,hgroup,menu,nav,output,ruby,section,summary,time,mark,audio,video{margin:0;padding:0;border:0;font:inherit;font-size:100%;vertical-align:baseline}html{line-height:1}ol,ul{list-style:none}table{border-collapse:collapse;border-spacing:0}caption,th,td{text-align:left;font-weight:normal;vertical-align:middle}q,blockquote{quotes:none}q:before,q:after,blockquote:before,blockquote:after{content:"";content:none}a img{border:none}article,aside,details,figcaption,figure,footer,header,hgroup,main,menu,nav,section,summary{display:block}html,body,div,span,applet,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,big,cite,code,del,dfn,em,img,ins,kbd,q,s,samp,small,strike,strong,sub,sup,tt,var,b,u,i,center,dl,dt,dd,ol,ul,li,fieldset,form,label,legend,table,caption,tbody,tfoot,thead,tr,th,td,article,aside,canvas,details,embed,figure,figcaption,footer,header,hgroup,menu,nav,output,ruby,section,summary,time,mark,audio,video{margin:0;padding:0;border:0;font:inherit;font-size:100%;vertical-align:baseline}html{line-height:1}ol,ul{list-style:none}table{border-collapse:collapse;border-spacing:0}caption,th,td{text-align:left;font-weight:normal;vertical-align:middle}q,blockquote{quotes:none}q:before,q:after,blockquote:before,blockquote:after{content:"";content:none}a img{border:none}article,aside,details,figcaption,figure,footer,header,hgroup,main,menu,nav,section,summary{display:block}.monsti--htmlarea-align-left{float:left;margin-right:15px}.monsti--htmlarea-align-right{float:right;margin-left:15px}.monsti--htmlarea-align-center{text-align:center}html{font:16px/23.3667px arial, sans-serif;background:#f5f7f8;position:relative}html,body{height:100%}body{padding:0;margin:0;color:#666}.site-wrap{box-sizing:border-box;max-width:1200px;min-width:900px;padding:0 20px;margin:0 auto}.site-wrap>article{padding:70px 0 30px 0}.main,.sidebar,.footer{background:white;border:1px solid #CCC;-moz-border-radius:3px;-webkit-border-radius:3px;border-radius:3px;padding:20px 50px}.bottom-wrap{margin-top:3em}.sidebar{margin-top:2em}.header{margin-top:3em}.site-title a{display:block;width:301px;height:71px;text-indent:-999999em;background:url("/static/img/logo.png");margin-bottom:30px}.top-wrap,.bottom-wrap{max-width:960px;margin:0 auto;overflow:hidden;*zoom:1}.footer{margin-top:30px;-moz-box-shadow:#DDD 0 -20px 15px -15px;-webkit-box-shadow:#DDD 0 -20px 15px -15px;box-shadow:#DDD 0 -20px 15px -15px;border-top:1px solid #CCC}fieldset{border:0;padding:0;margin:0}form .field{margin:15px 0 10px 0}form .field label{color:#274661}form .help{display:block;font-size:80%}form .errors{padding:0}form .errors li{list-style-type:none;color:#AA0000}input[type=text],input[type=password],input[type=datetime-local],select,textarea,button,.button{-moz-border-radius:5px;-webkit-border-radius:5px;border-radius:5px;border:1px solid #274661;background:rgba(248,155,22,0.05);padding:5px;color:black;width:100%;box-sizing:border-box;margin:5px 0}button{background:#274661;width:auto;color:white;padding:5px 15px}button:hover{background:#182c3d;text-decoration:none}textarea{height:150px}h1,h2,h3,h4,h5{color:#274661;font-weight:bold}h1,h2,h3,h4{margin:20px 0 10px}h1{font-size:120%}h2{font-size:110%;font-weight:normal}h3{font-size:105%}h4{font-size:102%}p{margin:10px 0}strong,b{font-weight:bold;color:#444}a{color:#DD8403}.main>article{padding-top:5px}.main>article>h1,.main>article .page-title{font-size:130%;border-bottom:1px solid #CCC;padding-bottom:10px}table{margin:20px 0;margin-left:-10px}table th{font-weight:bold;background:#FAFAFA}table th,table td{padding:3px 10px}.site-wrap{background:white;padding:0 50px 25px 50px;min-height:100%}.site-wrap-slim{min-height:auto;padding:0}.admin-bar{position:absolute;top:0;overflow:hidden;*zoom:1;margin-bottom:30px}.content-wrap{padding-top:30px}.content-wrap-slim{padding:15px}.node-list .node-list-actions li{display:inline-block;vertical-align:middle;*vertical-align:auto;*zoom:1;*display:inline}.node-list li{margin:15px 0}.node-list li .node-list-inner-wrap{background:#FAFAFA;border:1px solid #CCC;padding:10px}.node-list li .node-list-order{display:none}.node-list li .node-list-name{display:inline-block;vertical-align:middle;*vertical-align:auto;*zoom:1;*display:inline;width:30%}.node-list li .node-list-name a{text-decoration:none}.node-list li[draggable=true]{cursor:move}.node-list.drag-active li[draggable=true]{padding-top:0px;padding-bottom:0px}.node-list.drag-active li[draggable=true] *{pointer-events:none}.node-list.drag-active li[draggable=true].drag-start{opacity:0.5}.node-list.drag-active li[draggable=true].drag-over.drag-after,.node-list.drag-active li[draggable=true].drag-over.drag-before{position:relative}.node-list.drag-active li[draggable=true].drag-over.drag-after::after,.node-list.drag-active li[draggable=true].drag-over.drag-after::before,.node-list.drag-active li[draggable=true].drag-over.drag-before::after,.node-list.drag-active li[draggable=true].drag-over.drag-before::before{display:none;position:absolute;content:"";height:15px;width:100%;border:1px dotted #274661;background:#EEE}.node-list.drag-active li[draggable=true].drag-over.drag-after{margin-bottom:25px}.node-list.drag-active li[draggable=true].drag-over.drag-after::after{display:block;bottom:-20px}.node-list.drag-active li[draggable=true].drag-over.drag-before{margin-top:25px}.node-list.drag-active li[draggable=true].drag-over.drag-before::before{display:block;top:-20px}.alert{border:1px solid black;padding:15px 20px;-moz-border-radius:5px;-webkit-border-radius:5px;border-radius:5px;margin:20px 0}.alert-success{background:#DFF0D8;border-color:#c1e2b3;color:#3d6c2a}.alert-success strong{color:#28471c}.alert-warning{background:#FCF8E3;border-color:#f7ecb5;color:#9c8611}.alert-warning strong{color:#6e5e0c}.chooser-back{text-decoration:none;margin-right:5px;color:black}.chooser-current-path{display:inline-block;vertical-align:middle;*vertical-align:auto;*zoom:1;*display:inline;margin-right:15px}.chooser-back,.chooser-current-path-element{display:inline-block;vertical-align:middle;*vertical-align:auto;*zoom:1;*display:inline;padding:5px;background:#FAFAFA;border:1px solid #CCC;-moz-border-radius:2px;-webkit-border-radius:2px;border-radius:2px;min-width:15px;text-align:center}.chooser-current-path-link{display:block;margin:-5px;padding:5px;text-decoration:none}.chooser-back:hover,.chooser-current-path-link:hover{background-color:#EEE}.chooser-image{display:inline-block;vertical-align:middle;*vertical-align:auto;*zoom:1;*display:inline;width:152px;margin-top:20px;margin-right:20px}.chooser-image img{border:1px solid #555}.chooser-image-name{display:block} +html,body,div,span,applet,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,big,cite,code,del,dfn,em,img,ins,kbd,q,s,samp,small,strike,strong,sub,sup,tt,var,b,u,i,center,dl,dt,dd,ol,ul,li,fieldset,form,label,legend,table,caption,tbody,tfoot,thead,tr,th,td,article,aside,canvas,details,embed,figure,figcaption,footer,header,hgroup,menu,nav,output,ruby,section,summary,time,mark,audio,video{margin:0;padding:0;border:0;font:inherit;font-size:100%;vertical-align:baseline}html{line-height:1}ol,ul{list-style:none}table{border-collapse:collapse;border-spacing:0}caption,th,td{text-align:left;font-weight:normal;vertical-align:middle}q,blockquote{quotes:none}q:before,q:after,blockquote:before,blockquote:after{content:"";content:none}a img{border:none}article,aside,details,figcaption,figure,footer,header,hgroup,main,menu,nav,section,summary{display:block}html,body,div,span,applet,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,big,cite,code,del,dfn,em,img,ins,kbd,q,s,samp,small,strike,strong,sub,sup,tt,var,b,u,i,center,dl,dt,dd,ol,ul,li,fieldset,form,label,legend,table,caption,tbody,tfoot,thead,tr,th,td,article,aside,canvas,details,embed,figure,figcaption,footer,header,hgroup,menu,nav,output,ruby,section,summary,time,mark,audio,video{margin:0;padding:0;border:0;font:inherit;font-size:100%;vertical-align:baseline}html{line-height:1}ol,ul{list-style:none}table{border-collapse:collapse;border-spacing:0}caption,th,td{text-align:left;font-weight:normal;vertical-align:middle}q,blockquote{quotes:none}q:before,q:after,blockquote:before,blockquote:after{content:"";content:none}a img{border:none}article,aside,details,figcaption,figure,footer,header,hgroup,main,menu,nav,section,summary{display:block}.monsti--htmlarea-align-left{float:left;margin-right:15px}.monsti--htmlarea-align-right{float:right;margin-left:15px}.monsti--htmlarea-align-center{text-align:center}*{box-sizing:border-box}html{font:16px/23.3667px arial, sans-serif;background:#f5f7f8;position:relative}html,body{height:100%}body{padding:0;margin:0;color:#666}fieldset{border:0;padding:0;margin:0}form .field{margin:15px 0 10px 0}form .field label{color:#274661}form .help{display:block;font-size:80%}form .errors{padding:0}form .errors li{list-style-type:none;color:#AA0000}input[type=text],input[type=password],input[type=datetime-local],select,textarea,button,.button{-moz-border-radius:5px;-webkit-border-radius:5px;border-radius:5px;border:1px solid #274661;background:rgba(248,155,22,0.05);padding:5px;color:black;width:100%;margin:5px 0}button{background:#274661;width:auto;color:white;padding:5px 15px}button:hover{background:#182c3d;text-decoration:none}textarea{height:150px}h1,h2,h3,h4,h5{color:#274661;font-weight:bold}h1,h2,h3,h4{margin:20px 0 10px}h1{font-size:120%}h2{font-size:110%;font-weight:normal}h3{font-size:105%}h4{font-size:102%}p{margin:10px 0}strong,b{font-weight:bold;color:#444}a{color:#DD8403}.main>article{padding-top:5px}.main>article>h1,.main>article .page-title{font-size:130%;border-bottom:1px solid #CCC;padding-bottom:10px}table{margin:20px 0;margin-left:-10px}table th{font-weight:bold;background:#FAFAFA}table th,table td{padding:3px 10px}.site-wrap{box-sizing:border-box;max-width:1200px;min-width:900px;padding:0 20px;margin:0 auto}.site-wrap>article{padding:70px 0 30px 0}.main,.sidebar,.footer{background:white;border:1px solid #CCC;-moz-border-radius:3px;-webkit-border-radius:3px;border-radius:3px;padding:20px 50px}.bottom-wrap{margin-top:3em}.sidebar{margin-top:2em}.header{margin-top:3em}.site-title a{display:block;width:301px;height:71px;text-indent:-999999em;background:url("/static/img/logo.png");margin-bottom:30px}.top-wrap,.bottom-wrap{max-width:960px;margin:0 auto;overflow:hidden;*zoom:1}.footer{margin-top:30px;-moz-box-shadow:#DDD 0 -20px 15px -15px;-webkit-box-shadow:#DDD 0 -20px 15px -15px;box-shadow:#DDD 0 -20px 15px -15px;border-top:1px solid #CCC}.site-wrap{background:white;padding:0 50px 25px 50px;min-height:100%}.site-wrap-slim{min-height:auto;padding:0}.admin-bar{position:absolute;top:0;overflow:hidden;*zoom:1;margin-bottom:30px}.content-wrap{padding-top:30px}.content-wrap-slim{padding:15px}.node-list .node-list-actions li{display:inline-block;vertical-align:middle;*vertical-align:auto;*zoom:1;*display:inline}.node-list li{margin:15px 0}.node-list li .node-list-inner-wrap{background:#FAFAFA;border:1px solid #CCC;padding:10px}.node-list li .node-list-order{display:none}.node-list li .node-list-name{display:inline-block;vertical-align:middle;*vertical-align:auto;*zoom:1;*display:inline;width:30%}.node-list li .node-list-name a{text-decoration:none}.node-list li[draggable=true]{cursor:move}.node-list.drag-active li[draggable=true]{padding-top:0px;padding-bottom:0px}.node-list.drag-active li[draggable=true] *{pointer-events:none}.node-list.drag-active li[draggable=true].drag-start{opacity:0.5}.node-list.drag-active li[draggable=true].drag-over.drag-after,.node-list.drag-active li[draggable=true].drag-over.drag-before{position:relative}.node-list.drag-active li[draggable=true].drag-over.drag-after::after,.node-list.drag-active li[draggable=true].drag-over.drag-after::before,.node-list.drag-active li[draggable=true].drag-over.drag-before::after,.node-list.drag-active li[draggable=true].drag-over.drag-before::before{display:none;position:absolute;content:"";height:15px;width:100%;border:1px dotted #274661;background:#EEE}.node-list.drag-active li[draggable=true].drag-over.drag-after{margin-bottom:25px}.node-list.drag-active li[draggable=true].drag-over.drag-after::after{display:block;bottom:-20px}.node-list.drag-active li[draggable=true].drag-over.drag-before{margin-top:25px}.node-list.drag-active li[draggable=true].drag-over.drag-before::before{display:block;top:-20px}.alert{border:1px solid black;padding:15px 20px;-moz-border-radius:5px;-webkit-border-radius:5px;border-radius:5px;margin:20px 0}.alert-success{background:#DFF0D8;border-color:#c1e2b3;color:#3d6c2a}.alert-success strong{color:#28471c}.alert-warning{background:#FCF8E3;border-color:#f7ecb5;color:#9c8611}.alert-warning strong{color:#6e5e0c}.chooser-back{text-decoration:none;margin-right:5px;color:black}.chooser-current-path{display:inline-block;vertical-align:middle;*vertical-align:auto;*zoom:1;*display:inline;margin-right:15px}.chooser-back,.chooser-current-path-element{display:inline-block;vertical-align:middle;*vertical-align:auto;*zoom:1;*display:inline;padding:5px;background:#FAFAFA;border:1px solid #CCC;-moz-border-radius:2px;-webkit-border-radius:2px;border-radius:2px;min-width:15px;text-align:center}.chooser-current-path-link{display:block;margin:-5px;padding:5px;text-decoration:none}.chooser-back:hover,.chooser-current-path-link:hover{background-color:#EEE}.chooser-image{display:inline-block;vertical-align:middle;*vertical-align:auto;*zoom:1;*display:inline;width:152px;margin-top:20px;margin-right:20px}.chooser-image img{border:1px solid #555}.chooser-image-name{display:block} diff --git a/static/css/admin_bar.css b/static/css/admin_bar.css index b91fd381..41ad9c5e 100644 --- a/static/css/admin_bar.css +++ b/static/css/admin_bar.css @@ -1 +1 @@ -.monsti--htmlarea-align-left{float:left;margin-right:15px}.monsti--htmlarea-align-right{float:right;margin-left:15px}.monsti--htmlarea-align-center{text-align:center}html{margin-top:32px !important}#monsti--admin-bar{font:12px/18px arial, sans-serif;position:fixed;z-index:99999;top:0;left:0;width:100%;background:#EEE;padding:0;margin:0;border-bottom:1px solid #aaa;background-image:url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4gPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJncmFkIiBncmFkaWVudFVuaXRzPSJvYmplY3RCb3VuZGluZ0JveCIgeDE9IjAuNSIgeTE9IjEuMCIgeDI9IjAuNSIgeTI9IjAuMCI+PHN0b3Agb2Zmc2V0PSIyNSUiIHN0b3AtY29sb3I9IiNkZWRlZGUiLz48c3RvcCBvZmZzZXQ9IjYzJSIgc3RvcC1jb2xvcj0iI2Y3ZjdmNyIvPjwvbGluZWFyR3JhZGllbnQ+PC9kZWZzPjxyZWN0IHg9IjAiIHk9IjAiIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9InVybCgjZ3JhZCkiIC8+PC9zdmc+IA==');background-size:100%;background-image:-webkit-gradient(linear, 50% 100%, 50% 0%, color-stop(25%, #dedede),color-stop(63%, #f7f7f7));background-image:-moz-linear-gradient(bottom, #dedede 25%,#f7f7f7 63%);background-image:-webkit-linear-gradient(bottom, #dedede 25%,#f7f7f7 63%);background-image:linear-gradient(to top, #dedede 25%,#f7f7f7 63%)}#monsti--admin-bar>div{box-sizing:border-box;padding:0px 15px;overflow:hidden;*zoom:1}#monsti--admin-bar .brand{float:left;vertical-align:middle;margin:2px 40px 0 0;padding:0}#monsti--admin-bar ul{list-style:none;padding:0;margin:0}#monsti--admin-bar ul li{float:left;margin:0 20px 0 0;padding:0;line-height:30px}#monsti--admin-bar ul li img{vertical-align:middle}#monsti--admin-bar ul li:last-child{margin-right:0}#monsti--admin-bar .pull-right{float:right}#monsti--admin-bar a{text-decoration:none;color:#333;font-weight:200}#monsti--admin-bar .admin-bar-item-inactive{opacity:0.5}.current-node{margin:0;float:left;line-height:30px;margin-left:30px}.current-node-title{font-style:italic} +.monsti--htmlarea-align-left{float:left;margin-right:15px}.monsti--htmlarea-align-right{float:right;margin-left:15px}.monsti--htmlarea-align-center{text-align:center}*{box-sizing:border-box}html{font:16px/23.3667px arial, sans-serif;background:#f5f7f8;position:relative}html,body{height:100%}body{padding:0;margin:0;color:#666}fieldset{border:0;padding:0;margin:0}form .field{margin:15px 0 10px 0}form .field label{color:#274661}form .help{display:block;font-size:80%}form .errors{padding:0}form .errors li{list-style-type:none;color:#AA0000}input[type=text],input[type=password],input[type=datetime-local],select,textarea,button,.button{-moz-border-radius:5px;-webkit-border-radius:5px;border-radius:5px;border:1px solid #274661;background:rgba(248,155,22,0.05);padding:5px;color:black;width:100%;margin:5px 0}button{background:#274661;width:auto;color:white;padding:5px 15px}button:hover{background:#182c3d;text-decoration:none}textarea{height:150px}h1,h2,h3,h4,h5{color:#274661;font-weight:bold}h1,h2,h3,h4{margin:20px 0 10px}h1{font-size:120%}h2{font-size:110%;font-weight:normal}h3{font-size:105%}h4{font-size:102%}p{margin:10px 0}strong,b{font-weight:bold;color:#444}a{color:#DD8403}.main>article{padding-top:5px}.main>article>h1,.main>article .page-title{font-size:130%;border-bottom:1px solid #CCC;padding-bottom:10px}table{margin:20px 0;margin-left:-10px}table th{font-weight:bold;background:#FAFAFA}table th,table td{padding:3px 10px}html{margin-top:32px !important}#monsti--admin-bar{font:12px/18px arial, sans-serif;position:fixed;z-index:99999;top:0;left:0;width:100%;background:#EEE;padding:0;margin:0;border-bottom:1px solid #aaa;background-image:url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4gPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJncmFkIiBncmFkaWVudFVuaXRzPSJvYmplY3RCb3VuZGluZ0JveCIgeDE9IjAuNSIgeTE9IjEuMCIgeDI9IjAuNSIgeTI9IjAuMCI+PHN0b3Agb2Zmc2V0PSIyNSUiIHN0b3AtY29sb3I9IiNkZWRlZGUiLz48c3RvcCBvZmZzZXQ9IjYzJSIgc3RvcC1jb2xvcj0iI2Y3ZjdmNyIvPjwvbGluZWFyR3JhZGllbnQ+PC9kZWZzPjxyZWN0IHg9IjAiIHk9IjAiIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9InVybCgjZ3JhZCkiIC8+PC9zdmc+IA==');background-size:100%;background-image:-webkit-gradient(linear, 50% 100%, 50% 0%, color-stop(25%, #dedede),color-stop(63%, #f7f7f7));background-image:-moz-linear-gradient(bottom, #dedede 25%,#f7f7f7 63%);background-image:-webkit-linear-gradient(bottom, #dedede 25%,#f7f7f7 63%);background-image:linear-gradient(to top, #dedede 25%,#f7f7f7 63%)}#monsti--admin-bar>div{box-sizing:border-box;padding:0px 15px;overflow:hidden;*zoom:1}#monsti--admin-bar .brand{float:left;vertical-align:middle;margin:2px 40px 0 0;padding:0}#monsti--admin-bar ul{list-style:none;padding:0;margin:0}#monsti--admin-bar ul li{float:left;margin:0 20px 0 0;padding:0;line-height:30px}#monsti--admin-bar ul li img{vertical-align:middle}#monsti--admin-bar ul li:last-child{margin-right:0}#monsti--admin-bar .pull-right{float:right}#monsti--admin-bar a{text-decoration:none;color:#333;font-weight:200}#monsti--admin-bar .admin-bar-item-inactive{opacity:0.5}.current-node{margin:0;float:left;line-height:30px;margin-left:30px}.current-node-title{font-style:italic} diff --git a/static/css/monsti.css b/static/css/monsti.css index 35bd459c..ac02e287 100644 --- a/static/css/monsti.css +++ b/static/css/monsti.css @@ -1 +1 @@ -html,body,div,span,applet,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,big,cite,code,del,dfn,em,img,ins,kbd,q,s,samp,small,strike,strong,sub,sup,tt,var,b,u,i,center,dl,dt,dd,ol,ul,li,fieldset,form,label,legend,table,caption,tbody,tfoot,thead,tr,th,td,article,aside,canvas,details,embed,figure,figcaption,footer,header,hgroup,menu,nav,output,ruby,section,summary,time,mark,audio,video{margin:0;padding:0;border:0;font:inherit;font-size:100%;vertical-align:baseline}html{line-height:1}ol,ul{list-style:none}table{border-collapse:collapse;border-spacing:0}caption,th,td{text-align:left;font-weight:normal;vertical-align:middle}q,blockquote{quotes:none}q:before,q:after,blockquote:before,blockquote:after{content:"";content:none}a img{border:none}article,aside,details,figcaption,figure,footer,header,hgroup,menu,nav,section,summary{display:block}.monsti--htmlarea-align-left{float:left;margin-right:15px}.monsti--htmlarea-align-right{float:right;margin-left:15px}.monsti--htmlarea-align-center{text-align:center}html{font:16px/23.3667px arial, sans-serif;background:#f5f7f8;position:relative}html,body{height:100%}body{padding:0;margin:0;color:#666}.site-wrap{box-sizing:border-box;max-width:1200px;min-width:900px;padding:0 20px;margin:0 auto}.site-wrap>article{padding:70px 0 30px 0}.main,.sidebar,.footer{background:white;border:1px solid #ccc;-webkit-border-radius:3px;-moz-border-radius:3px;-ms-border-radius:3px;-o-border-radius:3px;border-radius:3px;padding:20px 50px}.bottom-wrap{margin-top:3em}.sidebar{margin-top:2em}.header{margin-top:3em}.site-title a{display:block;width:301px;height:71px;text-indent:-999999em;background:url("/static/img/logo.png");margin-bottom:30px}.top-wrap,.bottom-wrap{max-width:960px;margin:0 auto;overflow:hidden;*zoom:1}.footer{margin-top:30px;-webkit-box-shadow:#ddd 0 -20px 15px -15px;-moz-box-shadow:#ddd 0 -20px 15px -15px;box-shadow:#ddd 0 -20px 15px -15px;border-top:1px solid #ccc}fieldset{border:0;padding:0;margin:0}form .field{margin:15px 0 10px 0}form .field label{color:#274661}form .help{display:block;font-size:80%}form .errors{padding:0}form .errors li{list-style-type:none;color:#AA0000}input[type=text],input[type=password],input[type=datetime-local],select,textarea,button,.button{-webkit-border-radius:5px;-moz-border-radius:5px;-ms-border-radius:5px;-o-border-radius:5px;border-radius:5px;border:1px solid #274661;background:rgba(248,155,22,0.05);padding:5px;color:black;width:100%;box-sizing:border-box;margin:5px 0}button{background:#274661;width:auto;color:white;padding:5px 15px}button:hover{background:#182c3d;text-decoration:none}textarea{height:150px}h1,h2,h3,h4,h5{color:#274661;font-weight:bold}h1,h2,h3,h4{margin:20px 0 10px}h1{font-size:120%}h2{font-size:110%;font-weight:normal}h3{font-size:105%}h4{font-size:102%}p{margin:10px 0}strong,b{font-weight:bold;color:#444}a{color:#dd8403}.main>article{padding-top:5px}.main>article>h1,.main>article .page-title{font-size:130%;border-bottom:1px solid #ccc;padding-bottom:10px}table{margin:20px 0;margin-left:-10px}table th{font-weight:bold;background:#FAFAFA}table th,table td{padding:3px 10px}.primary-nav ul{list-style:none;background:#EEE;border:thin solid #ccc;overflow:hidden;margin-bottom:20px;padding-left:35px}.primary-nav li{display:-moz-inline-stack;display:inline-block;vertical-align:middle;*vertical-align:auto;zoom:1;*display:inline;padding:0;margin:0}.primary-nav li.active-below.child,.primary-nav li.active{background:#ddd}.primary-nav li:first-child a{border-left:thin solid gray}.primary-nav a{color:#333;padding:0.5em 1em;display:block;border-right:thin solid gray;text-decoration:none}.primary-nav a:hover{background:#f89b16;color:white}.content-wrap{display:table;width:100%}.sidebar,.main{vertical-align:top;padding-bottom:75px}.sidebar{border-left:none;box-sizing:border-box;width:33.33%;display:table-cell;background:#EEE;padding-top:50px}.main{display:table-cell;width:66.66%;padding-right:50px;box-sizing:border-box} +.monsti--htmlarea-align-left{float:left;margin-right:15px}.monsti--htmlarea-align-right{float:right;margin-left:15px}.monsti--htmlarea-align-center{text-align:center}*{box-sizing:border-box}html{font:16px/23.3667px arial, sans-serif;background:#f5f7f8;position:relative}html,body{height:100%}body{padding:0;margin:0;color:#666}fieldset{border:0;padding:0;margin:0}form .field{margin:15px 0 10px 0}form .field label{color:#274661}form .help{display:block;font-size:80%}form .errors{padding:0}form .errors li{list-style-type:none;color:#AA0000}input[type=text],input[type=password],input[type=datetime-local],select,textarea,button,.button{-moz-border-radius:5px;-webkit-border-radius:5px;border-radius:5px;border:1px solid #274661;background:rgba(248,155,22,0.05);padding:5px;color:black;width:100%;margin:5px 0}button{background:#274661;width:auto;color:white;padding:5px 15px}button:hover{background:#182c3d;text-decoration:none}textarea{height:150px}h1,h2,h3,h4,h5{color:#274661;font-weight:bold}h1,h2,h3,h4{margin:20px 0 10px}h1{font-size:120%}h2{font-size:110%;font-weight:normal}h3{font-size:105%}h4{font-size:102%}p{margin:10px 0}strong,b{font-weight:bold;color:#444}a{color:#DD8403}.main>article{padding-top:5px}.main>article>h1,.main>article .page-title{font-size:130%;border-bottom:1px solid #CCC;padding-bottom:10px}table{margin:20px 0;margin-left:-10px}table th{font-weight:bold;background:#FAFAFA}table th,table td{padding:3px 10px}.header-wrap{background:#EEE;border-bottom:3px solid #DDD}header{padding-top:10px}@media (min-width: 768px){header{padding-top:20px}}.site-logo{display:block;margin:0 auto}.site-logo img{display:block;max-width:100%}@media (min-width: 768px){.site-logo{margin-bottom:30px}}.primary-nav ul{margin-left:-15px;padding:0;margin-top:10px;list-style:none;margin-bottom:-3px}.primary-nav li{display:inline-block;vertical-align:middle;*vertical-align:auto;*zoom:1;*display:inline;padding:0;margin:0;margin-right:4px}.primary-nav li.active-below.child a,.primary-nav li.active a{border-bottom:3px solid #F89B16}.primary-nav a{padding:0 15px;color:#274661;font-weight:bold;display:block;text-decoration:none;border-bottom:3px solid rgba(39,70,97,0.2)}.primary-nav a:hover{border-bottom:3px solid #274661}body header,body .main-and-aside-wrap,body footer{max-width:1000px;width:90%;margin:0 auto}footer{border-top:1px solid #DDD}@media (min-width: 768px){.main-and-aside-wrap{max-width:1000px;display:table;width:100%;padding-bottom:30px}main,.sidebar{display:table-cell;vertical-align:top;padding-top:20px}main{width:66.6%}.sidebar{width:33.4%}} diff --git a/templates/blocks/widget.html b/templates/blocks/widget.html index bf644aea..356bcf14 100644 --- a/templates/blocks/widget.html +++ b/templates/blocks/widget.html @@ -34,6 +34,16 @@ {{else if eq .Template "time"}} + {{else if eq .Template "list"}} + {{$listWidget := .}} + {{range .Data.Fields}} + {{template "blocks/widget" .}} + + {{end}} + + {{else}} Unknown widget template: {{.Template}} {{end}} diff --git a/templates/master.html b/templates/master.html index 343339f5..e24348c9 100644 --- a/templates/master.html +++ b/templates/master.html @@ -3,47 +3,36 @@ {{template "blocks/headers" .}} - {{template "blocks/admin-bar" .}} -
-
-
-
- -
- {{template "blocks/navigation" .Page.PrimaryNav}} -
-
+
+
+ +
+ {{template "blocks/navigation" .Page.PrimaryNav}}
-
-
-
-
- {{.Page.Content}} -
- -
-
-
- +
+
+ {{.Page.Content}} +
+
+ {{end}} +
+