├── model ├── doc.go ├── reference.go ├── sliceiterator_test.go ├── iteratorfunc.go ├── emptyiterator.go ├── errors.go ├── erroronlyiterator.go ├── config.go ├── filteriterator.go ├── validator.go ├── limitediterator.go ├── value.go ├── file.go ├── conversioniterator.go ├── singletoniterator.go ├── visitorfunc.go ├── blob.go ├── bool.go ├── ref.go ├── geolocation.go ├── sliceiterator.go ├── time.go ├── indexedsliceiterator.go ├── sortiterator.go ├── email.go ├── multiplechoice.go ├── phone.go ├── randomiterator.go ├── url.go ├── country.go ├── language.go ├── text.go └── date.go ├── examples ├── ServeStatic │ ├── index.html │ ├── README.md │ └── main.go ├── FullTutorial │ ├── FullTutorial │ ├── static │ │ └── images │ │ │ └── gopher.png │ ├── views │ │ ├── admin │ │ │ ├── images.go │ │ │ ├── admin.go │ │ │ ├── auth.go │ │ │ ├── export-emails.go │ │ │ └── shared.go │ │ ├── root │ │ │ ├── css.go │ │ │ ├── homepage.go │ │ │ ├── loginsignup.go │ │ │ └── profile.go │ │ ├── paths.go │ │ └── shared.go │ ├── models │ │ ├── colorscheme.go │ │ └── user.go │ ├── templates │ │ └── css │ │ │ ├── style.css │ │ │ └── common.css │ └── config.json ├── CombineStaticDirs │ ├── static-dir-a │ │ ├── index.html │ │ └── favicon.ico │ ├── static-dir-b │ │ └── second-static-dir.html │ ├── README.md │ └── main.go └── ViewPaths │ ├── config.json │ ├── views │ ├── root │ │ ├── getjson.go │ │ ├── getxml.go │ │ └── homepage.go │ ├── admin │ │ ├── admin.go │ │ └── user_0 │ │ │ └── user_0.go │ └── paths.go │ ├── main.go │ └── README.md ├── user ├── doc.go ├── auth.go ├── formmodels.go ├── nav.go ├── xingidentity.go ├── skypeidentity.go ├── githubidentity.go ├── config.go ├── linkedinidentity.go ├── facebookidentity.go └── twitteridentity.go ├── media ├── static │ └── media │ │ ├── dummy.png │ │ ├── loading.gif │ │ └── media.js ├── viewpath.go ├── modelrect.go ├── fileview.go ├── api.go ├── blobref.go └── backend.go ├── utils ├── doc.go ├── experimental.go ├── float.go ├── set.go ├── net.go ├── stringwriter.go ├── datetime.go ├── stringbuilder.go └── filesystem.go ├── mongocsrfp └── doc.go ├── sublime-snippets ├── README.md ├── go-start-dynamicview.sublime-snippet ├── go-start-modeliterator.sublime-snippet └── go-start-form.sublime-snippet ├── static ├── img │ └── wysihtml5 │ │ └── spr_toolbar_icons_r1.png ├── css │ └── ui-lightness │ │ └── images │ │ ├── ui-icons_222222_256x240.png │ │ ├── ui-icons_228ef1_256x240.png │ │ ├── ui-icons_ef8c08_256x240.png │ │ ├── ui-icons_ffd27a_256x240.png │ │ ├── ui-icons_ffffff_256x240.png │ │ ├── ui-bg_flat_10_000000_40x100.png │ │ ├── ui-bg_glass_100_f6f6f6_1x400.png │ │ ├── ui-bg_glass_100_fdf5ce_1x400.png │ │ ├── ui-bg_glass_65_ffffff_1x400.png │ │ ├── ui-bg_gloss-wave_35_f6a828_500x100.png │ │ ├── ui-bg_diagonals-thick_18_b81900_40x40.png │ │ ├── ui-bg_diagonals-thick_20_666666_40x40.png │ │ ├── ui-bg_highlight-soft_100_eeeeee_1x100.png │ │ └── ui-bg_highlight-soft_75_ffe45c_1x100.png ├── crossdomain.xml └── js │ └── log.js ├── view ├── csrfprotector.go ├── url.go ├── viewwithurl.go ├── middleware.go ├── viewwithurlbase.go ├── notfoundview.go ├── comment.go ├── stringurl.go ├── redirectview.go ├── urlwithargs.go ├── html.go ├── errors.go ├── format.go ├── canvas.go ├── error.go ├── viewbasewithid.go ├── fileinput.go ├── pagevar.go ├── label.go ├── paragraph.go ├── aside.go ├── span.go ├── pagelink.go ├── hiddeninput.go ├── viewurlwrapper.go ├── urllink.go ├── stringlink.go ├── shorttag.go ├── viewbase.go ├── div.go ├── indirectviewwithurl.go ├── image.go ├── views.go ├── indirecturl.go ├── sessiontracker.go ├── submitbutton.go ├── iframe.go ├── view.go ├── css.go ├── checkbox.go ├── if.go ├── staticfile.go ├── button.go ├── link.go ├── tag.go ├── contactform.go ├── modaldialog.go ├── textpreview.go ├── video.go ├── modeliteratorview.go ├── dummyimage.go ├── frontcontroller.go ├── context.go ├── concatstaticfiles.go ├── google.go ├── renderer.go └── textfield.go ├── errs ├── variables.go └── functions.go ├── mongo ├── doc.go ├── query_filterref.go ├── query_skip.go ├── query_filterin.go ├── query_filterless.go ├── query_limit.go ├── query_or.go ├── query_filternotin.go ├── query_filtergreater.go ├── query_filternotequal.go ├── query_filterlessequal.go ├── query_filtergreaterequal.go ├── subdocument.go ├── query_filtermodulo.go ├── query_filterexists.go ├── query_filterallin.go ├── query_filterarraysize.go ├── query_subdocument.go ├── dereferenceiterator.go ├── query_filterstartswith.go ├── query_filterequal.go ├── query_filterequalcaseinsensitive.go ├── document.go ├── query_sort.go ├── query_filtercontains.go ├── query_filterendswith.go ├── mongoiterator.go ├── config.go └── query_filterquery.go ├── templatesystem ├── interfaces.go ├── gotemplate.go ├── passthrough.go ├── kasia.go ├── mustache.go └── printf.go ├── .gitignore ├── i18n └── languages.go ├── mongomedia ├── blobdoc.go ├── imagedoc.go └── config.go ├── states ├── basestate.go ├── state.go ├── transition.go └── statemachine.go ├── reflection ├── functions_test.go ├── modifyslicestructvisitor.go └── slices.go ├── modelext ├── postaladdress.go └── name.go ├── LICENSE ├── templates └── 404.html ├── mongoadmin └── removeinvalidrefsbutton.go └── config └── config.go /model/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | The Model of MVC. 3 | */ 4 | package model -------------------------------------------------------------------------------- /examples/ServeStatic/index.html: -------------------------------------------------------------------------------- 1 |

Serving static -------------------------------------------------------------------------------- /user/doc.go: -------------------------------------------------------------------------------- 1 | // User management (sign up, login, sessions). 2 | package user 3 | -------------------------------------------------------------------------------- /media/static/media/dummy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ungerik/go-start/HEAD/media/static/media/dummy.png -------------------------------------------------------------------------------- /utils/doc.go: -------------------------------------------------------------------------------- 1 | // Helpers, convenience functions and stuff missing in the standard packages. 2 | package utils -------------------------------------------------------------------------------- /media/static/media/loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ungerik/go-start/HEAD/media/static/media/loading.gif -------------------------------------------------------------------------------- /model/reference.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | type Reference interface { 4 | Value 5 | StringID() string 6 | } 7 | -------------------------------------------------------------------------------- /examples/FullTutorial/FullTutorial: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ungerik/go-start/HEAD/examples/FullTutorial/FullTutorial -------------------------------------------------------------------------------- /mongocsrfp/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Implementation of view.CSRFProtector with a MongoDB collection. 3 | */ 4 | package mongocsrfp 5 | -------------------------------------------------------------------------------- /sublime-snippets/README.md: -------------------------------------------------------------------------------- 1 | Install: 2 | 3 | cp sublime-snippets/*.sublime-snippet ~/.config/sublime-text-2/Packages/User/ -------------------------------------------------------------------------------- /static/img/wysihtml5/spr_toolbar_icons_r1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ungerik/go-start/HEAD/static/img/wysihtml5/spr_toolbar_icons_r1.png -------------------------------------------------------------------------------- /examples/FullTutorial/static/images/gopher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ungerik/go-start/HEAD/examples/FullTutorial/static/images/gopher.png -------------------------------------------------------------------------------- /examples/CombineStaticDirs/static-dir-a/index.html: -------------------------------------------------------------------------------- 1 | 2 |

/index.html

3 | /second-static-dir.html -------------------------------------------------------------------------------- /examples/CombineStaticDirs/static-dir-a/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ungerik/go-start/HEAD/examples/CombineStaticDirs/static-dir-a/favicon.ico -------------------------------------------------------------------------------- /view/csrfprotector.go: -------------------------------------------------------------------------------- 1 | package view 2 | 3 | type CSRFProtector interface { 4 | ExtraFormField() View 5 | Validate(ctx *Context) (ok bool, err error) 6 | } 7 | -------------------------------------------------------------------------------- /model/sliceiterator_test.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func Test_SliceIterator(t *testing.T) { 8 | { 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /static/css/ui-lightness/images/ui-icons_222222_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ungerik/go-start/HEAD/static/css/ui-lightness/images/ui-icons_222222_256x240.png -------------------------------------------------------------------------------- /static/css/ui-lightness/images/ui-icons_228ef1_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ungerik/go-start/HEAD/static/css/ui-lightness/images/ui-icons_228ef1_256x240.png -------------------------------------------------------------------------------- /static/css/ui-lightness/images/ui-icons_ef8c08_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ungerik/go-start/HEAD/static/css/ui-lightness/images/ui-icons_ef8c08_256x240.png -------------------------------------------------------------------------------- /static/css/ui-lightness/images/ui-icons_ffd27a_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ungerik/go-start/HEAD/static/css/ui-lightness/images/ui-icons_ffd27a_256x240.png -------------------------------------------------------------------------------- /static/css/ui-lightness/images/ui-icons_ffffff_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ungerik/go-start/HEAD/static/css/ui-lightness/images/ui-icons_ffffff_256x240.png -------------------------------------------------------------------------------- /static/css/ui-lightness/images/ui-bg_flat_10_000000_40x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ungerik/go-start/HEAD/static/css/ui-lightness/images/ui-bg_flat_10_000000_40x100.png -------------------------------------------------------------------------------- /static/css/ui-lightness/images/ui-bg_glass_100_f6f6f6_1x400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ungerik/go-start/HEAD/static/css/ui-lightness/images/ui-bg_glass_100_f6f6f6_1x400.png -------------------------------------------------------------------------------- /static/css/ui-lightness/images/ui-bg_glass_100_fdf5ce_1x400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ungerik/go-start/HEAD/static/css/ui-lightness/images/ui-bg_glass_100_fdf5ce_1x400.png -------------------------------------------------------------------------------- /static/css/ui-lightness/images/ui-bg_glass_65_ffffff_1x400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ungerik/go-start/HEAD/static/css/ui-lightness/images/ui-bg_glass_65_ffffff_1x400.png -------------------------------------------------------------------------------- /view/url.go: -------------------------------------------------------------------------------- 1 | package view 2 | 3 | // URL is an interface to return URL strings depending on the request path args. 4 | type URL interface { 5 | URL(ctx *Context) string 6 | } 7 | -------------------------------------------------------------------------------- /errs/variables.go: -------------------------------------------------------------------------------- 1 | package errs 2 | 3 | type Configuration struct { 4 | FormatWithCallStack bool 5 | } 6 | 7 | var Config = Configuration{ 8 | FormatWithCallStack: true, 9 | } 10 | -------------------------------------------------------------------------------- /mongo/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Wraps MongoDB to make it compatible with the model package. 3 | 4 | Built on top of Gustavo Niemeyer's great mgo package: http://labix.org/mgo 5 | 6 | */ 7 | package mongo -------------------------------------------------------------------------------- /static/css/ui-lightness/images/ui-bg_gloss-wave_35_f6a828_500x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ungerik/go-start/HEAD/static/css/ui-lightness/images/ui-bg_gloss-wave_35_f6a828_500x100.png -------------------------------------------------------------------------------- /static/css/ui-lightness/images/ui-bg_diagonals-thick_18_b81900_40x40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ungerik/go-start/HEAD/static/css/ui-lightness/images/ui-bg_diagonals-thick_18_b81900_40x40.png -------------------------------------------------------------------------------- /static/css/ui-lightness/images/ui-bg_diagonals-thick_20_666666_40x40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ungerik/go-start/HEAD/static/css/ui-lightness/images/ui-bg_diagonals-thick_20_666666_40x40.png -------------------------------------------------------------------------------- /static/css/ui-lightness/images/ui-bg_highlight-soft_100_eeeeee_1x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ungerik/go-start/HEAD/static/css/ui-lightness/images/ui-bg_highlight-soft_100_eeeeee_1x100.png -------------------------------------------------------------------------------- /static/css/ui-lightness/images/ui-bg_highlight-soft_75_ffe45c_1x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ungerik/go-start/HEAD/static/css/ui-lightness/images/ui-bg_highlight-soft_75_ffe45c_1x100.png -------------------------------------------------------------------------------- /view/viewwithurl.go: -------------------------------------------------------------------------------- 1 | package view 2 | 3 | // ViewWithURL combines the View interface with the URL interface 4 | // for views that have an URL 5 | type ViewWithURL interface { 6 | View 7 | URL 8 | SetPath(path string) 9 | } 10 | -------------------------------------------------------------------------------- /examples/CombineStaticDirs/static-dir-b/second-static-dir.html: -------------------------------------------------------------------------------- 1 | 2 |

This file is in the root of the second static directory

3 | The content of multiple static directories gets merged under / 4 |

5 | Back to / -------------------------------------------------------------------------------- /model/iteratorfunc.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | type IteratorFunc func() interface{} 4 | 5 | func (self IteratorFunc) Next() interface{} { 6 | return self() 7 | } 8 | 9 | func (self IteratorFunc) Err() error { 10 | return nil 11 | } 12 | -------------------------------------------------------------------------------- /model/emptyiterator.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | type EmptyIterator struct { 4 | } 5 | 6 | func (self *EmptyIterator) Next(resultRef interface{}) bool { 7 | return false 8 | } 9 | 10 | func (self *EmptyIterator) Err() error { 11 | return nil 12 | } 13 | -------------------------------------------------------------------------------- /examples/ViewPaths/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "view": { 3 | "ListenAndServeAt": "0.0.0.0:80", 4 | "Debug": { 5 | "ListenAndServeAt": "0.0.0.0:8080" 6 | }, 7 | "BaseDirs": [".", "../.."], 8 | "CookieSecret": "Test123" 9 | } 10 | } -------------------------------------------------------------------------------- /examples/FullTutorial/views/admin/images.go: -------------------------------------------------------------------------------- 1 | package admin 2 | 3 | import ( 4 | "github.com/ungerik/go-start/media" 5 | 6 | . "github.com/ungerik/go-start/examples/FullTutorial/views" 7 | ) 8 | 9 | func init() { 10 | Admin_Images = NewAdminPage("Images | Admin", 11 | media.ImagesAdmin(), 12 | ) 13 | } 14 | -------------------------------------------------------------------------------- /view/middleware.go: -------------------------------------------------------------------------------- 1 | package view 2 | 3 | /////////////////////////////////////////////////////////////////////////////// 4 | // Middleware 5 | 6 | type Middleware interface { 7 | PreRender(ctx *Context) (abort bool) 8 | PostRender(response *Response, html string, err error) (newHtml string, newErr error) 9 | } 10 | -------------------------------------------------------------------------------- /view/viewwithurlbase.go: -------------------------------------------------------------------------------- 1 | package view 2 | 3 | type ViewWithURLBase struct { 4 | ViewBase 5 | path string 6 | } 7 | 8 | func (self *ViewWithURLBase) SetPath(path string) { 9 | self.path = path 10 | } 11 | 12 | func (self *ViewWithURLBase) URL(ctx *Context) string { 13 | return StringURL(self.path).URL(ctx) 14 | } 15 | -------------------------------------------------------------------------------- /templatesystem/interfaces.go: -------------------------------------------------------------------------------- 1 | package templatesystem 2 | 3 | import "io" 4 | 5 | type Template interface { 6 | Render(out io.Writer, context interface{}) error 7 | } 8 | 9 | type Implementation interface { 10 | ParseFile(filename string) (Template, error) 11 | ParseString(text, name string) (Template, error) 12 | } 13 | -------------------------------------------------------------------------------- /view/notfoundview.go: -------------------------------------------------------------------------------- 1 | package view 2 | 3 | /////////////////////////////////////////////////////////////////////////////// 4 | // NotFoundView 5 | 6 | type NotFoundView struct { 7 | ViewBase 8 | Message string 9 | } 10 | 11 | func (self *NotFoundView) Render(ctx *Context) (err error) { 12 | return NotFound(self.Message) 13 | } 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ############################# 2 | # Example executables: 3 | 4 | examples/FullTutorial/FullTutorial 5 | examples/CombineStaticDirs/CombineStaticDirs 6 | examples/ServeStatic/ServeStatic 7 | examples/ViewPaths/ViewPaths 8 | 9 | ############################# 10 | # OS generated files: 11 | 12 | .DS_Store 13 | Icon? 14 | Thumbs.db 15 | -------------------------------------------------------------------------------- /examples/ServeStatic/README.md: -------------------------------------------------------------------------------- 1 | ## Serves static files in the current directory 2 | 3 | * index.html will be handled as expected 4 | * Does not list directory contents 5 | 6 | Download, build and run example: 7 | 8 | go get github.com/ungerik/go-start/examples/ServeStatic 9 | go install github.com/ungerik/go-start/examples/ServeStatic && ServeStatic 10 | -------------------------------------------------------------------------------- /sublime-snippets/go-start-dynamicview.sublime-snippet: -------------------------------------------------------------------------------- 1 | 2 | 9 | DynamicView 10 | source.go 11 | go-start view.DynamicView 12 | 13 | -------------------------------------------------------------------------------- /model/errors.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import "fmt" 4 | 5 | func NewRequiredError(metaData *MetaData) *RequiredError { 6 | return &RequiredError{metaData} 7 | } 8 | 9 | type RequiredError struct { 10 | metaData *MetaData 11 | } 12 | 13 | func (self RequiredError) Error() string { 14 | return fmt.Sprintf("Field '%s' is required", self.metaData.Selector()) 15 | } 16 | -------------------------------------------------------------------------------- /examples/CombineStaticDirs/README.md: -------------------------------------------------------------------------------- 1 | ## Serves static files combined from multiple static directories 2 | 3 | * index.html will be handled as expected 4 | * No directory listings 5 | 6 | Download, build and run example: 7 | 8 | go get github.com/ungerik/go-start/examples/CombineStaticDirs 9 | go install github.com/ungerik/go-start/examples/CombineStaticDirs && CombineStaticDirs 10 | -------------------------------------------------------------------------------- /model/erroronlyiterator.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | func NewErrorOnlyIterator(err error) Iterator { 4 | return &ErrorOnlyIterator{err} 5 | } 6 | 7 | type ErrorOnlyIterator struct { 8 | err error 9 | } 10 | 11 | func (self *ErrorOnlyIterator) Next(resultRef interface{}) bool { 12 | return false 13 | } 14 | 15 | func (self *ErrorOnlyIterator) Err() error { 16 | return self.err 17 | } 18 | -------------------------------------------------------------------------------- /utils/experimental.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | func IntRange(start int, end int) <-chan int { 4 | return IntRangeStep(start, end, 1) 5 | } 6 | 7 | func IntRangeStep(start int, end int, step int) <-chan int { 8 | result := make(chan int) 9 | go func() { 10 | for i := start; i < end; i += step { 11 | result <- i 12 | } 13 | close(result) 14 | }() 15 | return result 16 | } 17 | -------------------------------------------------------------------------------- /model/config.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | var StructTagKey = "model" 4 | 5 | var Config Configuration 6 | 7 | type Configuration struct { 8 | Debug bool 9 | } 10 | 11 | func (self *Configuration) Name() string { 12 | return "model" 13 | } 14 | 15 | func (self *Configuration) Init() error { 16 | return nil 17 | } 18 | 19 | func (self *Configuration) Close() error { 20 | return nil 21 | } 22 | -------------------------------------------------------------------------------- /model/filteriterator.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | type FilterFunc func(resultRef interface{}) bool 4 | 5 | type FilterIterator struct { 6 | Iterator 7 | PassFilter FilterFunc 8 | } 9 | 10 | func (self *FilterIterator) Next(resultRef interface{}) bool { 11 | for self.Iterator.Next(resultRef) { 12 | if self.PassFilter(resultRef) { 13 | return true 14 | } 15 | } 16 | return false 17 | } 18 | -------------------------------------------------------------------------------- /i18n/languages.go: -------------------------------------------------------------------------------- 1 | package i18n 2 | 3 | // This can be used as independent library 4 | 5 | var iso639_1 map[string]string 6 | 7 | func EnglishLanguageName(code string) string { 8 | name, ok := Languages()[code] 9 | if !ok { 10 | return code 11 | } 12 | return name 13 | } 14 | 15 | // ISO 639-1 16 | func Languages() map[string]string { 17 | if iso639_1 == nil { 18 | } 19 | return iso639_1 20 | } 21 | -------------------------------------------------------------------------------- /view/comment.go: -------------------------------------------------------------------------------- 1 | package view 2 | 3 | type Comment string 4 | 5 | func (self Comment) Init(thisView View) { 6 | } 7 | 8 | func (self Comment) ID() string { 9 | return "" 10 | } 11 | 12 | func (self Comment) IterateChildren(callback IterateChildrenCallback) { 13 | } 14 | 15 | func (self Comment) Render(ctx *Context) (err error) { 16 | ctx.Response.Printf("", self) 17 | return nil 18 | } 19 | -------------------------------------------------------------------------------- /view/stringurl.go: -------------------------------------------------------------------------------- 1 | package view 2 | 3 | import "strings" 4 | 5 | // StringURL implements the URL interface for a URL string. 6 | type StringURL string 7 | 8 | func (self StringURL) URL(ctx *Context) string { 9 | url := string(self) 10 | for _, arg := range ctx.URLArgs { 11 | url = strings.Replace(url, PathFragmentPattern, arg, 1) 12 | } 13 | return ctx.Request.AddProtocolAndHostToURL(url) 14 | } 15 | -------------------------------------------------------------------------------- /sublime-snippets/go-start-modeliterator.sublime-snippet: -------------------------------------------------------------------------------- 1 | 2 | 11 | iter 12 | source.go 13 | go-start model.Iterator 14 | 15 | -------------------------------------------------------------------------------- /examples/FullTutorial/views/root/css.go: -------------------------------------------------------------------------------- 1 | package root 2 | 3 | import ( 4 | . "github.com/ungerik/go-start/view" 5 | 6 | "github.com/ungerik/go-start/examples/FullTutorial/models" 7 | . "github.com/ungerik/go-start/examples/FullTutorial/views" 8 | ) 9 | 10 | func init() { 11 | CSS = NewHTML5BoilerplateCSSTemplate( 12 | TemplateContext(models.NewColorScheme()), 13 | "css/common.css", 14 | "css/style.css", 15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /utils/float.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "math" 5 | ) 6 | 7 | func Round(val float64, prec int) float64 { 8 | 9 | var rounder float64 10 | intermed := val * math.Pow(10, float64(prec)) 11 | 12 | if val >= 0.5 { 13 | rounder = math.Ceil(intermed) 14 | } else { 15 | rounder = math.Floor(intermed) 16 | } 17 | 18 | return rounder / math.Pow(10, float64(prec)) 19 | 20 | } -------------------------------------------------------------------------------- /examples/ViewPaths/views/root/getjson.go: -------------------------------------------------------------------------------- 1 | package root 2 | 3 | import ( 4 | . "github.com/ungerik/go-start/view" 5 | 6 | "github.com/ungerik/go-start/examples/ViewPaths/views" 7 | ) 8 | 9 | func init() { 10 | views.GetJSON = NewViewURLWrapper(RenderView( 11 | func(ctx *Context) error { 12 | ctx.Response.SetContentTypeByExt(".json") 13 | ctx.Response.WriteString(`{"data": "Hello World!"}`) 14 | return nil 15 | }, 16 | )) 17 | } 18 | -------------------------------------------------------------------------------- /view/redirectview.go: -------------------------------------------------------------------------------- 1 | package view 2 | 3 | /////////////////////////////////////////////////////////////////////////////// 4 | // RedirectView 5 | 6 | // If rendered, this view will cause a HTTP redirect. 7 | type RedirectView struct { 8 | ViewBase 9 | URL string 10 | Permanent bool 11 | } 12 | 13 | func (self *RedirectView) Render(ctx *Context) (err error) { 14 | if self.Permanent { 15 | return PermanentRedirect(self.URL) 16 | } 17 | return Redirect(self.URL) 18 | } 19 | -------------------------------------------------------------------------------- /view/urlwithargs.go: -------------------------------------------------------------------------------- 1 | package view 2 | 3 | func NewURLWithArgs(url URL, args ...string) *URLWithArgs { 4 | return &URLWithArgs{ 5 | Url: url, 6 | Args: args, 7 | } 8 | } 9 | 10 | // URLWithArgs binds Args to an URL. 11 | // Url.URL() will be called with response.URLArgs(Args...) 12 | type URLWithArgs struct { 13 | Url URL 14 | Args []string 15 | } 16 | 17 | func (self *URLWithArgs) URL(ctx *Context) string { 18 | return self.Url.URL(ctx.ForURLArgs(self.Args...)) 19 | } 20 | -------------------------------------------------------------------------------- /view/html.go: -------------------------------------------------------------------------------- 1 | package view 2 | 3 | /////////////////////////////////////////////////////////////////////////////// 4 | // HTML 5 | 6 | type HTML string 7 | 8 | func (self HTML) Init(thisView View) { 9 | } 10 | 11 | func (self HTML) ID() string { 12 | return "" 13 | } 14 | 15 | func (self HTML) IterateChildren(callback IterateChildrenCallback) { 16 | } 17 | 18 | func (self HTML) Render(ctx *Context) (err error) { 19 | ctx.Response.WriteString(string(self)) 20 | return nil 21 | } 22 | -------------------------------------------------------------------------------- /view/errors.go: -------------------------------------------------------------------------------- 1 | package view 2 | 3 | type Redirect string 4 | 5 | func (self Redirect) Error() string { 6 | return string(self) 7 | } 8 | 9 | type PermanentRedirect string 10 | 11 | func (self PermanentRedirect) Error() string { 12 | return string(self) 13 | } 14 | 15 | type NotFound string 16 | 17 | func (self NotFound) Error() string { 18 | return string(self) 19 | } 20 | 21 | type Forbidden string 22 | 23 | func (self Forbidden) Error() string { 24 | return string(self) 25 | } 26 | -------------------------------------------------------------------------------- /view/format.go: -------------------------------------------------------------------------------- 1 | package view 2 | 3 | /////////////////////////////////////////////////////////////////////////////// 4 | // Format 5 | 6 | type Format struct { 7 | ViewBase 8 | Text string 9 | Args []interface{} 10 | Escape bool 11 | } 12 | 13 | func (self *Format) Render(ctx *Context) (err error) { 14 | if self.Escape { 15 | ctx.Response.XML.PrintfEscape(self.Text, self.Args...) 16 | } else { 17 | ctx.Response.XML.Printf(self.Text, self.Args...) 18 | } 19 | return nil 20 | } 21 | -------------------------------------------------------------------------------- /view/canvas.go: -------------------------------------------------------------------------------- 1 | package view 2 | 3 | type Canvas struct { 4 | ViewBaseWithId 5 | Class string 6 | Width int 7 | Height int 8 | } 9 | 10 | func (self *Canvas) Render(ctx *Context) (err error) { 11 | ctx.Response.XML.OpenTag("label") 12 | ctx.Response.XML.AttribIfNotDefault("id", self.id) 13 | ctx.Response.XML.AttribIfNotDefault("class", self.Class) 14 | ctx.Response.XML.Attrib("width", self.Width).Attrib("height", self.Height) 15 | ctx.Response.XML.CloseTagAlways() 16 | return err 17 | } 18 | -------------------------------------------------------------------------------- /model/validator.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "reflect" 5 | ) 6 | 7 | type Validator interface { 8 | // Validate returns an error if metaData is not valid. 9 | // In case of multiple errors errs.ErrSlice is returned. 10 | Validate(metaData *MetaData) error 11 | } 12 | 13 | var ValidatorType = reflect.TypeOf((*Validator)(nil)).Elem() 14 | 15 | func IsValidator(v reflect.Value) bool { 16 | return v.Type().Implements(ValidatorType) || (v.CanAddr() && v.Addr().Type().Implements(ValidatorType)) 17 | } 18 | -------------------------------------------------------------------------------- /examples/FullTutorial/models/colorscheme.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "github.com/ungerik/go-start/model" 5 | "github.com/ungerik/go-start/mongo" 6 | ) 7 | 8 | var ColorSchemes = mongo.NewCollection("colorschemes") 9 | 10 | func NewColorScheme() *ColorScheme { 11 | return &ColorScheme{ 12 | Primary: "blue", 13 | Secondary: "black", 14 | } 15 | } 16 | 17 | type ColorScheme struct { 18 | mongo.DocumentBase `bson:",inline"` 19 | 20 | Primary model.String 21 | Secondary model.String 22 | } 23 | -------------------------------------------------------------------------------- /mongo/query_filterref.go: -------------------------------------------------------------------------------- 1 | package mongo 2 | 3 | import ( 4 | "labix.org/v2/mgo/bson" 5 | ) 6 | 7 | /////////////////////////////////////////////////////////////////////////////// 8 | // query_filterRef 9 | 10 | type query_filterRef struct { 11 | query_filterBase 12 | selector string 13 | refs []Ref 14 | } 15 | 16 | func (self *query_filterRef) bsonSelector() bson.M { 17 | return bson.M{self.selector: bson.M{"$in": self.refs}} 18 | } 19 | 20 | func (self *query_filterRef) Selector() string { 21 | return self.selector 22 | } 23 | -------------------------------------------------------------------------------- /mongo/query_skip.go: -------------------------------------------------------------------------------- 1 | package mongo 2 | 3 | import "labix.org/v2/mgo" 4 | 5 | /////////////////////////////////////////////////////////////////////////////// 6 | // query_skip 7 | 8 | type query_skip struct { 9 | query_base 10 | skip int 11 | } 12 | 13 | func (self *query_skip) mongoQuery() (q *mgo.Query, err error) { 14 | q, err = self.parentQuery.mongoQuery() 15 | if err != nil { 16 | return nil, err 17 | } 18 | return q.Skip(self.skip), nil 19 | } 20 | 21 | func (self *query_skip) Selector() string { 22 | return "" 23 | } 24 | -------------------------------------------------------------------------------- /mongo/query_filterin.go: -------------------------------------------------------------------------------- 1 | package mongo 2 | 3 | import ( 4 | "labix.org/v2/mgo/bson" 5 | ) 6 | 7 | /////////////////////////////////////////////////////////////////////////////// 8 | // query_filterIn 9 | 10 | type query_filterIn struct { 11 | query_filterBase 12 | selector string 13 | values []interface{} 14 | } 15 | 16 | func (self *query_filterIn) bsonSelector() bson.M { 17 | return bson.M{self.selector: bson.M{"$in": self.values}} 18 | } 19 | 20 | func (self *query_filterIn) Selector() string { 21 | return self.selector 22 | } 23 | -------------------------------------------------------------------------------- /examples/FullTutorial/views/root/homepage.go: -------------------------------------------------------------------------------- 1 | package root 2 | 3 | import ( 4 | "time" 5 | 6 | . "github.com/ungerik/go-start/view" 7 | 8 | // "github.com/ungerik/go-start/examples/FullTutorial/models" 9 | . "github.com/ungerik/go-start/examples/FullTutorial/views" 10 | ) 11 | 12 | const PublicPageCacheDuration = time.Hour 13 | 14 | func init() { 15 | Homepage = CacheView(PublicPageCacheDuration, NewPublicPage("go-start Tutorial", 16 | DIV("main", 17 | H1("Hello World from go-start"), 18 | DivClearBoth(), 19 | ), 20 | )) 21 | } 22 | -------------------------------------------------------------------------------- /mongo/query_filterless.go: -------------------------------------------------------------------------------- 1 | package mongo 2 | 3 | import ( 4 | "labix.org/v2/mgo/bson" 5 | ) 6 | 7 | /////////////////////////////////////////////////////////////////////////////// 8 | // query_filterLess 9 | 10 | type query_filterLess struct { 11 | query_filterBase 12 | selector string 13 | value interface{} 14 | } 15 | 16 | func (self *query_filterLess) bsonSelector() bson.M { 17 | return bson.M{self.selector: bson.M{"$lt": self.value}} 18 | } 19 | 20 | func (self *query_filterLess) Selector() string { 21 | return self.selector 22 | } 23 | -------------------------------------------------------------------------------- /mongo/query_limit.go: -------------------------------------------------------------------------------- 1 | package mongo 2 | 3 | import "labix.org/v2/mgo" 4 | 5 | /////////////////////////////////////////////////////////////////////////////// 6 | // query_limit 7 | 8 | type query_limit struct { 9 | query_base 10 | limit int 11 | } 12 | 13 | func (self *query_limit) mongoQuery() (q *mgo.Query, err error) { 14 | q, err = self.parentQuery.mongoQuery() 15 | if err != nil { 16 | return nil, err 17 | } 18 | return q.Limit(self.limit), nil 19 | } 20 | 21 | func (self *query_limit) Selector() string { 22 | return "" 23 | } 24 | -------------------------------------------------------------------------------- /mongo/query_or.go: -------------------------------------------------------------------------------- 1 | package mongo 2 | 3 | import ( 4 | "github.com/ungerik/go-start/errs" 5 | "labix.org/v2/mgo" 6 | ) 7 | 8 | /////////////////////////////////////////////////////////////////////////////// 9 | // query_or 10 | 11 | type query_or struct { 12 | query_base 13 | } 14 | 15 | func (self *query_or) mongoQuery() (q *mgo.Query, err error) { 16 | return nil, errs.Format("Can't create a mongo query. query_or needs a query_filterEqual chained after it") 17 | } 18 | 19 | func (self *query_or) Selector() string { 20 | return "" 21 | } 22 | -------------------------------------------------------------------------------- /media/viewpath.go: -------------------------------------------------------------------------------- 1 | package media 2 | 3 | import ( 4 | "github.com/ungerik/go-start/view" 5 | ) 6 | 7 | // ViewPath returns the view.ViewPath for all media URLs. 8 | func ViewPath(name string) view.ViewPath { 9 | return view.ViewPath{Name: name, Sub: []view.ViewPath{ 10 | {Name: "file", Args: 2, View: FileView}, 11 | {Name: "upload-blob", View: UploadBlob}, 12 | {Name: "upload-image", Args: 1, View: UploadImage}, 13 | {Name: "thumbnails.json", Args: 1, View: API.AllThumbnails}, 14 | // {Name: "blobs.json", View: API.AllBlobs}, 15 | }} 16 | } 17 | -------------------------------------------------------------------------------- /mongo/query_filternotin.go: -------------------------------------------------------------------------------- 1 | package mongo 2 | 3 | import ( 4 | "labix.org/v2/mgo/bson" 5 | ) 6 | 7 | /////////////////////////////////////////////////////////////////////////////// 8 | // query_filterNotIn 9 | 10 | type query_filterNotIn struct { 11 | query_filterBase 12 | selector string 13 | values []interface{} 14 | } 15 | 16 | func (self *query_filterNotIn) bsonSelector() bson.M { 17 | return bson.M{self.selector: bson.M{"$nin": self.values}} 18 | } 19 | 20 | func (self *query_filterNotIn) Selector() string { 21 | return self.selector 22 | } 23 | -------------------------------------------------------------------------------- /mongo/query_filtergreater.go: -------------------------------------------------------------------------------- 1 | package mongo 2 | 3 | import ( 4 | "labix.org/v2/mgo/bson" 5 | ) 6 | 7 | /////////////////////////////////////////////////////////////////////////////// 8 | // query_filterGreater 9 | 10 | type query_filterGreater struct { 11 | query_filterBase 12 | selector string 13 | value interface{} 14 | } 15 | 16 | func (self *query_filterGreater) bsonSelector() bson.M { 17 | return bson.M{self.selector: bson.M{"$gt": self.value}} 18 | } 19 | 20 | func (self *query_filterGreater) Selector() string { 21 | return self.selector 22 | } 23 | -------------------------------------------------------------------------------- /mongo/query_filternotequal.go: -------------------------------------------------------------------------------- 1 | package mongo 2 | 3 | import ( 4 | "labix.org/v2/mgo/bson" 5 | ) 6 | 7 | /////////////////////////////////////////////////////////////////////////////// 8 | // query_filterNotEqual 9 | 10 | type query_filterNotEqual struct { 11 | query_filterBase 12 | selector string 13 | value interface{} 14 | } 15 | 16 | func (self *query_filterNotEqual) bsonSelector() bson.M { 17 | return bson.M{self.selector: bson.M{"$ne": self.value}} 18 | } 19 | 20 | func (self *query_filterNotEqual) Selector() string { 21 | return self.selector 22 | } 23 | -------------------------------------------------------------------------------- /mongo/query_filterlessequal.go: -------------------------------------------------------------------------------- 1 | package mongo 2 | 3 | import ( 4 | "labix.org/v2/mgo/bson" 5 | ) 6 | 7 | /////////////////////////////////////////////////////////////////////////////// 8 | // query_filterLessEqual 9 | 10 | type query_filterLessEqual struct { 11 | query_filterBase 12 | selector string 13 | value interface{} 14 | } 15 | 16 | func (self *query_filterLessEqual) bsonSelector() bson.M { 17 | return bson.M{self.selector: bson.M{"$lte": self.value}} 18 | } 19 | 20 | func (self *query_filterLessEqual) Selector() string { 21 | return self.selector 22 | } 23 | -------------------------------------------------------------------------------- /model/limitediterator.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | func NewLimitedIterator(iter Iterator, limit int) *LimitedIterator { 4 | return &LimitedIterator{Iterator: iter, limit: limit} 5 | } 6 | 7 | /////////////////////////////////////////////////////////////////////////////// 8 | // LimitedIterator 9 | 10 | type LimitedIterator struct { 11 | Iterator 12 | limit int 13 | index int 14 | } 15 | 16 | func (self *LimitedIterator) Next(resultRef interface{}) bool { 17 | if self.index >= self.limit { 18 | return false 19 | } 20 | self.index++ 21 | return self.Iterator.Next(resultRef) 22 | } 23 | -------------------------------------------------------------------------------- /mongo/query_filtergreaterequal.go: -------------------------------------------------------------------------------- 1 | package mongo 2 | 3 | import ( 4 | "labix.org/v2/mgo/bson" 5 | ) 6 | 7 | /////////////////////////////////////////////////////////////////////////////// 8 | // query_filterGreaterEqual 9 | 10 | type query_filterGreaterEqual struct { 11 | query_filterBase 12 | selector string 13 | value interface{} 14 | } 15 | 16 | func (self *query_filterGreaterEqual) bsonSelector() bson.M { 17 | return bson.M{self.selector: bson.M{"$gte": self.value}} 18 | } 19 | 20 | func (self *query_filterGreaterEqual) Selector() string { 21 | return self.selector 22 | } 23 | -------------------------------------------------------------------------------- /examples/FullTutorial/views/admin/admin.go: -------------------------------------------------------------------------------- 1 | package admin 2 | 3 | import ( 4 | . "github.com/ungerik/go-start/view" 5 | 6 | . "github.com/ungerik/go-start/examples/FullTutorial/views" 7 | ) 8 | 9 | func init() { 10 | Admin = NewAdminPage("Admin Dashboard", 11 | &Form{ 12 | FormID: "clearcaches", 13 | SubmitButtonText: "Clear Page Caches", 14 | SubmitButtonClass: "button", 15 | OnSubmit: func(form *Form, formModel interface{}, ctx *Context) (string, URL, error) { 16 | ClearAllCaches() 17 | return "All caches cleared", nil, nil 18 | }, 19 | }, 20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /mongo/subdocument.go: -------------------------------------------------------------------------------- 1 | package mongo 2 | 3 | // import ( 4 | // "labix.org/v2/mgo/bson" 5 | // "github.com/ungerik/go-start/model" 6 | // ) 7 | 8 | // /////////////////////////////////////////////////////////////////////////////// 9 | // // SubDocument 10 | 11 | // type SubDocument interface { 12 | // Init(collection *Collection, selector string, embeddingStruct interface{}) 13 | // Collection() *Collection 14 | // RootDocumentObjectId() bson.ObjectId 15 | // RootDocumentSetObjectId(id bson.ObjectId) 16 | // Iterator() model.Iterator 17 | // Save() error 18 | // RemoveRootDocument() error 19 | // } 20 | -------------------------------------------------------------------------------- /view/error.go: -------------------------------------------------------------------------------- 1 | package view 2 | 3 | func NewError(err error) *Error { 4 | return &Error{Err: err} 5 | } 6 | 7 | // Error wraps an error and returns it at the Render() method. 8 | type Error struct { 9 | Err error 10 | } 11 | 12 | func (self *Error) Init(thisView View) { 13 | } 14 | 15 | func (self *Error) ID() string { 16 | return "" 17 | } 18 | 19 | func (self *Error) IterateChildren(callback IterateChildrenCallback) { 20 | } 21 | 22 | func (self *Error) Render(ctx *Context) (err error) { 23 | return self.Err 24 | } 25 | 26 | func (self *Error) Error() string { 27 | return self.Err.Error() 28 | } 29 | -------------------------------------------------------------------------------- /mongo/query_filtermodulo.go: -------------------------------------------------------------------------------- 1 | package mongo 2 | 3 | import ( 4 | "labix.org/v2/mgo/bson" 5 | ) 6 | 7 | /////////////////////////////////////////////////////////////////////////////// 8 | // query_filterModulo 9 | 10 | type query_filterModulo struct { 11 | query_filterBase 12 | selector string 13 | divisor interface{} 14 | result interface{} 15 | } 16 | 17 | func (self *query_filterModulo) bsonSelector() bson.M { 18 | return bson.M{self.selector: bson.M{"$mod": []interface{}{self.divisor, self.result}}} 19 | } 20 | 21 | func (self *query_filterModulo) Selector() string { 22 | return self.selector 23 | } 24 | -------------------------------------------------------------------------------- /mongomedia/blobdoc.go: -------------------------------------------------------------------------------- 1 | package mongomedia 2 | 3 | import ( 4 | "github.com/ungerik/go-start/media" 5 | "github.com/ungerik/go-start/mongo" 6 | ) 7 | 8 | type BlobDoc struct { 9 | mongo.DocumentBase `bson:",inline"` 10 | media.Blob `bson:",inline"` 11 | } 12 | 13 | func (self *BlobDoc) Init(collection *mongo.Collection, embeddingStructPtr interface{}) { 14 | self.DocumentBase.Init(collection, embeddingStructPtr) 15 | } 16 | 17 | func (self *BlobDoc) Save() error { 18 | return self.DocumentBase.Save() 19 | } 20 | 21 | func (self *BlobDoc) Delete() error { 22 | return self.DocumentBase.Delete() 23 | } 24 | -------------------------------------------------------------------------------- /mongomedia/imagedoc.go: -------------------------------------------------------------------------------- 1 | package mongomedia 2 | 3 | import ( 4 | "github.com/ungerik/go-start/media" 5 | "github.com/ungerik/go-start/mongo" 6 | ) 7 | 8 | type ImageDoc struct { 9 | mongo.DocumentBase `bson:",inline"` 10 | media.Image `bson:",inline"` 11 | } 12 | 13 | func (self *ImageDoc) Init(collection *mongo.Collection, embeddingStructPtr interface{}) { 14 | self.DocumentBase.Init(collection, embeddingStructPtr) 15 | } 16 | 17 | func (self *ImageDoc) Save() error { 18 | return self.DocumentBase.Save() 19 | } 20 | 21 | func (self *ImageDoc) Delete() error { 22 | return self.DocumentBase.Delete() 23 | } 24 | -------------------------------------------------------------------------------- /view/viewbasewithid.go: -------------------------------------------------------------------------------- 1 | package view 2 | 3 | /////////////////////////////////////////////////////////////////////////////// 4 | // ViewBaseWithId 5 | 6 | // ViewBaseWithId extends ViewBase with an id for the view. 7 | type ViewBaseWithId struct { 8 | ViewBase 9 | id string 10 | } 11 | 12 | func (self *ViewBaseWithId) Init(thisView View) { 13 | if thisView == self.thisView { 14 | return // already initialized 15 | } 16 | self.ViewBase.Init(thisView) 17 | } 18 | 19 | func (self *ViewBaseWithId) ID() string { 20 | if self.id == "" { 21 | self.id = NewViewID(self.thisView) 22 | } 23 | return self.id 24 | } 25 | -------------------------------------------------------------------------------- /media/modelrect.go: -------------------------------------------------------------------------------- 1 | package media 2 | 3 | import ( 4 | "image" 5 | "github.com/ungerik/go-start/model" 6 | ) 7 | 8 | type ModelRect struct { 9 | MinX model.Int 10 | MinY model.Int 11 | MaxX model.Int 12 | MaxY model.Int 13 | } 14 | 15 | func (self *ModelRect) Rectangle() image.Rectangle { 16 | return image.Rect(self.MinX.GetInt(), self.MinY.GetInt(), self.MaxX.GetInt(), self.MaxY.GetInt()) 17 | } 18 | 19 | func (self *ModelRect) SetRectangle(r image.Rectangle) { 20 | self.MinX = model.Int(r.Min.X) 21 | self.MinY = model.Int(r.Min.Y) 22 | self.MaxX = model.Int(r.Max.X) 23 | self.MaxY = model.Int(r.Max.Y) 24 | } 25 | -------------------------------------------------------------------------------- /model/value.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "reflect" 5 | ) 6 | 7 | type Value interface { 8 | String() string 9 | // SetString returns only error from converting str to the 10 | // underlying value type. 11 | // It does not return validation errors of the converted value. 12 | SetString(str string) (strconvErr error) 13 | IsEmpty() bool 14 | Required(metaData *MetaData) bool 15 | Validator 16 | } 17 | 18 | var ValueType = reflect.TypeOf((*Value)(nil)).Elem() 19 | 20 | func IsValue(v reflect.Value) bool { 21 | return v.Type().Implements(ValueType) || (v.CanAddr() && v.Addr().Type().Implements(ValueType)) 22 | } 23 | -------------------------------------------------------------------------------- /examples/CombineStaticDirs/main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Download, build and run example: 3 | 4 | go get github.com/ungerik/go-start/examples/CombineStaticDirs 5 | go install github.com/ungerik/go-start/examples/CombineStaticDirs && CombineStaticDirs 6 | 7 | */ 8 | package main 9 | 10 | import ( 11 | "fmt" 12 | "github.com/ungerik/go-start/view" 13 | ) 14 | 15 | func main() { 16 | fmt.Println("Try http://0.0.0.0:8080/ -> /index.html will work as expected\n") 17 | 18 | // The content of multiple static directories gets merged under / 19 | view.Config.StaticDirs = []string{"static-dir-a", "static-dir-b"} 20 | view.RunServerAddr("0.0.0.0:8080", nil) 21 | } 22 | -------------------------------------------------------------------------------- /examples/ViewPaths/views/root/getxml.go: -------------------------------------------------------------------------------- 1 | package root 2 | 3 | import ( 4 | . "github.com/ungerik/go-start/view" 5 | 6 | "github.com/ungerik/go-start/examples/ViewPaths/views" 7 | ) 8 | 9 | func init() { 10 | views.GetXML = NewViewURLWrapper(RenderView( 11 | func(ctx *Context) error { 12 | ctx.Response.SetContentTypeByExt(".xml") 13 | // ctx.Response.XML is convenience writer for XML and HTML 14 | ctx.Response.XML.WriteXMLDeclaration() 15 | ctx.Response.XML.OpenTag("xml") 16 | ctx.Response.XML.OpenTag("data").Content("Hello World!").CloseTag() 17 | ctx.Response.XML.CloseTag() // xml 18 | return nil 19 | }, 20 | )) 21 | } 22 | -------------------------------------------------------------------------------- /mongo/query_filterexists.go: -------------------------------------------------------------------------------- 1 | package mongo 2 | 3 | import ( 4 | "labix.org/v2/mgo/bson" 5 | ) 6 | 7 | /////////////////////////////////////////////////////////////////////////////// 8 | // query_filterExists 9 | 10 | type query_filterExists struct { 11 | query_filterBase 12 | selector string 13 | exists bool 14 | } 15 | 16 | func (self *query_filterExists) bsonSelector() bson.M { 17 | return bson.M{self.selector: bson.M{"$exists": self.exists}} 18 | } 19 | 20 | func (self *query_filterExists) Selector() string { 21 | return self.selector 22 | } 23 | 24 | //func (self *query_filterExists) Exists() bool { 25 | // return self.exists 26 | //} 27 | -------------------------------------------------------------------------------- /view/fileinput.go: -------------------------------------------------------------------------------- 1 | package view 2 | 3 | type FileInput struct { 4 | ViewBaseWithId 5 | Class string 6 | Name string 7 | Accept string 8 | Disabled bool 9 | } 10 | 11 | func (self *FileInput) Render(ctx *Context) (err error) { 12 | ctx.Response.XML.OpenTag("input") 13 | ctx.Response.XML.AttribIfNotDefault("id", self.id) 14 | ctx.Response.XML.AttribIfNotDefault("class", self.Class) 15 | ctx.Response.XML.Attrib("type", "file").Attrib("name", self.Name) 16 | ctx.Response.XML.AttribIfNotDefault("accept", self.Accept) 17 | ctx.Response.XML.AttribFlag("disabled", self.Disabled) 18 | ctx.Response.XML.CloseTag() 19 | return err 20 | } 21 | -------------------------------------------------------------------------------- /user/auth.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | import "github.com/ungerik/go-start/view" 4 | 5 | /////////////////////////////////////////////////////////////////////////////// 6 | // Auth 7 | 8 | type Auth struct { 9 | LoginURL view.URL 10 | } 11 | 12 | func (self *Auth) Authenticate(ctx *view.Context) (ok bool, err error) { 13 | id := ctx.Session.ID() 14 | if id == "" { 15 | return false, nil 16 | } 17 | 18 | /// todo: Use hashed logged in cookie instead of user ID!! 19 | ok, err = IsConfirmedUserID(id) 20 | if !ok && err == nil && self.LoginURL != nil { 21 | ctx.Response.RedirectTemporary302(auth.LoginURL.URL(ctx)) 22 | } 23 | return ok, err 24 | } 25 | -------------------------------------------------------------------------------- /examples/ViewPaths/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/ungerik/go-start/config" 5 | "github.com/ungerik/go-start/view" 6 | 7 | "github.com/ungerik/go-start/examples/ViewPaths/views" 8 | 9 | // Dummy-import all views sub-packages for initialization: 10 | _ "github.com/ungerik/go-start/examples/ViewPaths/views/admin" 11 | _ "github.com/ungerik/go-start/examples/ViewPaths/views/admin/user_0" 12 | _ "github.com/ungerik/go-start/examples/ViewPaths/views/root" 13 | ) 14 | 15 | func main() { 16 | defer config.Close() // Close all packages on exit 17 | config.Load("config.json", &view.Config) 18 | view.RunServer(views.Paths()) 19 | } 20 | -------------------------------------------------------------------------------- /mongo/query_filterallin.go: -------------------------------------------------------------------------------- 1 | package mongo 2 | 3 | import ( 4 | "labix.org/v2/mgo/bson" 5 | ) 6 | 7 | /////////////////////////////////////////////////////////////////////////////// 8 | // query_filterAllIn 9 | 10 | type query_filterAllIn struct { 11 | query_filterBase 12 | selector string 13 | values []interface{} 14 | } 15 | 16 | func (self *query_filterAllIn) bsonSelector() bson.M { 17 | return bson.M{self.selector: bson.M{"$all": self.values}} 18 | } 19 | 20 | func (self *query_filterAllIn) Selector() string { 21 | return self.selector 22 | } 23 | 24 | //func (self *query_filterAllIn) Values() []interface{} { 25 | // return self.values 26 | //} 27 | -------------------------------------------------------------------------------- /mongo/query_filterarraysize.go: -------------------------------------------------------------------------------- 1 | package mongo 2 | 3 | import ( 4 | "labix.org/v2/mgo/bson" 5 | ) 6 | 7 | /////////////////////////////////////////////////////////////////////////////// 8 | // query_filterArraySize 9 | 10 | type query_filterArraySize struct { 11 | query_filterBase 12 | selector string 13 | size int 14 | } 15 | 16 | func (self *query_filterArraySize) bsonSelector() bson.M { 17 | return bson.M{self.selector: bson.M{"$size": self.size}} 18 | } 19 | 20 | func (self *query_filterArraySize) Selector() string { 21 | return self.selector 22 | } 23 | 24 | //func (self *query_filterArraySize) Size() int { 25 | // return self.size 26 | //} 27 | -------------------------------------------------------------------------------- /examples/FullTutorial/views/admin/auth.go: -------------------------------------------------------------------------------- 1 | package admin 2 | 3 | import ( 4 | "github.com/ungerik/go-start/user" 5 | . "github.com/ungerik/go-start/view" 6 | 7 | "github.com/ungerik/go-start/examples/FullTutorial/models" 8 | . "github.com/ungerik/go-start/examples/FullTutorial/views" 9 | ) 10 | 11 | type Admin_Authenticator struct{} 12 | 13 | func (self *Admin_Authenticator) Authenticate(ctx *Context) (ok bool, err error) { 14 | var u models.User 15 | found, err := user.OfSession(ctx.Session, &u) 16 | if !found { 17 | return false, err 18 | } 19 | return u.Admin.Get(), nil 20 | } 21 | 22 | func init() { 23 | Admin_Auth = new(Admin_Authenticator) 24 | } 25 | -------------------------------------------------------------------------------- /model/file.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | type File struct { 4 | Name string 5 | Data []byte 6 | } 7 | 8 | func (self *File) String() string { 9 | return self.Name 10 | } 11 | 12 | func (self *File) SetString(str string) error { 13 | self.Name = str 14 | return nil 15 | } 16 | 17 | func (self *File) IsEmpty() bool { 18 | return len(self.Data) == 0 19 | } 20 | 21 | func (self *File) Required(metaData *MetaData) bool { 22 | return metaData.BoolAttrib(StructTagKey, "required") 23 | } 24 | 25 | func (self *File) Validate(metaData *MetaData) error { 26 | if self.Required(metaData) && self.IsEmpty() { 27 | return NewRequiredError(metaData) 28 | } 29 | return nil 30 | } 31 | -------------------------------------------------------------------------------- /mongo/query_subdocument.go: -------------------------------------------------------------------------------- 1 | package mongo 2 | 3 | import ( 4 | "labix.org/v2/mgo/bson" 5 | ) 6 | 7 | /////////////////////////////////////////////////////////////////////////////// 8 | // query_subDocument 9 | 10 | type query_subDocument struct { 11 | query_filterBase 12 | selector string 13 | } 14 | 15 | func (self *query_subDocument) subDocumentSelector() string { 16 | return self.selector 17 | } 18 | 19 | func (self *query_subDocument) bsonSelector() bson.M { 20 | return bson.M{self.selector: 1} 21 | } 22 | 23 | func (self *query_subDocument) Selector() string { 24 | return "" // Empty because the self.selector is already returned by subDocumentSelector() 25 | } 26 | -------------------------------------------------------------------------------- /states/basestate.go: -------------------------------------------------------------------------------- 1 | package states 2 | 3 | 4 | type BaseState struct { 5 | name string 6 | stateMachine *StateMachine 7 | } 8 | 9 | 10 | func (self *BaseState) SetStateMachine(parent *StateMachine) { 11 | self.stateMachine = parent 12 | } 13 | 14 | 15 | func (self *BaseState) StateMachine() *StateMachine { 16 | return self.stateMachine 17 | } 18 | 19 | 20 | func (self *BaseState) Name() string { 21 | return self.name 22 | } 23 | 24 | 25 | func (self *BaseState) Enter(*Transition) { 26 | panic("Enter() of state not implemented") 27 | } 28 | 29 | 30 | func (self *BaseState) Leave(*Transition) { 31 | panic("Leave() of state not implemented") 32 | } 33 | -------------------------------------------------------------------------------- /media/static/media/media.js: -------------------------------------------------------------------------------- 1 | var gostart_media = gostart_media || {}; 2 | 3 | gostart_media.fillChooseDialog = function(thumbnailsSelector, thumbnailsURL, onClickFunc) { 4 | var thumbnails = jQuery(thumbnailsSelector); 5 | thumbnails.empty(); 6 | jQuery.ajax({url: thumbnailsURL, dataType: "json"}) 7 | .fail(function(jqXHR, textStatus) { 8 | alert("Request failed: " + textStatus); 9 | }) 10 | .done(function(data) { 11 | jQuery.each(data, function(index, value) { 12 | var img = jQuery(''+value.title+''); 13 | img.click(function(){ onClickFunc(value); }); 14 | thumbnails.append(img); 15 | }); 16 | }); 17 | }; 18 | 19 | -------------------------------------------------------------------------------- /view/pagevar.go: -------------------------------------------------------------------------------- 1 | package view 2 | /* 3 | import ( 4 | "os" 5 | "github.com/ungerik/go-start/utils" 6 | ) 7 | 8 | /////////////////////////////////////////////////////////////////////////////// 9 | // PageVar 10 | 11 | type PageVar struct { 12 | Page **Page 13 | } 14 | 15 | func (self PageVar) Init(parent View, thisView View) { 16 | (*self.Page).Init(parent, thisView) 17 | } 18 | 19 | func (self PageVar) ID() string { 20 | return (*self.Page).ID() 21 | } 22 | 23 | func (self PageVar) IterateChildren(callback IterateChildrenCallback) { 24 | (*self.Page).IterateChildren(callback) 25 | } 26 | 27 | func (self PageVar) Render(ctx *Context) (err error) { 28 | return (*self.Page).Render(ctx) 29 | } 30 | */ -------------------------------------------------------------------------------- /mongo/dereferenceiterator.go: -------------------------------------------------------------------------------- 1 | package mongo 2 | 3 | // Returns an iterator of dereferenced refs, or an error iterator if there was an error 4 | func NewDereferenceIterator(refs ...Ref) *DereferenceIterator { 5 | return &DereferenceIterator{Refs: refs} 6 | } 7 | 8 | type DereferenceIterator struct { 9 | Refs []Ref 10 | index int 11 | err error 12 | } 13 | 14 | func (self *DereferenceIterator) Next(resultRef interface{}) bool { 15 | if self.err != nil || self.index >= len(self.Refs) { 16 | return false 17 | } 18 | self.err = self.Refs[self.index].Get(resultRef) 19 | self.index++ 20 | return self.err == nil 21 | } 22 | 23 | func (self *DereferenceIterator) Err() error { 24 | return self.err 25 | } 26 | -------------------------------------------------------------------------------- /examples/ServeStatic/main.go: -------------------------------------------------------------------------------- 1 | /* 2 | ## Serves static files in the current directory 3 | 4 | * index.html will be handled as expected 5 | * Does not list directory contents 6 | 7 | Download, build and run example: 8 | 9 | go get github.com/ungerik/go-start/examples/ServeStatic 10 | go install github.com/ungerik/go-start/examples/ServeStatic && ServeStatic 11 | 12 | */ 13 | package main 14 | 15 | import ( 16 | "fmt" 17 | "path/filepath" 18 | 19 | "github.com/ungerik/go-start/view" 20 | ) 21 | 22 | func main() { 23 | absPath, _ := filepath.Abs(".") 24 | fmt.Printf("Serving %s/ at http://0.0.0.0:8080/\n", absPath) 25 | view.Config.StaticDirs = []string{"."} 26 | view.RunServerAddr("0.0.0.0:8080", nil) 27 | } 28 | -------------------------------------------------------------------------------- /sublime-snippets/go-start-form.sublime-snippet: -------------------------------------------------------------------------------- 1 | 2 | 16 | Form 17 | source.go 18 | go-start view.Form 19 | 20 | -------------------------------------------------------------------------------- /utils/set.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | type dummy struct {} 4 | 5 | type StringSet struct { 6 | m map[string]dummy 7 | } 8 | 9 | func (self StringSet) Add(val string) { 10 | self.m[val] = dummy{} 11 | } 12 | 13 | func (self StringSet) Delete(val string) { 14 | delete(self.m, val) 15 | } 16 | 17 | func (self StringSet) Contains(val string) bool { 18 | _, ok := self.m[val] 19 | return ok 20 | } 21 | 22 | type IntSet struct { 23 | m map[int]dummy 24 | } 25 | 26 | func (self IntSet) Add(val int) { 27 | self.m[val] = dummy{} 28 | } 29 | 30 | func (self IntSet) Delete(val int) { 31 | delete(self.m, val) 32 | } 33 | 34 | func (self IntSet) Contains(val int) bool { 35 | _, ok := self.m[val] 36 | return ok 37 | } 38 | -------------------------------------------------------------------------------- /examples/FullTutorial/templates/css/style.css: -------------------------------------------------------------------------------- 1 | .title-bar { 2 | position: absolute; 3 | top: 20px; 4 | left:50px; 5 | -webkit-transform: rotate(-2deg); 6 | -moz-transform: rotate(-2deg); 7 | -ms-transform: rotate(-2deg); 8 | -o-transform: rotate(-2deg); 9 | transform: rotate(-2deg); 10 | background-color: {{{Primary}}}; 11 | color: #fff; 12 | text-shadow: 0 1px 0 rgba(0, 0, 0, 0.3); 13 | font-size: 24px; 14 | line-height: 24px; 15 | padding: 10px 15px 7px; 16 | } 17 | 18 | .title-bar.right { 19 | -webkit-transform: rotate(2deg); 20 | -moz-transform: rotate(2deg); 21 | -ms-transform: rotate(2deg); 22 | -o-transform: rotate(2deg); 23 | transform: rotate(2deg); 24 | } 25 | -------------------------------------------------------------------------------- /view/label.go: -------------------------------------------------------------------------------- 1 | package view 2 | 3 | /////////////////////////////////////////////////////////////////////////////// 4 | // Label 5 | 6 | type Label struct { 7 | ViewBaseWithId 8 | Class string 9 | For View 10 | Content View 11 | } 12 | 13 | func (self *Label) Render(ctx *Context) (err error) { 14 | var forID string 15 | if self.For != nil { 16 | forID = self.For.ID() 17 | } 18 | ctx.Response.XML.OpenTag("label") 19 | ctx.Response.XML.AttribIfNotDefault("id", self.id) 20 | ctx.Response.XML.AttribIfNotDefault("class", self.Class) 21 | ctx.Response.XML.AttribIfNotDefault("for", forID) 22 | if self.Content != nil { 23 | err = self.Content.Render(ctx) 24 | } 25 | ctx.Response.XML.CloseTag() 26 | return err 27 | } 28 | -------------------------------------------------------------------------------- /reflection/functions_test.go: -------------------------------------------------------------------------------- 1 | package reflection 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func Test_SmartCopy(t *testing.T) { 8 | { 9 | src := 1 10 | dst := 0 11 | SmartCopy(src, &dst) 12 | if dst != 1 { 13 | t.Fail() 14 | } 15 | } 16 | { 17 | src := 1 18 | var dst *int 19 | SmartCopy(src, &dst) 20 | if dst == nil || *dst != 1 { 21 | t.Fail() 22 | } 23 | } 24 | return // todo 25 | 26 | { 27 | src := new(int) 28 | *src = 1 29 | var dst *int 30 | SmartCopy(src, &dst) 31 | if dst == nil || *dst != 1 { 32 | t.Fail() 33 | } 34 | } 35 | 36 | { 37 | src := new(int) 38 | *src = 1 39 | dst := 0 40 | SmartCopy(src, &dst) 41 | if dst != 1 { 42 | t.Fail() 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /mongo/query_filterstartswith.go: -------------------------------------------------------------------------------- 1 | package mongo 2 | 3 | import ( 4 | "labix.org/v2/mgo/bson" 5 | ) 6 | 7 | /////////////////////////////////////////////////////////////////////////////// 8 | // query_filterStartsWith 9 | 10 | type query_filterStartsWith struct { 11 | query_filterBase 12 | selector string 13 | str string 14 | caseInsensitive bool 15 | } 16 | 17 | func (self *query_filterStartsWith) bsonSelector() bson.M { 18 | s := escapeStringForRegex(self.str) 19 | var options string 20 | if self.caseInsensitive { 21 | options = "i" 22 | } 23 | return bson.M{self.selector: bson.RegEx{"^" + s, options}} 24 | } 25 | 26 | func (self *query_filterStartsWith) Selector() string { 27 | return self.selector 28 | } 29 | -------------------------------------------------------------------------------- /view/paragraph.go: -------------------------------------------------------------------------------- 1 | package view 2 | 3 | /////////////////////////////////////////////////////////////////////////////// 4 | // Paragraph 5 | 6 | type Paragraph struct { 7 | ViewBaseWithId 8 | Class string 9 | Content View 10 | } 11 | 12 | func (self *Paragraph) IterateChildren(callback IterateChildrenCallback) { 13 | if self.Content != nil { 14 | callback(self, self.Content) 15 | } 16 | } 17 | 18 | func (self *Paragraph) Render(ctx *Context) (err error) { 19 | ctx.Response.XML.OpenTag("p") 20 | ctx.Response.XML.AttribIfNotDefault("id", self.id) 21 | ctx.Response.XML.AttribIfNotDefault("class", self.Class) 22 | if self.Content != nil { 23 | err = self.Content.Render(ctx) 24 | } 25 | ctx.Response.XML.CloseTagAlways() 26 | return err 27 | } 28 | -------------------------------------------------------------------------------- /view/aside.go: -------------------------------------------------------------------------------- 1 | package view 2 | 3 | // Aside represents a HTML aside element. 4 | type Aside struct { 5 | ViewBaseWithId 6 | Class string 7 | Style string 8 | Content View 9 | } 10 | 11 | func (self *Aside) IterateChildren(callback IterateChildrenCallback) { 12 | if self.Content != nil { 13 | callback(self, self.Content) 14 | } 15 | } 16 | 17 | func (self *Aside) Render(ctx *Context) (err error) { 18 | ctx.Response.XML.OpenTag("aside") 19 | ctx.Response.XML.AttribIfNotDefault("id", self.id) 20 | ctx.Response.XML.AttribIfNotDefault("class", self.Class) 21 | ctx.Response.XML.AttribIfNotDefault("style", self.Style) 22 | if self.Content != nil { 23 | err = self.Content.Render(ctx) 24 | } 25 | ctx.Response.XML.CloseTagAlways() 26 | return err 27 | } 28 | -------------------------------------------------------------------------------- /mongo/query_filterequal.go: -------------------------------------------------------------------------------- 1 | package mongo 2 | 3 | import ( 4 | "labix.org/v2/mgo/bson" 5 | ) 6 | 7 | /////////////////////////////////////////////////////////////////////////////// 8 | // query_filterEqual 9 | 10 | type query_filterEqual struct { 11 | query_filterBase 12 | selector string 13 | value interface{} 14 | } 15 | 16 | func (self *query_filterEqual) bsonSelector() bson.M { 17 | return bson.M{self.selector: self.value} 18 | } 19 | 20 | func (self *query_filterEqual) Selector() string { 21 | // Don't return special selectors that start with $ 22 | if self.selector != "" && self.selector[0] == '$' { 23 | return "" 24 | } 25 | return self.selector 26 | } 27 | 28 | //func (self *query_filterEqual) Value() interface{} { 29 | // return self.value 30 | //} 31 | -------------------------------------------------------------------------------- /mongo/query_filterequalcaseinsensitive.go: -------------------------------------------------------------------------------- 1 | package mongo 2 | 3 | import ( 4 | "labix.org/v2/mgo/bson" 5 | ) 6 | 7 | /////////////////////////////////////////////////////////////////////////////// 8 | // query_filterEqualCaseInsensitive 9 | 10 | type query_filterEqualCaseInsensitive struct { 11 | query_filterBase 12 | selector string 13 | str string 14 | } 15 | 16 | func (self *query_filterEqualCaseInsensitive) bsonSelector() bson.M { 17 | s := escapeStringForRegex(self.str) 18 | return bson.M{self.selector: bson.RegEx{"^" + s + "$", "i"}} 19 | } 20 | 21 | func (self *query_filterEqualCaseInsensitive) Selector() string { 22 | return self.selector 23 | } 24 | 25 | //func (self *query_filterEqualCaseInsensitive) CompareString() string { 26 | // return self.str 27 | //} 28 | -------------------------------------------------------------------------------- /view/span.go: -------------------------------------------------------------------------------- 1 | package view 2 | 3 | /////////////////////////////////////////////////////////////////////////////// 4 | // Span 5 | 6 | // Span represents a HTML span element. 7 | type Span struct { 8 | ViewBaseWithId 9 | Class string 10 | Content View 11 | } 12 | 13 | func (self *Span) IterateChildren(callback IterateChildrenCallback) { 14 | if self.Content != nil { 15 | callback(self, self.Content) 16 | } 17 | } 18 | 19 | func (self *Span) Render(ctx *Context) (err error) { 20 | ctx.Response.XML.OpenTag("span") 21 | ctx.Response.XML.AttribIfNotDefault("id", self.id) 22 | ctx.Response.XML.AttribIfNotDefault("class", self.Class) 23 | if self.Content != nil { 24 | err = self.Content.Render(ctx) 25 | } 26 | ctx.Response.XML.CloseTagAlways() 27 | return err 28 | } 29 | -------------------------------------------------------------------------------- /templatesystem/gotemplate.go: -------------------------------------------------------------------------------- 1 | package templatesystem 2 | 3 | import ( 4 | "io" 5 | "text/template" 6 | ) 7 | 8 | type GoTemplate struct { 9 | templ *template.Template 10 | } 11 | 12 | func (self *GoTemplate) Render(out io.Writer, context interface{}) (err error) { 13 | return self.templ.Execute(out, context) 14 | } 15 | 16 | type Go struct{} 17 | 18 | func (self *Go) ParseFile(filename string) (Template, error) { 19 | templ, err := template.ParseFiles(filename) 20 | if err != nil { 21 | return nil, err 22 | } 23 | return &GoTemplate{templ}, nil 24 | } 25 | 26 | func (self *Go) ParseString(text, name string) (Template, error) { 27 | templ, err := template.New(name).Parse(text) 28 | if err != nil { 29 | return nil, err 30 | } 31 | return &GoTemplate{templ}, nil 32 | } 33 | -------------------------------------------------------------------------------- /templatesystem/passthrough.go: -------------------------------------------------------------------------------- 1 | package templatesystem 2 | 3 | import ( 4 | "io" 5 | "io/ioutil" 6 | "path/filepath" 7 | ) 8 | 9 | type PassThroughTemplate struct { 10 | text string 11 | } 12 | 13 | func (self *PassThroughTemplate) Render(out io.Writer, context interface{}) (err error) { 14 | _, err = out.Write([]byte(self.text)) 15 | return err 16 | } 17 | 18 | type PassThrough struct{} 19 | 20 | func (self *PassThrough) ParseFile(filename string) (Template, error) { 21 | text, err := ioutil.ReadFile(filename) 22 | if err != nil { 23 | return nil, err 24 | } 25 | return self.ParseString(string(text), filepath.Base(filename)) 26 | } 27 | 28 | func (self *PassThrough) ParseString(text, name string) (Template, error) { 29 | return &PassThroughTemplate{text: text}, nil 30 | } 31 | -------------------------------------------------------------------------------- /view/pagelink.go: -------------------------------------------------------------------------------- 1 | package view 2 | 3 | func NewPageLink(page **Page, title string) *PageLink { 4 | return &PageLink{Page: page, Title: title} 5 | } 6 | 7 | type PageLink struct { 8 | Page **Page 9 | Content View 10 | Title string 11 | Rel string 12 | } 13 | 14 | func (self *PageLink) URL(ctx *Context) string { 15 | return (*self.Page).URL(ctx) 16 | } 17 | 18 | func (self *PageLink) LinkContent(ctx *Context) View { 19 | if self.Content == nil { 20 | return HTML(self.LinkTitle(ctx)) 21 | } 22 | return self.Content 23 | } 24 | 25 | func (self *PageLink) LinkTitle(ctx *Context) string { 26 | if self.Title == "" { 27 | return (*self.Page).LinkTitle(ctx) 28 | } 29 | return self.Title 30 | } 31 | 32 | func (self *PageLink) LinkRel(ctx *Context) string { 33 | return self.Rel 34 | } 35 | -------------------------------------------------------------------------------- /examples/FullTutorial/views/admin/export-emails.go: -------------------------------------------------------------------------------- 1 | package admin 2 | 3 | import ( 4 | . "github.com/ungerik/go-start/view" 5 | 6 | "github.com/ungerik/go-start/examples/FullTutorial/models" 7 | . "github.com/ungerik/go-start/examples/FullTutorial/views" 8 | ) 9 | 10 | func init() { 11 | Admin_ExportEmails = NewViewURLWrapper( 12 | RenderView( 13 | func(ctx *Context) (err error) { 14 | // Download instead of display: 15 | // ctx.Response.ContentDispositionAttachment("emails.csv") 16 | i := models.Users.Iterator() 17 | var u models.User 18 | for i.Next(&u) { 19 | if len(u.Email) > 0 { 20 | ctx.Response.Printf("%s, %s, %s \n", u.Email[0].Address, u.Name.First.String(), u.Name.Last.String()) 21 | } 22 | } 23 | return i.Err() 24 | }, 25 | ), 26 | ) 27 | } 28 | -------------------------------------------------------------------------------- /mongo/document.go: -------------------------------------------------------------------------------- 1 | package mongo 2 | 3 | import ( 4 | "time" 5 | 6 | "labix.org/v2/mgo/bson" 7 | 8 | "github.com/ungerik/go-start/model" 9 | ) 10 | 11 | /////////////////////////////////////////////////////////////////////////////// 12 | // Document 13 | 14 | type Document interface { 15 | Init(collection *Collection, embeddingStruct interface{}) 16 | Collection() *Collection 17 | ObjectId() bson.ObjectId 18 | SetObjectId(id bson.ObjectId) 19 | // Iterator returns an iterator that iterates only this document. 20 | Iterator() model.Iterator 21 | CreationTime() time.Time 22 | Ref() Ref 23 | Save() error 24 | Delete() error 25 | 26 | // RemoveInvalidRefs sets all invalid mongo.Ref instances to empty, 27 | // but does not save the document. 28 | RemoveInvalidRefs() (invalidRefs []InvalidRefData, err error) 29 | } 30 | -------------------------------------------------------------------------------- /media/fileview.go: -------------------------------------------------------------------------------- 1 | package media 2 | 3 | import ( 4 | "io" 5 | 6 | "github.com/ungerik/go-start/config" 7 | "github.com/ungerik/go-start/view" 8 | ) 9 | 10 | var FileView = view.NewViewURLWrapper(view.RenderView( 11 | func(ctx *view.Context) error { 12 | reader, _, contentType, err := Config.Backend.FileReader(ctx.URLArgs[0]) 13 | if err != nil { 14 | if _, ok := err.(ErrNotFound); ok { 15 | err = view.NotFound(ctx.URLArgs[0] + "/" + ctx.URLArgs[1] + " not found") 16 | config.Logger.Println("FileView:", err) 17 | return err 18 | } 19 | return err 20 | } 21 | defer reader.Close() 22 | _, err = io.Copy(ctx.Response, reader) 23 | if err != nil { 24 | config.Logger.Println("FileView:", err) 25 | return err 26 | } 27 | ctx.Response.Header().Set("Content-Type", contentType) 28 | return nil 29 | }, 30 | )) 31 | -------------------------------------------------------------------------------- /view/hiddeninput.go: -------------------------------------------------------------------------------- 1 | package view 2 | 3 | /////////////////////////////////////////////////////////////////////////////// 4 | // HiddenInput 5 | 6 | type HiddenInput struct { 7 | ViewBaseWithId 8 | Name string 9 | Value string 10 | } 11 | 12 | func (self *HiddenInput) Render(ctx *Context) (err error) { 13 | ctx.Response.XML.OpenTag("input") 14 | ctx.Response.XML.AttribIfNotDefault("id", self.id) 15 | ctx.Response.XML.Attrib("type", "hidden") 16 | ctx.Response.XML.Attrib("name", self.Name) 17 | ctx.Response.XML.Attrib("value", self.Value) 18 | ctx.Response.XML.CloseTag() 19 | return nil 20 | } 21 | 22 | //func (self *HiddenInput) SetName(name string) { 23 | // self.Name = name 24 | // ViewChanged(self) 25 | //} 26 | // 27 | //func (self *HiddenInput) SetValue(value string) { 28 | // self.Value = value 29 | // ViewChanged(self) 30 | //} 31 | -------------------------------------------------------------------------------- /examples/ViewPaths/views/root/homepage.go: -------------------------------------------------------------------------------- 1 | package root 2 | 3 | import ( 4 | "time" 5 | 6 | . "github.com/ungerik/go-start/view" 7 | 8 | "github.com/ungerik/go-start/examples/ViewPaths/views" 9 | ) 10 | 11 | // Navigation is a function so it can be re-used by other views 12 | func Navigation() View { 13 | return UL( 14 | // Use addresses of views variables to avoid 15 | // circular initialization dependencies 16 | A(&views.Homepage, "Home"), 17 | A(&views.Admin, "Admin"), 18 | A(&views.GetJSON, "get.json"), 19 | A(&views.GetXML, "get.xml"), 20 | ) 21 | } 22 | 23 | func init() { 24 | views.Homepage = CacheView( 25 | time.Hour, // Cache Homepage for one hour 26 | &Page{ 27 | Title: Escape("Page Title"), 28 | Content: Views{ 29 | H1("go-start ViewPaths Example"), 30 | Navigation(), 31 | }, 32 | }, 33 | ) 34 | } 35 | -------------------------------------------------------------------------------- /mongo/query_sort.go: -------------------------------------------------------------------------------- 1 | package mongo 2 | 3 | import ( 4 | "labix.org/v2/mgo" 5 | "strings" 6 | ) 7 | 8 | /////////////////////////////////////////////////////////////////////////////// 9 | // query_sort 10 | 11 | type query_sort struct { 12 | query_base 13 | selectors []string 14 | } 15 | 16 | func (self *query_sort) mongoQuery() (q *mgo.Query, err error) { 17 | selectors := self.selectors 18 | for query := self.parentQuery; query != nil; query = query.ParentQuery() { 19 | s, ok := query.(*query_sort) 20 | if !ok { 21 | break 22 | } 23 | selectors = append(s.selectors, selectors...) 24 | } 25 | q, err = self.parentQuery.mongoQuery() 26 | if err != nil { 27 | return nil, err 28 | } 29 | return q.Sort(selectors...), nil 30 | } 31 | 32 | func (self *query_sort) Selector() string { 33 | return strings.Join(self.selectors, ",") 34 | } 35 | -------------------------------------------------------------------------------- /user/formmodels.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | import ( 4 | "errors" 5 | "github.com/ungerik/go-start/model" 6 | ) 7 | 8 | type PasswordFormModel struct { 9 | Password1 model.Password `model:"minlen=6" view:"label=Password|size=20"` 10 | Password2 model.Password `view:"label=Repeat password|size=20"` 11 | } 12 | 13 | func (self *PasswordFormModel) Validate(metaData *model.MetaData) error { 14 | if self.Password1 != self.Password2 { 15 | return errors.New("Passwords don't match") 16 | } 17 | return nil 18 | } 19 | 20 | type EmailPasswordFormModel struct { 21 | Email model.Email `model:"required" view:"size=20"` 22 | PasswordFormModel `bson:",inline" view:"size=20"` 23 | } 24 | 25 | type LoginFormModel struct { 26 | Email model.Email `model:"required" view:"size=20"` 27 | Password model.Password `model:"required" view:"size=20"` 28 | } 29 | -------------------------------------------------------------------------------- /view/viewurlwrapper.go: -------------------------------------------------------------------------------- 1 | package view 2 | 3 | func NewViewURLWrapper(view View) *ViewURLWrapper { 4 | return &ViewURLWrapper{View: view} 5 | } 6 | 7 | type ViewURLWrapper struct { 8 | View View 9 | path string 10 | } 11 | 12 | func (self *ViewURLWrapper) Init(thisView View) { 13 | self.View.Init(self.View) 14 | } 15 | 16 | func (self *ViewURLWrapper) ID() string { 17 | return self.View.ID() 18 | } 19 | 20 | func (self *ViewURLWrapper) IterateChildren(callback IterateChildrenCallback) { 21 | self.View.IterateChildren(callback) 22 | } 23 | 24 | func (self *ViewURLWrapper) Render(ctx *Context) (err error) { 25 | return self.View.Render(ctx) 26 | } 27 | 28 | func (self *ViewURLWrapper) SetPath(path string) { 29 | self.path = path 30 | } 31 | 32 | func (self *ViewURLWrapper) URL(ctx *Context) string { 33 | return StringURL(self.path).URL(ctx) 34 | } 35 | -------------------------------------------------------------------------------- /view/urllink.go: -------------------------------------------------------------------------------- 1 | package view 2 | 3 | // URLLink implements the LinkModel interface with a reference to a URL 4 | // and string values for Title and Rel. 5 | type URLLink struct { 6 | Url URL 7 | Content View // If is nil, then self.LinkTitle() will be used 8 | Title string // If is "", then self.URL will be used 9 | Rel string 10 | } 11 | 12 | func (self *URLLink) URL(ctx *Context) string { 13 | return self.Url.URL(ctx) 14 | } 15 | 16 | func (self *URLLink) LinkContent(ctx *Context) View { 17 | if self.Content == nil { 18 | return HTML(self.LinkTitle(ctx)) 19 | } 20 | return self.Content 21 | } 22 | 23 | func (self *URLLink) LinkTitle(ctx *Context) string { 24 | if self.Title == "" { 25 | return self.URL(ctx) 26 | } 27 | return self.Title 28 | } 29 | 30 | func (self *URLLink) LinkRel(ctx *Context) string { 31 | return self.Rel 32 | } 33 | -------------------------------------------------------------------------------- /utils/net.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "io/ioutil" 5 | "net" 6 | "net/http" 7 | "strings" 8 | ) 9 | 10 | const LoopbackIP = "127.0.0.1" 11 | 12 | // OwnIP returns the primary IP address of the system or an empty string. 13 | func OwnIP() string { 14 | addrs, err := net.InterfaceAddrs() 15 | if err != nil { 16 | return "" 17 | } 18 | for _, addr := range addrs { 19 | ip := addr.String() 20 | if ip != LoopbackIP { 21 | return ip 22 | } 23 | } 24 | return "" 25 | } 26 | 27 | // ReadURL reads all data from an URL, including file:// URLs. 28 | func ReadURL(url string) ([]byte, error) { 29 | if strings.Index(url, "file://") == 0 { 30 | return ioutil.ReadFile(url[len("file://"):]) 31 | } 32 | r, err := http.Get(url) 33 | if err != nil { 34 | return nil, err 35 | } 36 | defer r.Body.Close() 37 | return ioutil.ReadAll(r.Body) 38 | } 39 | -------------------------------------------------------------------------------- /model/conversioniterator.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import "github.com/ungerik/go-start/reflection" 4 | 5 | // ConvertIterator returns an Iterator that calls conversionFunc 6 | // for every from.Next() result and returns the result 7 | // of conversionFunc at every Next(). 8 | func ConversionIterator(from Iterator, fromResultPtr interface{}, conversionFunc func(interface{}) interface{}) Iterator { 9 | return &conversionIterator{from, fromResultPtr, conversionFunc} 10 | } 11 | 12 | type conversionIterator struct { 13 | Iterator 14 | FromResultPtr interface{} 15 | conversionFunc func(interface{}) interface{} 16 | } 17 | 18 | func (self *conversionIterator) Next(resultRef interface{}) bool { 19 | if !self.Iterator.Next(self.FromResultPtr) { 20 | return false 21 | } 22 | reflection.SmartCopy(self.conversionFunc(self.FromResultPtr), resultRef) 23 | return true 24 | } 25 | -------------------------------------------------------------------------------- /model/singletoniterator.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import "github.com/ungerik/go-start/reflection" 4 | 5 | func NewSingletonIterator(singleton interface{}) *SingletonIterator { 6 | return &SingletonIterator{Singleton: singleton} 7 | } 8 | 9 | func NewSingletonOrErrorOnlyIterator(singleton interface{}, err error) Iterator { 10 | if err != nil { 11 | return NewErrorOnlyIterator(err) 12 | } 13 | return NewSingletonIterator(singleton) 14 | } 15 | 16 | type SingletonIterator struct { 17 | Singleton interface{} 18 | iterated bool 19 | } 20 | 21 | func (self *SingletonIterator) Next(resultRef interface{}) bool { 22 | if self.iterated || self.Singleton == nil { 23 | return false 24 | } 25 | reflection.SmartCopy(self.Singleton, resultRef) 26 | self.iterated = true 27 | return true 28 | } 29 | 30 | func (self *SingletonIterator) Err() error { 31 | return nil 32 | } 33 | -------------------------------------------------------------------------------- /view/stringlink.go: -------------------------------------------------------------------------------- 1 | package view 2 | 3 | // StringLink implements the LinkModel interface with string values 4 | // for Url, Title, and Rel. 5 | type StringLink struct { 6 | Url string 7 | Content View // If nil, then self.LinkTitle() will be used 8 | Title string // If "", then self.URL will be used 9 | Rel string 10 | } 11 | 12 | func (self *StringLink) URL(ctx *Context) string { 13 | return StringURL(self.Url).URL(ctx) 14 | } 15 | 16 | func (self *StringLink) LinkContent(ctx *Context) View { 17 | if self.Content == nil { 18 | return HTML(self.LinkTitle(ctx)) 19 | } 20 | return self.Content 21 | } 22 | 23 | func (self *StringLink) LinkTitle(ctx *Context) string { 24 | if self.Title == "" { 25 | return self.URL(ctx) 26 | } 27 | return self.Title 28 | } 29 | 30 | func (self *StringLink) LinkRel(ctx *Context) string { 31 | return self.Rel 32 | } 33 | -------------------------------------------------------------------------------- /examples/FullTutorial/models/user.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "github.com/ungerik/go-start/media" 5 | "github.com/ungerik/go-start/model" 6 | "github.com/ungerik/go-start/user" 7 | ) 8 | 9 | // We are using user.NewCollection here instead of mongo.NewCollection 10 | // because user.NewCollection sets the correct mongo.Collection.DocLabelSelectors 11 | // so that mongo.Collection.DocumentLabel(id) returns a label for 12 | // the document with id composed of the name modelext.Name components 13 | // Prefix + First + Middle + Last + Postfix + Organization. 14 | var Users = user.NewCollection("users") 15 | 16 | func NewUser() *User { 17 | var doc User 18 | Users.InitDocument(&doc) 19 | return &doc 20 | } 21 | 22 | type User struct { 23 | user.User `bson:",inline"` 24 | 25 | Image media.ImageRef 26 | Gender model.Choice `model:"options=,Male,Female"` 27 | } 28 | -------------------------------------------------------------------------------- /templatesystem/kasia.go: -------------------------------------------------------------------------------- 1 | package templatesystem 2 | 3 | /* 4 | import ( 5 | "io" 6 | "io/ioutil" 7 | "kasia" 8 | "path/filepath" 9 | ) 10 | 11 | type KasiaTemplate struct { 12 | templ *kasia.Template 13 | } 14 | 15 | func (self *KasiaTemplate) Render(out io.Writer, context interface{}) (err error) { 16 | return self.templ.Run(out, context) 17 | } 18 | 19 | type Kasia struct{} 20 | 21 | func (self *Kasia) ParseFile(filename string) (Template, error) { 22 | text, err := ioutil.ReadFile(filename) 23 | if err != nil { 24 | return nil, err 25 | } 26 | return self.ParseString(string(text), filepath.Base(filename)) 27 | } 28 | 29 | func (self *Kasia) ParseString(text, name string) (Template, error) { 30 | templ := kasia.New() 31 | err := templ.Parse([]byte(text)) 32 | if err != nil { 33 | return nil, err 34 | } 35 | return &KasiaTemplate{templ: templ}, nil 36 | } 37 | */ 38 | -------------------------------------------------------------------------------- /model/visitorfunc.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | // VisitorFunc calls its function for every Visitor method call, 4 | // thus mapping all Visitor methods on a single function. 5 | type VisitorFunc func(data *MetaData) error 6 | 7 | func (self VisitorFunc) BeginNamedFields(namedFields *MetaData) error { 8 | return self(namedFields) 9 | } 10 | 11 | func (self VisitorFunc) NamedField(field *MetaData) error { 12 | return self(field) 13 | } 14 | 15 | func (self VisitorFunc) EndNamedFields(namedFields *MetaData) error { 16 | return self(namedFields) 17 | } 18 | 19 | func (self VisitorFunc) BeginIndexedFields(indexedFields *MetaData) error { 20 | return self(indexedFields) 21 | } 22 | 23 | func (self VisitorFunc) IndexedField(field *MetaData) error { 24 | return self(field) 25 | } 26 | 27 | func (self VisitorFunc) EndIndexedFields(indexedFields *MetaData) error { 28 | return self(indexedFields) 29 | } 30 | -------------------------------------------------------------------------------- /states/state.go: -------------------------------------------------------------------------------- 1 | package states 2 | 3 | import ( 4 | "bytes" 5 | ) 6 | 7 | 8 | const ( 9 | Transient = iota 10 | SessionPersistent 11 | UserPersistent 12 | ) 13 | 14 | 15 | type Persistence uint 16 | 17 | 18 | type State interface { 19 | SetStateMachine(*StateMachine) 20 | StateMachine() *StateMachine 21 | Name() string 22 | Enter(*Transition) 23 | Leave(*Transition) 24 | } 25 | 26 | 27 | type TransitionVetoer interface { 28 | TransitionVeto(*Transition) bool 29 | } 30 | 31 | 32 | func writeStatePathRecursive(state State, buf *bytes.Buffer) *bytes.Buffer { 33 | parent := state.StateMachine() 34 | if parent != nil { 35 | writeStatePathRecursive(parent, buf) 36 | } 37 | buf.WriteByte('/') 38 | buf.WriteString(state.Name()) 39 | return buf 40 | } 41 | 42 | 43 | func StatePath(state State) string { 44 | return writeStatePathRecursive(state, &bytes.Buffer{}).String() 45 | } 46 | -------------------------------------------------------------------------------- /templatesystem/mustache.go: -------------------------------------------------------------------------------- 1 | package templatesystem 2 | 3 | import ( 4 | "github.com/ungerik/mustache.go" 5 | "io" 6 | ) 7 | 8 | type MustacheTemplate struct { 9 | templ *mustache.Template 10 | } 11 | 12 | func (self *MustacheTemplate) Render(out io.Writer, context interface{}) (err error) { 13 | str := self.templ.Render(context) 14 | _, err = out.Write([]byte(str)) 15 | return err 16 | } 17 | 18 | type Mustache struct{} 19 | 20 | func (self *Mustache) ParseFile(filename string) (Template, error) { 21 | templ, err := mustache.ParseFile(filename) 22 | if err != nil { 23 | return nil, err 24 | } 25 | return &MustacheTemplate{templ: templ}, nil 26 | } 27 | 28 | func (self *Mustache) ParseString(text, name string) (Template, error) { 29 | templ, err := mustache.ParseString(text) 30 | if err != nil { 31 | return nil, err 32 | } 33 | return &MustacheTemplate{templ: templ}, nil 34 | } 35 | -------------------------------------------------------------------------------- /view/shorttag.go: -------------------------------------------------------------------------------- 1 | package view 2 | 3 | /////////////////////////////////////////////////////////////////////////////// 4 | // ShortTag 5 | 6 | // ShortTag represents an arbitrary HTML element. It has a smaller footprint than Tag. 7 | type ShortTag struct { 8 | ViewBase 9 | Tag string 10 | Class string 11 | Attribs map[string]string 12 | Content View 13 | } 14 | 15 | func (self *ShortTag) IterateChildren(callback IterateChildrenCallback) { 16 | if self.Content != nil { 17 | callback(self, self.Content) 18 | } 19 | } 20 | 21 | func (self *ShortTag) Render(ctx *Context) (err error) { 22 | ctx.Response.XML.OpenTag(self.Tag).AttribIfNotDefault("class", self.Class) 23 | for key, value := range self.Attribs { 24 | ctx.Response.XML.Attrib(key, value) 25 | } 26 | if self.Content != nil { 27 | err = self.Content.Render(ctx) 28 | } 29 | ctx.Response.XML.CloseTagAlways() 30 | return err 31 | } 32 | -------------------------------------------------------------------------------- /static/crossdomain.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 19 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /static/js/log.js: -------------------------------------------------------------------------------- 1 | 2 | // usage: log('inside coolFunc', this, arguments); 3 | // paulirish.com/2009/log-a-lightweight-wrapper-for-consolelog/ 4 | window.log = function(){ 5 | log.history = log.history || []; // store logs to an array for reference 6 | log.history.push(arguments); 7 | if(this.console) { 8 | arguments.callee = arguments.callee.caller; 9 | var newarr = [].slice.call(arguments); 10 | (typeof console.log === 'object' ? log.apply.call(console.log, console, newarr) : console.log.apply(console, newarr)); 11 | } 12 | }; 13 | 14 | // make it safe to use console.log always 15 | (function(b){function c(){}for(var d="assert,count,debug,dir,dirxml,error,exception,group,groupCollapsed,groupEnd,info,log,timeStamp,profile,profileEnd,time,timeEnd,trace,warn".split(","),a;a=d.pop();){b[a]=b[a]||c}})((function(){try 16 | {console.log();return window.console;}catch(err){return window.console={};}})()); 17 | 18 | -------------------------------------------------------------------------------- /user/nav.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | import ( 4 | "github.com/ungerik/go-start/view" 5 | ) 6 | 7 | // Nav returns user navigation links depending on if there is an user session. 8 | // If a user session is active, logout and profile will be returned, 9 | // if there is no active user session, login and signup will be returned. 10 | // separator will be put between the returned links. 11 | // signup, profile, and separator are optional and can be nil. 12 | func Nav(login, signup, logout, profile *view.Link, separator view.View) view.View { 13 | return view.DynamicView( 14 | func(ctx *view.Context) (view.View, error) { 15 | if ctx.Session.ID() != "" { 16 | if profile == nil { 17 | return logout, nil 18 | } 19 | return view.Views{logout, separator, profile}, nil 20 | } 21 | if signup == nil { 22 | return login, nil 23 | } 24 | return view.Views{login, separator, signup}, nil 25 | }, 26 | ) 27 | } 28 | -------------------------------------------------------------------------------- /view/viewbase.go: -------------------------------------------------------------------------------- 1 | package view 2 | 3 | // import "github.com/ungerik/go-start/debug" 4 | 5 | /////////////////////////////////////////////////////////////////////////////// 6 | // ViewBase 7 | 8 | // ViewBase is a base for View implementations. 9 | type ViewBase struct { 10 | // thisView holds the implemented View as seen from outside of ViewBase 11 | // because ViewBase can't know how View will be implemented. 12 | thisView View 13 | } 14 | 15 | func (self *ViewBase) Init(thisView View) { 16 | if thisView == self.thisView { 17 | return // already initialized 18 | } 19 | self.thisView = thisView 20 | thisView.IterateChildren(func(parent View, child View) (next bool) { 21 | if child != nil { 22 | child.Init(child) 23 | } 24 | return true 25 | }) 26 | } 27 | 28 | func (self *ViewBase) ID() string { 29 | return "" 30 | } 31 | 32 | func (self *ViewBase) IterateChildren(callback IterateChildrenCallback) { 33 | } 34 | -------------------------------------------------------------------------------- /mongo/query_filtercontains.go: -------------------------------------------------------------------------------- 1 | package mongo 2 | 3 | import ( 4 | "labix.org/v2/mgo/bson" 5 | ) 6 | 7 | /////////////////////////////////////////////////////////////////////////////// 8 | // query_filterContains 9 | 10 | type query_filterContains struct { 11 | query_filterBase 12 | selector string 13 | str string 14 | caseInsensitive bool 15 | } 16 | 17 | func (self *query_filterContains) bsonSelector() bson.M { 18 | s := escapeStringForRegex(self.str) 19 | var options string 20 | if self.caseInsensitive { 21 | options = "i" 22 | } 23 | return bson.M{self.selector: bson.RegEx{s, options}} 24 | } 25 | 26 | func (self *query_filterContains) Selector() string { 27 | return self.selector 28 | } 29 | 30 | //func (self *query_filterContains) CompareString() string { 31 | // return self.str 32 | //} 33 | // 34 | //func (self *query_filterContains) CaseInsensitive() bool { 35 | // return self.caseInsensitive 36 | //} 37 | -------------------------------------------------------------------------------- /view/div.go: -------------------------------------------------------------------------------- 1 | package view 2 | 3 | /////////////////////////////////////////////////////////////////////////////// 4 | // Div 5 | 6 | // Div represents a HTML div element. 7 | type Div struct { 8 | ViewBaseWithId 9 | Class string 10 | Style string 11 | Content View 12 | OnClick string 13 | } 14 | 15 | func (self *Div) IterateChildren(callback IterateChildrenCallback) { 16 | if self.Content != nil { 17 | callback(self, self.Content) 18 | } 19 | } 20 | 21 | func (self *Div) Render(ctx *Context) (err error) { 22 | ctx.Response.XML.OpenTag("div") 23 | ctx.Response.XML.AttribIfNotDefault("id", self.id) 24 | ctx.Response.XML.AttribIfNotDefault("class", self.Class) 25 | ctx.Response.XML.AttribIfNotDefault("style", self.Style) 26 | ctx.Response.XML.AttribIfNotDefault("onclick", self.OnClick) 27 | if self.Content != nil { 28 | err = self.Content.Render(ctx) 29 | } 30 | ctx.Response.XML.CloseTagAlways() 31 | return err 32 | } 33 | -------------------------------------------------------------------------------- /mongo/query_filterendswith.go: -------------------------------------------------------------------------------- 1 | package mongo 2 | 3 | import ( 4 | "labix.org/v2/mgo/bson" 5 | ) 6 | 7 | /////////////////////////////////////////////////////////////////////////////// 8 | // query_filterEndsWith 9 | 10 | type query_filterEndsWith struct { 11 | query_filterBase 12 | selector string 13 | str string 14 | caseInsensitive bool 15 | } 16 | 17 | func (self *query_filterEndsWith) bsonSelector() bson.M { 18 | s := escapeStringForRegex(self.str) 19 | var options string 20 | if self.caseInsensitive { 21 | options = "i" 22 | } 23 | return bson.M{self.selector: bson.RegEx{s + "$", options}} 24 | } 25 | 26 | func (self *query_filterEndsWith) Selector() string { 27 | return self.selector 28 | } 29 | 30 | //func (self *query_filterEndsWith) CompareString() string { 31 | // return self.str 32 | //} 33 | // 34 | //func (self *query_filterEndsWith) CaseInsensitive() bool { 35 | // return self.caseInsensitive 36 | //} 37 | -------------------------------------------------------------------------------- /modelext/postaladdress.go: -------------------------------------------------------------------------------- 1 | package modelext 2 | 3 | import ( 4 | "github.com/ungerik/go-start/model" 5 | "github.com/ungerik/go-start/utils" 6 | ) 7 | 8 | /////////////////////////////////////////////////////////////////////////////// 9 | // PostalAddress 10 | 11 | type PostalAddress struct { 12 | FirstLine model.String `view:"size=40"` 13 | SecondLine model.String `view:"size=40"` 14 | ZIP model.String `view:"size=10"` 15 | City model.String `view:"size=20"` 16 | State model.String `view:"size=20"` 17 | Country model.Country 18 | } 19 | 20 | func (self *PostalAddress) String() string { 21 | return self.StringSep(", ") 22 | } 23 | 24 | func (self *PostalAddress) StringSep(sep string) string { 25 | return utils.JoinNonEmptyStrings( 26 | sep, self.FirstLine.Get(), 27 | self.SecondLine.Get(), 28 | utils.JoinNonEmptyStrings(" ", self.ZIP.Get(), self.City.Get()), 29 | self.State.Get(), 30 | self.Country.Get(), 31 | ) 32 | } 33 | -------------------------------------------------------------------------------- /view/indirectviewwithurl.go: -------------------------------------------------------------------------------- 1 | package view 2 | 3 | func IndirectViewWithURL(viewWithURL *ViewWithURL) ViewWithURL { 4 | return &indirectViewWithURL{viewWithURL} 5 | } 6 | 7 | type indirectViewWithURL struct { 8 | viewWithURL *ViewWithURL 9 | } 10 | 11 | func (self *indirectViewWithURL) Init(thisView View) { 12 | (*self.viewWithURL).Init(thisView) 13 | } 14 | 15 | func (self *indirectViewWithURL) ID() string { 16 | return (*self.viewWithURL).ID() 17 | } 18 | 19 | func (self *indirectViewWithURL) IterateChildren(callback IterateChildrenCallback) { 20 | (*self.viewWithURL).IterateChildren(callback) 21 | } 22 | 23 | func (self *indirectViewWithURL) Render(ctx *Context) (err error) { 24 | return (*self.viewWithURL).Render(ctx) 25 | } 26 | 27 | func (self *indirectViewWithURL) URL(ctx *Context) string { 28 | return (*self.viewWithURL).URL(ctx) 29 | } 30 | 31 | func (self *indirectViewWithURL) SetPath(path string) { 32 | (*self.viewWithURL).SetPath(path) 33 | } 34 | -------------------------------------------------------------------------------- /view/image.go: -------------------------------------------------------------------------------- 1 | package view 2 | 3 | /////////////////////////////////////////////////////////////////////////////// 4 | // Image 5 | 6 | type Image struct { 7 | ViewBaseWithId 8 | Class string 9 | URL URL // If URL is set, then Src will be ignored 10 | Src string // String URL of the image, used when URL is nil 11 | Width int 12 | Height int 13 | Title string 14 | } 15 | 16 | func (self *Image) Render(ctx *Context) (err error) { 17 | ctx.Response.XML.OpenTag("img") 18 | ctx.Response.XML.AttribIfNotDefault("id", self.id) 19 | ctx.Response.XML.AttribIfNotDefault("class", self.Class) 20 | src := self.Src 21 | if self.URL != nil { 22 | src = self.URL.URL(ctx) 23 | } 24 | ctx.Response.XML.Attrib("src", src) 25 | ctx.Response.XML.AttribIfNotDefault("width", self.Width) 26 | ctx.Response.XML.AttribIfNotDefault("height", self.Height) 27 | ctx.Response.XML.AttribIfNotDefault("alt", self.Title) 28 | ctx.Response.XML.CloseTag() 29 | return nil 30 | } 31 | -------------------------------------------------------------------------------- /examples/ViewPaths/views/admin/admin.go: -------------------------------------------------------------------------------- 1 | package admin 2 | 3 | import ( 4 | . "github.com/ungerik/go-start/view" 5 | 6 | "github.com/ungerik/go-start/examples/ViewPaths/views" 7 | "github.com/ungerik/go-start/examples/ViewPaths/views/root" 8 | ) 9 | 10 | func init() { 11 | views.AdminAuth = NewBasicAuth("Username: admin, Password: admin", "admin", "admin") 12 | 13 | views.Admin = &Page{ 14 | Title: Escape("Admin"), 15 | Content: Views{ 16 | H1("Admin Page"), 17 | root.Navigation(), 18 | H3("Manage Users:"), 19 | UL( 20 | DynamicView( 21 | func(ctx *Context) (View, error) { 22 | url := views.Admin_User0.URL(ctx.ForURLArgs("ErikUnger")) 23 | return A(url, "ErikUnger"), nil 24 | }, 25 | ), 26 | DynamicView( 27 | func(ctx *Context) (View, error) { 28 | url := views.Admin_User0.URL(ctx.ForURLArgs("AlexTacho")) 29 | return A(url, "AlexTacho"), nil 30 | }, 31 | ), 32 | ), 33 | }, 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /states/transition.go: -------------------------------------------------------------------------------- 1 | package states 2 | 3 | 4 | type Transition struct { 5 | Event string 6 | From State 7 | To State 8 | } 9 | 10 | 11 | func (self *Transition) Veto() bool { 12 | if vetoer, ok := self.From.(TransitionVetoer); ok { 13 | if vetoer.TransitionVeto(self) { 14 | return true 15 | } 16 | } 17 | if vetoer, ok := self.To.(TransitionVetoer); ok { 18 | if vetoer.TransitionVeto(self) { 19 | return true 20 | } 21 | } 22 | return false 23 | } 24 | 25 | 26 | func (self *Transition) Do() { 27 | if self.From != nil { 28 | stateMachine := self.From.StateMachine() 29 | 30 | self.From.Leave(self) 31 | if stateMachine != nil { 32 | stateMachine.currentState = nil 33 | } 34 | 35 | if self.To == nil { 36 | if stateMachine != nil { 37 | stateMachine.Leave(self) 38 | } 39 | return 40 | } 41 | } 42 | 43 | self.To.Enter(self) 44 | if stateMachine := self.To.StateMachine(); stateMachine != nil { 45 | stateMachine.currentState = self.To 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /model/blob.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | // "io/ioutil" 5 | ) 6 | 7 | func NewBlob(value []byte) *Blob { 8 | return (*Blob)(&value) 9 | } 10 | 11 | /* 12 | Blob is just a bunch of bytes. 13 | Struct tag attributes: 14 | `model:"required"` 15 | */ 16 | type Blob []byte 17 | 18 | func (self *Blob) Get() []byte { 19 | return []byte(*self) 20 | } 21 | 22 | func (self *Blob) Set(value []byte) { 23 | *self = Blob(value) 24 | } 25 | 26 | func (self *Blob) String() string { 27 | return string(*self) 28 | } 29 | 30 | func (self *Blob) SetString(str string) error { 31 | *self = Blob(str) 32 | return nil 33 | } 34 | 35 | func (self *Blob) IsEmpty() bool { 36 | return len(*self) == 0 37 | } 38 | 39 | func (self *Blob) Required(metaData *MetaData) bool { 40 | return metaData.BoolAttrib(StructTagKey, "required") 41 | } 42 | 43 | func (self *Blob) Validate(metaData *MetaData) error { 44 | if self.Required(metaData) && self.IsEmpty() { 45 | return NewRequiredError(metaData) 46 | } 47 | return nil 48 | } 49 | -------------------------------------------------------------------------------- /view/views.go: -------------------------------------------------------------------------------- 1 | package view 2 | 3 | /////////////////////////////////////////////////////////////////////////////// 4 | // Views 5 | 6 | // Views implements the View interface for a slice of views. 7 | // The views of the slice are the child views. 8 | type Views []View 9 | 10 | func (self Views) Init(thisView View) { 11 | self.IterateChildren(func(parent View, child View) (next bool) { 12 | if child != nil { 13 | child.Init(child) 14 | } 15 | return true 16 | }) 17 | } 18 | 19 | func (self Views) ID() string { 20 | return "" 21 | } 22 | 23 | // Does not iterate nil children 24 | func (self Views) IterateChildren(callback IterateChildrenCallback) { 25 | for _, view := range self { 26 | if view != nil && !callback(self, view) { 27 | return 28 | } 29 | } 30 | } 31 | 32 | func (self Views) Render(ctx *Context) (err error) { 33 | for _, view := range self { 34 | if view != nil { 35 | err = view.Render(ctx) 36 | if err != nil { 37 | return err 38 | } 39 | } 40 | } 41 | return nil 42 | } 43 | -------------------------------------------------------------------------------- /view/indirecturl.go: -------------------------------------------------------------------------------- 1 | package view 2 | 3 | import ( 4 | "github.com/ungerik/go-start/errs" 5 | ) 6 | 7 | // IndirectURL encapsulates pointers to URL implementations. 8 | // To break circular dependencies, addresses of URL implementing variables 9 | // can be passed to this function that encapsulates it with an URL 10 | // implementation that dereferences the pointers at runtime. 11 | func IndirectURL(urlPtr interface{}) URL { 12 | switch s := urlPtr.(type) { 13 | case **Page: 14 | return &indirectPageURL{s} 15 | case *ViewWithURL: 16 | return IndirectViewWithURL(s) 17 | case *URL: 18 | return &indirectURL{s} 19 | } 20 | panic(errs.Format("%T not a pointer to a view.URL", urlPtr)) 21 | } 22 | 23 | type indirectURL struct { 24 | url *URL 25 | } 26 | 27 | func (self *indirectURL) URL(ctx *Context) string { 28 | return (*self.url).URL(ctx) 29 | } 30 | 31 | type indirectPageURL struct { 32 | page **Page 33 | } 34 | 35 | func (self *indirectPageURL) URL(ctx *Context) string { 36 | return (*self.page).URL(ctx) 37 | } 38 | -------------------------------------------------------------------------------- /mongomedia/config.go: -------------------------------------------------------------------------------- 1 | package mongomedia 2 | 3 | import ( 4 | "errors" 5 | 6 | // "github.com/ungerik/go-start/debug" 7 | "github.com/ungerik/go-start/media" 8 | "github.com/ungerik/go-start/mongo" 9 | ) 10 | 11 | var Config = Configuration{ 12 | GridFSName: "media", 13 | } 14 | 15 | type Configuration struct { 16 | GridFSName string 17 | Backend Backend 18 | } 19 | 20 | func (self *Configuration) Name() string { 21 | return "mongomedia" 22 | } 23 | 24 | func (self *Configuration) Init() error { 25 | if mongo.Database == nil { 26 | panic("Package mongo must be initialized before mongomedia") 27 | } 28 | self.Backend.Init(self.GridFSName) 29 | media.Config.Backend = &self.Backend 30 | return nil 31 | } 32 | 33 | func (self *Configuration) Close() error { 34 | return nil 35 | } 36 | 37 | // Init must be called after mongo.Init() 38 | func Init(name string) error { 39 | if name == "" { 40 | return errors.New("media.Init() called with empty name") 41 | } 42 | Config.GridFSName = name 43 | return Config.Init() 44 | } 45 | -------------------------------------------------------------------------------- /model/bool.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import "strconv" 4 | 5 | func NewBool(value bool) *Bool { 6 | return (*Bool)(&value) 7 | } 8 | 9 | /* 10 | Bool model value. 11 | Struct tag attributes: 12 | required 13 | */ 14 | type Bool bool 15 | 16 | func (self *Bool) Get() bool { 17 | return bool(*self) 18 | } 19 | 20 | func (self *Bool) Set(value bool) { 21 | *self = Bool(value) 22 | } 23 | 24 | func (self *Bool) String() string { 25 | return strconv.FormatBool(self.Get()) 26 | } 27 | 28 | func (self *Bool) SetString(str string) error { 29 | value, err := strconv.ParseBool(str) 30 | if err == nil { 31 | self.Set(value) 32 | } 33 | return err 34 | } 35 | 36 | func (self *Bool) IsEmpty() bool { 37 | return false 38 | } 39 | 40 | func (self *Bool) Required(metaData *MetaData) bool { 41 | return metaData.BoolAttrib(StructTagKey, "required") 42 | } 43 | 44 | func (self *Bool) Validate(metaData *MetaData) error { 45 | if self.Required(metaData) && *self == false { 46 | return NewRequiredError(metaData) 47 | } 48 | return nil 49 | } 50 | -------------------------------------------------------------------------------- /model/ref.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "labix.org/v2/mgo/bson" 5 | ) 6 | 7 | type Ref string 8 | 9 | func (self *Ref) String() string { 10 | return string(*self) 11 | } 12 | 13 | func (self *Ref) SetString(str string) (strconvErr error) { 14 | *self = Ref(str) 15 | return nil 16 | } 17 | 18 | func (self *Ref) IsEmpty() bool { 19 | return *self == "" 20 | } 21 | 22 | func (self *Ref) Required(metaData *MetaData) bool { 23 | return metaData.BoolAttrib(StructTagKey, "required") 24 | } 25 | 26 | func (self *Ref) StringID() string { 27 | return self.String() 28 | } 29 | 30 | // Implements bson.Setter for mongo.Ref compatibility. 31 | // Yes this bson dependency is dirty, but necessary 32 | // for transition form mongo.Ref to model.Ref. 33 | func (self *Ref) SetBSON(raw bson.Raw) error { 34 | var objectId *bson.ObjectId 35 | if raw.Unmarshal(&objectId) == nil { 36 | if objectId != nil { 37 | *self = Ref(objectId.Hex()) 38 | } else { 39 | *self = "" 40 | } 41 | return nil 42 | } 43 | return raw.Unmarshal(self) 44 | } 45 | -------------------------------------------------------------------------------- /view/sessiontracker.go: -------------------------------------------------------------------------------- 1 | package view 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | /////////////////////////////////////////////////////////////////////////////// 8 | // SessionTracker 9 | 10 | type SessionTracker interface { 11 | ID(ctx *Context) (id string) 12 | SetID(ctx *Context, id string) 13 | DeleteID(ctx *Context) 14 | } 15 | 16 | const sessionIdCookie = "gostart_sid" 17 | 18 | /////////////////////////////////////////////////////////////////////////////// 19 | // CookieSessionTracker 20 | 21 | // http://en.wikipedia.org/wiki/HTTP_cookie 22 | type CookieSessionTracker struct { 23 | } 24 | 25 | func (self *CookieSessionTracker) ID(ctx *Context) string { 26 | id, _ := ctx.Request.GetSecureCookie(sessionIdCookie) 27 | return id 28 | } 29 | 30 | func (self *CookieSessionTracker) SetID(ctx *Context, id string) { 31 | ctx.Response.SetSecureCookie(sessionIdCookie, id, 0, "/") 32 | } 33 | 34 | func (self *CookieSessionTracker) DeleteID(ctx *Context) { 35 | ctx.Response.SetSecureCookie(sessionIdCookie, "delete", -time.Now().Unix(), "/") 36 | } 37 | -------------------------------------------------------------------------------- /user/xingidentity.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | import ( 4 | "github.com/ungerik/go-start/model" 5 | "github.com/ungerik/go-start/view" 6 | ) 7 | 8 | /////////////////////////////////////////////////////////////////////////////// 9 | // XingIdentity 10 | 11 | type XingIdentity struct { 12 | ID model.String 13 | } 14 | 15 | func (self *XingIdentity) ProfileURL() string { 16 | if self == nil { 17 | return "" 18 | } 19 | if self.ID == "" { 20 | return "" 21 | } 22 | return "http://www.xing.com/profile/" + self.ID.Get() 23 | } 24 | 25 | func (self *XingIdentity) URL(ctx *view.Context) string { 26 | return self.ProfileURL() 27 | } 28 | 29 | func (self *XingIdentity) LinkContent(ctx *view.Context) view.View { 30 | if self == nil { 31 | return nil 32 | } 33 | return view.Escape(self.LinkTitle(ctx)) 34 | } 35 | 36 | func (self *XingIdentity) LinkTitle(ctx *view.Context) string { 37 | if self == nil { 38 | return "" 39 | } 40 | return self.ID.Get() 41 | } 42 | 43 | func (self *XingIdentity) LinkRel(ctx *view.Context) string { 44 | return "" 45 | } 46 | -------------------------------------------------------------------------------- /model/geolocation.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "fmt" 5 | // "github.com/ungerik/go-start/debug" 6 | ) 7 | 8 | // todo change data type 9 | 10 | type GeoLocation struct { 11 | Longitude Float 12 | Latitude Float 13 | } 14 | 15 | func (self *GeoLocation) String() string { 16 | return fmt.Sprintf("%v / %v", self.Longitude, self.Latitude) 17 | } 18 | 19 | func (self *GeoLocation) SetString(str string) error { 20 | // At the moment GeoLocation will be handled as a struct of Float, 21 | // so there is no value for GeoLocation itself when calling SetString 22 | // from Form, only two separate Floats for the struct. 23 | // That's why we ignore an empty string for the moment 24 | if str == "" { 25 | return nil 26 | } 27 | panic("not implemented") 28 | } 29 | 30 | func (self *GeoLocation) IsEmpty() bool { 31 | return false 32 | } 33 | 34 | func (self *GeoLocation) Required(metaData *MetaData) bool { 35 | return metaData.BoolAttrib(StructTagKey, "required") 36 | } 37 | 38 | func (self *GeoLocation) Validate(metaData *MetaData) error { 39 | return nil 40 | } 41 | -------------------------------------------------------------------------------- /user/skypeidentity.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | import ( 4 | "github.com/ungerik/go-start/model" 5 | "github.com/ungerik/go-start/view" 6 | ) 7 | 8 | /////////////////////////////////////////////////////////////////////////////// 9 | // SkypeIdentity 10 | 11 | type SkypeIdentity struct { 12 | ID model.String 13 | Confirmed model.DateTime 14 | } 15 | 16 | func (self *SkypeIdentity) CallURL() string { 17 | if self == nil { 18 | return "" 19 | } 20 | if self.ID == "" { 21 | return "" 22 | } 23 | return "skype:" + self.ID.Get() 24 | } 25 | 26 | func (self *SkypeIdentity) URL(ctx *view.Context) string { 27 | return self.CallURL() 28 | } 29 | 30 | func (self *SkypeIdentity) LinkContent(ctx *view.Context) view.View { 31 | if self == nil { 32 | return nil 33 | } 34 | return view.Escape(self.LinkTitle(ctx)) 35 | } 36 | 37 | func (self *SkypeIdentity) LinkTitle(ctx *view.Context) string { 38 | if self == nil { 39 | return "" 40 | } 41 | return self.ID.Get() 42 | } 43 | 44 | func (self *SkypeIdentity) LinkRel(ctx *view.Context) string { 45 | return "" 46 | } 47 | -------------------------------------------------------------------------------- /model/sliceiterator.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | 7 | // "github.com/ungerik/go-start/debug" 8 | "github.com/ungerik/go-start/reflection" 9 | ) 10 | 11 | func NewSliceIterator(slice interface{}) *SliceIterator { 12 | v := reflect.ValueOf(slice) 13 | if v.Kind() != reflect.Slice && v.Kind() != reflect.Array { 14 | panic(fmt.Errorf("Expected slice or array, got %T", slice)) 15 | } 16 | return &SliceIterator{slice: v} 17 | } 18 | 19 | func NewSliceOrErrorOnlyIterator(slice interface{}, err error) Iterator { 20 | if err != nil { 21 | return NewErrorOnlyIterator(err) 22 | } 23 | return NewSliceIterator(slice) 24 | } 25 | 26 | // SliceIterator 27 | type SliceIterator struct { 28 | slice reflect.Value 29 | index int 30 | } 31 | 32 | func (self *SliceIterator) Next(resultRef interface{}) bool { 33 | if self.index >= self.slice.Len() { 34 | return false 35 | } 36 | reflection.SmartCopy(self.slice.Index(self.index).Interface(), resultRef) 37 | self.index++ 38 | return true 39 | } 40 | 41 | func (self *SliceIterator) Err() error { 42 | return nil 43 | } 44 | -------------------------------------------------------------------------------- /model/time.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import "time" 4 | 5 | const unixDateSec int64 = (1969*365 + 1969/4 - 1969/100 + 1969/400) * (24 * 60 * 60) 6 | 7 | func NewTime(value int64) *Time { 8 | return (*Time)(&value) 9 | } 10 | 11 | // Time in milliseconds since January 1, year 1 00:00:00 UTC. 12 | // Time values are always in UTC. 13 | type Time int64 14 | 15 | func (self *Time) Get() time.Time { 16 | unixsec := int64(*self)/1000 - unixDateSec 17 | unixmsec := int64(*self) % 1000 18 | return time.Unix(unixsec, unixmsec*1e6) 19 | } 20 | 21 | func (self *Time) Set(t time.Time) { 22 | unixsec := t.Unix() 23 | unixmsec := int64(t.Nanosecond()) / 1e6 24 | *self = Time((unixsec+unixDateSec)*1000 + unixmsec) 25 | } 26 | 27 | func (self *Time) IsEmpty() bool { 28 | return *self == 0 29 | } 30 | 31 | func (self *Time) Required(metaData *MetaData) bool { 32 | return metaData.BoolAttrib(StructTagKey, "required") 33 | } 34 | 35 | func (self *Time) Validate(metaData *MetaData) error { 36 | if self.Required(metaData) && self.IsEmpty() { 37 | return NewRequiredError(metaData) 38 | } 39 | return nil 40 | } 41 | -------------------------------------------------------------------------------- /templatesystem/printf.go: -------------------------------------------------------------------------------- 1 | package templatesystem 2 | 3 | import ( 4 | "fmt" 5 | "github.com/ungerik/go-start/errs" 6 | "io" 7 | "io/ioutil" 8 | "path/filepath" 9 | "reflect" 10 | "strings" 11 | ) 12 | 13 | type PrintfTemplate struct { 14 | text string 15 | } 16 | 17 | func (self *PrintfTemplate) Render(out io.Writer, context interface{}) (err error) { 18 | var str string 19 | switch reflect.TypeOf(context).Kind() { 20 | case reflect.Slice, reflect.Array: 21 | panic("todo implementation") 22 | default: 23 | str = fmt.Sprintf(self.text, context) 24 | } 25 | if strings.HasPrefix(str, "%!") { 26 | return errs.Format(str) 27 | } 28 | _, err = out.Write([]byte(str)) 29 | return err 30 | } 31 | 32 | type Printf struct{} 33 | 34 | func (self *Printf) ParseFile(filename string) (Template, error) { 35 | text, err := ioutil.ReadFile(filename) 36 | if err != nil { 37 | return nil, err 38 | } 39 | return self.ParseString(string(text), filepath.Base(filename)) 40 | } 41 | 42 | func (self *Printf) ParseString(text, name string) (Template, error) { 43 | return &PrintfTemplate{text: text}, nil 44 | } 45 | -------------------------------------------------------------------------------- /examples/FullTutorial/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "email": { 3 | "Host": "smtp.gmail.com", 4 | "Username": "gostarttutorial@gmail.com", 5 | "Password": "gopher123", 6 | "From": { 7 | "Name": "Gopher" 8 | } 9 | }, 10 | "mongo": { 11 | "Database": "gostarttutorial", 12 | "User": "", 13 | "Password": "" 14 | }, 15 | "media": { 16 | "Admin": { 17 | "ButtonClass": "button" 18 | } 19 | }, 20 | "user": { 21 | "CollectionName": "users" 22 | }, 23 | "view": { 24 | "ListenAndServeAt": "0.0.0.0:80", 25 | "Debug": { 26 | "ListenAndServeAt": "0.0.0.0:8080" 27 | }, 28 | "ProductionServerIPs": [ 29 | ], 30 | "SiteName": "go-start Tutorial", 31 | "BaseDirs": [ 32 | ".", 33 | "../../", 34 | "../../media/" 35 | ], 36 | "RedirectSubdomains": ["www"], 37 | "Page": { 38 | "DefaultMetaViewport": "width=1000px" 39 | }, 40 | "CookieSecret": "Test123" 41 | } 42 | } -------------------------------------------------------------------------------- /examples/ViewPaths/views/admin/user_0/user_0.go: -------------------------------------------------------------------------------- 1 | package subpage 2 | 3 | import ( 4 | . "github.com/ungerik/go-start/view" 5 | 6 | "github.com/ungerik/go-start/examples/ViewPaths/views" 7 | "github.com/ungerik/go-start/examples/ViewPaths/views/root" 8 | ) 9 | 10 | func init() { 11 | views.Admin_User0 = &Page{ 12 | Title: RenderView( 13 | func(ctx *Context) error { 14 | // The username is in ctx.URLArgs[0] 15 | ctx.Response.WriteString("Manage " + ctx.URLArgs[0]) 16 | return nil 17 | }, 18 | ), 19 | Content: Views{ 20 | DynamicViewBindURLArgs( 21 | // The URL argument 0 can also be bound dynamically 22 | // to a function argument: 23 | func(ctx *Context, username string) (View, error) { 24 | return H1("Manage user ", username), nil 25 | }, 26 | ), 27 | root.Navigation(), 28 | DynamicViewBindURLArgs( 29 | func(ctx *Context, username string) (View, error) { 30 | return Views{ 31 | H4(Printf("This view uses the URL argument '%s':", username)), 32 | HTML(views.Admin_User0.URL(ctx)), 33 | }, nil 34 | }, 35 | ), 36 | }, 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /view/submitbutton.go: -------------------------------------------------------------------------------- 1 | package view 2 | 3 | type SubmitButton struct { 4 | ViewBaseWithId 5 | Name string 6 | Value interface{} 7 | Class string 8 | Disabled bool 9 | TabIndex int 10 | OnClick string 11 | OnClickConfirm string // Will add a confirmation dialog for onclick 12 | } 13 | 14 | func (self *SubmitButton) Render(ctx *Context) (err error) { 15 | ctx.Response.XML.OpenTag("input") 16 | ctx.Response.XML.AttribIfNotDefault("id", self.id) 17 | ctx.Response.XML.AttribIfNotDefault("class", self.Class) 18 | ctx.Response.XML.Attrib("type", "submit") 19 | ctx.Response.XML.AttribIfNotDefault("name", self.Name) 20 | ctx.Response.XML.AttribIfNotDefault("value", self.Value) 21 | ctx.Response.XML.AttribFlag("disabled", self.Disabled) 22 | ctx.Response.XML.AttribIfNotDefault("tabindex", self.TabIndex) 23 | if self.OnClickConfirm != "" { 24 | ctx.Response.XML.Attrib("onclick", self.OnClick, "; return confirm('", self.OnClickConfirm, "');") 25 | } else { 26 | ctx.Response.XML.AttribIfNotDefault("onclick", self.OnClick) 27 | } 28 | ctx.Response.XML.CloseTag() 29 | return nil 30 | } 31 | -------------------------------------------------------------------------------- /examples/ViewPaths/views/paths.go: -------------------------------------------------------------------------------- 1 | package views 2 | 3 | import ( 4 | . "github.com/ungerik/go-start/view" 5 | ) 6 | 7 | /* 8 | Declare variables for all views. 9 | Will be initialized by sub-packages. 10 | Declared here to make them accessible from all sub-packages, 11 | for instance to get their URL. 12 | */ 13 | var ( 14 | Homepage ViewWithURL 15 | 16 | Admin *Page 17 | Admin_User0 *Page // User0 means URL argument 0 defines the user for the view 18 | AdminAuth Authenticator 19 | 20 | GetXML ViewWithURL 21 | GetJSON ViewWithURL 22 | ) 23 | 24 | // Paths() returns the URL structure of the site 25 | func Paths() *ViewPath { 26 | return &ViewPath{View: Homepage, Sub: []ViewPath{ 27 | {Name: "get.xml", View: GetXML}, 28 | {Name: "get.json", View: GetJSON}, 29 | // Paths can have an Authenticator 30 | {Name: "admin", View: Admin, Auth: AdminAuth, Sub: []ViewPath{ 31 | // Args is the number arguments that get parsed from the URL. 32 | // URL for empty Name: /admin// 33 | // URL for Name = "user": /admin/user// 34 | {Args: 1, View: Admin_User0, Auth: AdminAuth}, 35 | }}, 36 | }} 37 | } 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | For licenses of dependencies see LICENSE files in their directories. 2 | 3 | MIT License 4 | Copyright (c) 2012 Erik Unger 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 7 | 8 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 9 | 10 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /examples/ViewPaths/README.md: -------------------------------------------------------------------------------- 1 | ## Demonstrates the basic setup of a site with an URL structure 2 | 3 | Views are defined in sub-directories of the "views" directory. 4 | The directory structure mimics the URL structure with the exception 5 | that root views are defined in the "views/root" directory instead of in "views". 6 | 7 | "views" contains a paths.go file where variables for all views and the 8 | URL structure get declared. "views" has no dependencies on its sub packages, 9 | so views with can be imported in all sub packages to access the variables 10 | for all views (usually to get their URL). 11 | 12 | If views have URL arguments, then the index of the URL argument is added 13 | to the package name (see user_0 below). 14 | 15 | * Project directory 16 | * config.json 17 | * main.go 18 | * views 19 | * paths.go 20 | * root 21 | * homepage.go 22 | * getjson.go 23 | * getxml.go 24 | * admin 25 | * admin.go 26 | * user_0 27 | * user_0.go 28 | 29 | 30 | Download, build and run example: 31 | 32 | go get github.com/ungerik/go-start/examples/ViewPaths 33 | go install github.com/ungerik/go-start/examples/ViewPaths && ViewPaths 34 | -------------------------------------------------------------------------------- /user/githubidentity.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | import ( 4 | "github.com/ungerik/go-start/model" 5 | "github.com/ungerik/go-start/view" 6 | ) 7 | 8 | /////////////////////////////////////////////////////////////////////////////// 9 | // GitHubIdentity 10 | 11 | type GitHubIdentity struct { 12 | ID model.String 13 | Name model.String 14 | Confirmed model.DateTime 15 | AccessToken model.String 16 | } 17 | 18 | func (self *GitHubIdentity) ProfileURL() string { 19 | if self.Name == "" { 20 | return "" 21 | } 22 | return "https://github.com/" + self.Name.Get() 23 | } 24 | 25 | func (self *GitHubIdentity) URL(ctx *view.Context) string { 26 | return self.ProfileURL() 27 | } 28 | 29 | func (self *GitHubIdentity) LinkContent(ctx *view.Context) view.View { 30 | return view.Escape(self.LinkTitle(ctx)) 31 | } 32 | 33 | func (self *GitHubIdentity) LinkTitle(ctx *view.Context) string { 34 | name := self.Name.Get() 35 | if name == "" { 36 | name = self.ID.Get() 37 | if name == "" { 38 | return "" 39 | } 40 | } 41 | return name 42 | } 43 | 44 | func (self *GitHubIdentity) LinkRel(ctx *view.Context) string { 45 | return "" 46 | } 47 | -------------------------------------------------------------------------------- /view/iframe.go: -------------------------------------------------------------------------------- 1 | package view 2 | 3 | /////////////////////////////////////////////////////////////////////////////// 4 | // Iframe 5 | 6 | type Iframe struct { 7 | ViewBaseWithId 8 | Class string 9 | Width int 10 | Height int 11 | Border int 12 | Scrolling bool 13 | MarginWidth int 14 | MarginHeight int 15 | Seamless bool 16 | URL string 17 | } 18 | 19 | func (self *Iframe) Render(ctx *Context) (err error) { 20 | ctx.Response.XML.OpenTag("iframe") 21 | ctx.Response.XML.AttribIfNotDefault("id", self.id) 22 | ctx.Response.XML.AttribIfNotDefault("class", self.Class) 23 | ctx.Response.XML.Attrib("width", self.Width).Attrib("height", self.Height) 24 | ctx.Response.XML.Attrib("frameborder", self.Border) 25 | ctx.Response.XML.Attrib("marginwidth", self.MarginWidth).Attrib("marginheight", self.MarginHeight) 26 | if self.Scrolling { 27 | ctx.Response.XML.Attrib("scrolling", "yes") 28 | } else { 29 | ctx.Response.XML.Attrib("scrolling", "no") 30 | } 31 | if self.Seamless { 32 | ctx.Response.XML.Attrib("seamless", "seamless") 33 | } 34 | ctx.Response.XML.Attrib("src", self.URL) 35 | ctx.Response.XML.CloseTagAlways() 36 | return nil 37 | } 38 | -------------------------------------------------------------------------------- /view/view.go: -------------------------------------------------------------------------------- 1 | package view 2 | 3 | type IterateChildrenCallback func(parent View, child View) (next bool) 4 | 5 | // View is the basic interface for all types in the view package. 6 | // A view can have an id, child views and renders its content to a XMLWriter. 7 | // nil is permitted as View value and will be ignored while rendering HTML. 8 | type View interface { 9 | Init(thisView View) 10 | ID() string 11 | IterateChildren(callback IterateChildrenCallback) 12 | // Everything written to out will be discarded if there was an error 13 | // out.Write() is not expected to return errors like bytes.Buffer 14 | Render(ctx *Context) (err error) 15 | } 16 | 17 | // ProductionServerView returns view if view.Config.IsProductionServer 18 | // is true, else nil which is a valid value for a View. 19 | func ProductionServerView(view View) View { 20 | if !Config.IsProductionServer { 21 | return nil 22 | } 23 | return view 24 | } 25 | 26 | // NotProductionServerView returns view if view.Config.IsProductionServer 27 | // is false, else nil which is a valid value for a View. 28 | func NonProductionServerView(view View) View { 29 | if Config.IsProductionServer { 30 | return nil 31 | } 32 | return view 33 | } 34 | -------------------------------------------------------------------------------- /view/css.go: -------------------------------------------------------------------------------- 1 | package view 2 | 3 | /* 4 | HTML5BoilerplateCSS returns a ViewWithURL that concatenates 5 | static files with HTML5 Boilerplate CSS normalization. 6 | 7 | Example: 8 | 9 | myCSS := HTML5BoilerplateCSS("common.css", "special.css") 10 | page := &Page{ 11 | CSS: myCSS, 12 | } 13 | */ 14 | func HTML5BoilerplateCSS(staticCssFilenames ...string) ViewWithURL { 15 | staticCssFilenames = append(staticCssFilenames, "css/html5boilerplate/poststyle.css") 16 | staticCssFilenames = append([]string{"css/html5boilerplate/normalize.css"}, staticCssFilenames...) 17 | return NewConcatStaticFiles(staticCssFilenames...) 18 | } 19 | 20 | // NewHTML5BoilerplateCSSTemplate returns a ViewWithURL that concatenates 21 | // text templates with HTML5 Boilerplate CSS normalization. 22 | func NewHTML5BoilerplateCSSTemplate(getContext GetTemplateContextFunc, filenames ...string) ViewWithURL { 23 | views := make(Views, len(filenames)+2) 24 | views[0] = NewStaticFile("css/html5boilerplate/normalize.css") 25 | for i := range filenames { 26 | views[i+1] = NewTemplate(filenames[i], getContext) 27 | } 28 | views[len(views)-1] = NewStaticFile("css/html5boilerplate/poststyle.css") 29 | return NewViewURLWrapper(views) 30 | } 31 | -------------------------------------------------------------------------------- /model/indexedsliceiterator.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | 7 | "github.com/ungerik/go-start/errs" 8 | "github.com/ungerik/go-start/reflection" 9 | ) 10 | 11 | func NewIndexedSliceIterator(slice interface{}, indices []int) *IndexedSliceIterator { 12 | v := reflect.ValueOf(slice) 13 | if v.Kind() != reflect.Slice && v.Kind() != reflect.Array { 14 | panic(fmt.Errorf("Expected slice or array, got %T", slice)) 15 | } 16 | return &IndexedSliceIterator{slice: v, indices: indices} 17 | } 18 | 19 | type IndexedSliceIterator struct { 20 | slice reflect.Value 21 | indices []int 22 | index int 23 | err error 24 | } 25 | 26 | func (self *IndexedSliceIterator) Next(resultRef interface{}) bool { 27 | if self.err != nil || self.index >= len(self.indices) { 28 | return false 29 | } 30 | if self.indices[self.index] >= self.slice.Len() { 31 | self.err = errs.Format("Index %d from indices greater or equal than length of slice %d", self.indices[self.index], self.slice.Len()) 32 | return false 33 | } 34 | reflection.SmartCopy(self.slice.Index(self.indices[self.index]), resultRef) 35 | self.index++ 36 | return false 37 | } 38 | 39 | func (self *IndexedSliceIterator) Err() error { 40 | return self.err 41 | } 42 | -------------------------------------------------------------------------------- /view/checkbox.go: -------------------------------------------------------------------------------- 1 | package view 2 | 3 | /////////////////////////////////////////////////////////////////////////////// 4 | // Checkbox 5 | 6 | // Checkbox represents a HTML input element of type checkbox. 7 | type Checkbox struct { 8 | ViewBaseWithId 9 | Name string 10 | Label string 11 | Checked bool 12 | Disabled bool 13 | Class string 14 | } 15 | 16 | func (self *Checkbox) Render(ctx *Context) (err error) { 17 | ctx.Response.XML.OpenTag("input") 18 | ctx.Response.XML.Attrib("id", self.ID()) 19 | ctx.Response.XML.AttribIfNotDefault("class", self.Class) 20 | ctx.Response.XML.Attrib("type", "checkbox") 21 | ctx.Response.XML.Attrib("name", self.Name) 22 | ctx.Response.XML.Attrib("value", "true") 23 | ctx.Response.XML.AttribFlag("disabled", self.Disabled) 24 | ctx.Response.XML.AttribFlag("checked", self.Checked) 25 | ctx.Response.XML.CloseTag() 26 | 27 | if self.Label != "" { 28 | ctx.Response.XML.OpenTag("label").Attrib("for", self.ID()).Content(self.Label).CloseTag() 29 | } 30 | return nil 31 | } 32 | 33 | //func (self *Checkbox) SetName(name string) { 34 | // self.Name = name 35 | // ViewChanged(self) 36 | //} 37 | // 38 | //func (self *Checkbox) SetLabel(label string) { 39 | // self.Label = label 40 | // ViewChanged(self) 41 | //} 42 | -------------------------------------------------------------------------------- /examples/FullTutorial/views/admin/shared.go: -------------------------------------------------------------------------------- 1 | package admin 2 | 3 | import ( 4 | . "github.com/ungerik/go-start/examples/FullTutorial/views" 5 | . "github.com/ungerik/go-start/view" 6 | ) 7 | 8 | func NewAdminPage(title string, main View) *Page { 9 | return &Page{ 10 | Title: Escape(title), 11 | // CSS: IndirectURL(&Admin_CSS), 12 | Scripts: Renderers{ 13 | JQuery, 14 | }, 15 | Content: Views{ 16 | DIV("header", 17 | &Link{ 18 | Class: "title", 19 | Model: &PageLink{ 20 | Page: &Admin, 21 | Content: H1(&Image{Class: "logo", Src: "/images/gopher.png"}, HTML("Admin Panel")), 22 | }, 23 | }, 24 | HeaderUserNav(), 25 | DIV("menu-frame", 26 | &Menu{ 27 | Class: "menu", 28 | ItemClass: "menu-item", 29 | ActiveItemClass: "active", 30 | BetweenItems: "  /  ", 31 | Items: []LinkModel{ 32 | NewPageLink(&Admin, "Dashboard"), 33 | NewPageLink(&Admin_Users, "Users"), 34 | NewPageLink(&Admin_Images, "Images"), 35 | }, 36 | }, 37 | DivClearBoth(), 38 | ), 39 | ), 40 | DIV("content", 41 | DIV("center", 42 | DIV("main", main), 43 | DivClearBoth(), 44 | ), 45 | ), 46 | Footer(), 47 | }, 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /view/if.go: -------------------------------------------------------------------------------- 1 | package view 2 | 3 | /////////////////////////////////////////////////////////////////////////////// 4 | // If 5 | 6 | type If struct { 7 | ViewBaseWithId 8 | Condition bool 9 | Content View 10 | ElseContent View 11 | } 12 | 13 | func (self *If) Init(thisView View) { 14 | if thisView == self.thisView { 15 | return // already initialized 16 | } 17 | self.ViewBaseWithId.Init(thisView) 18 | 19 | // ViewBaseWithId.Init() initializes the child reported by IterateChildren(), 20 | // we need to initialize the child for the other case of !self.Condition 21 | var child View 22 | if !self.Condition { 23 | child = self.Content 24 | } else { 25 | child = self.ElseContent 26 | } 27 | if child != nil { 28 | child.Init(child) 29 | } 30 | } 31 | 32 | func (self *If) IterateChildren(callback IterateChildrenCallback) { 33 | var child View 34 | if self.Condition { 35 | child = self.Content 36 | } else { 37 | child = self.ElseContent 38 | } 39 | if child != nil { 40 | callback(self, child) 41 | } 42 | } 43 | 44 | func (self *If) Render(ctx *Context) (err error) { 45 | content := self.Content 46 | if !self.Condition { 47 | content = self.ElseContent 48 | } 49 | if content == nil { 50 | return nil 51 | } 52 | return content.Render(ctx) 53 | } 54 | -------------------------------------------------------------------------------- /view/staticfile.go: -------------------------------------------------------------------------------- 1 | package view 2 | 3 | import ( 4 | "io/ioutil" 5 | "path" 6 | "time" 7 | 8 | "github.com/ungerik/go-start/errs" 9 | ) 10 | 11 | func NewStaticFile(filename string) *StaticFile { 12 | return &StaticFile{Filename: filename} 13 | } 14 | 15 | // StaticFile renders a static file. 16 | // The output is cached in memory but changes to the file on the filesystem 17 | // cause the the cache to be rebuilt. 18 | type StaticFile struct { 19 | ViewWithURLBase 20 | Filename string 21 | // Will be set automatically from Filename if empty 22 | ContentTypeExt string 23 | modifiedTime time.Time 24 | cachedFileData []byte 25 | } 26 | 27 | func (self *StaticFile) Render(ctx *Context) (err error) { 28 | filePath, found, modified := FindStaticFile(self.Filename) 29 | if !found { 30 | return errs.Format("Static file not found: %s", self.Filename) 31 | } 32 | 33 | if self.ContentTypeExt == "" { 34 | self.ContentTypeExt = path.Ext(filePath) 35 | } 36 | 37 | if self.cachedFileData == nil || modified.After(self.modifiedTime) { 38 | self.cachedFileData, err = ioutil.ReadFile(filePath) 39 | if err != nil { 40 | return err 41 | } 42 | } 43 | 44 | ctx.Response.SetContentTypeByExt(self.ContentTypeExt) 45 | ctx.Response.Write(self.cachedFileData) 46 | return nil 47 | } 48 | -------------------------------------------------------------------------------- /view/button.go: -------------------------------------------------------------------------------- 1 | package view 2 | 3 | type Button struct { 4 | ViewBaseWithId 5 | Name string 6 | Class string 7 | Disabled bool 8 | TabIndex int 9 | OnClick string 10 | OnClickConfirm string // Will add a confirmation dialog for onclick 11 | Content View // Only used when Submit is false 12 | } 13 | 14 | func (self *Button) IterateChildren(callback IterateChildrenCallback) { 15 | if self.Content != nil { 16 | callback(self, self.Content) 17 | } 18 | } 19 | 20 | func (self *Button) Render(ctx *Context) (err error) { 21 | ctx.Response.XML.OpenTag("button") 22 | ctx.Response.XML.AttribIfNotDefault("id", self.id) 23 | ctx.Response.XML.AttribIfNotDefault("class", self.Class) 24 | ctx.Response.XML.Attrib("type", "button") 25 | ctx.Response.XML.AttribIfNotDefault("name", self.Name) 26 | ctx.Response.XML.AttribFlag("disabled", self.Disabled) 27 | ctx.Response.XML.AttribIfNotDefault("tabindex", self.TabIndex) 28 | if self.OnClickConfirm != "" { 29 | ctx.Response.XML.Attrib("onclick", "return confirm('", self.OnClickConfirm, "');") 30 | } else { 31 | ctx.Response.XML.AttribIfNotDefault("onclick", self.OnClick) 32 | } 33 | if self.Content != nil { 34 | err = self.Content.Render(ctx) 35 | } 36 | ctx.Response.XML.CloseTagAlways() 37 | return err 38 | } 39 | -------------------------------------------------------------------------------- /view/link.go: -------------------------------------------------------------------------------- 1 | package view 2 | 3 | // Link represents an HTML or element depending on UseLinkTag. 4 | // Content and title of the Model will only be rendered for . 5 | type Link struct { 6 | ViewBaseWithId 7 | Class string 8 | Model LinkModel 9 | NewWindow bool 10 | UseLinkTag bool 11 | } 12 | 13 | func (self *Link) Render(ctx *Context) (err error) { 14 | if self.UseLinkTag { 15 | ctx.Response.XML.OpenTag("link") 16 | } else { 17 | ctx.Response.XML.OpenTag("a") 18 | } 19 | ctx.Response.XML.AttribIfNotDefault("id", self.id) 20 | ctx.Response.XML.AttribIfNotDefault("class", self.Class) 21 | if self.NewWindow { 22 | ctx.Response.XML.Attrib("target", "_blank") 23 | } 24 | if self.Model != nil { 25 | ctx.Response.XML.Attrib("href", self.Model.URL(ctx)) 26 | ctx.Response.XML.AttribIfNotDefault("rel", self.Model.LinkRel(ctx)) 27 | } 28 | if self.UseLinkTag { 29 | ctx.Response.XML.CloseTag() // link 30 | } else { 31 | ctx.Response.XML.AttribIfNotDefault("title", self.Model.LinkTitle(ctx)) 32 | content := self.Model.LinkContent(ctx) 33 | if content != nil { 34 | err = content.Render(ctx) 35 | } 36 | ctx.Response.XML.CloseTagAlways() // a 37 | } 38 | return err 39 | } 40 | 41 | func (self *Link) URL(ctx *Context) string { 42 | return self.Model.URL(ctx) 43 | } 44 | -------------------------------------------------------------------------------- /examples/FullTutorial/views/paths.go: -------------------------------------------------------------------------------- 1 | package views 2 | 3 | import ( 4 | "github.com/ungerik/go-start/media" 5 | // "github.com/ungerik/go-start/debug" 6 | . "github.com/ungerik/go-start/view" 7 | ) 8 | 9 | var ( 10 | CSS View 11 | Homepage ViewWithURL 12 | LoginSignup *Page 13 | Logout ViewWithURL 14 | ConfirmEmail *Page 15 | Profile *Page 16 | Admin *Page 17 | Admin_Auth Authenticator 18 | Admin_Users *Page 19 | Admin_ExportEmails ViewWithURL 20 | Admin_UserX *Page 21 | Admin_Images *Page 22 | ) 23 | 24 | func Paths() *ViewPath { 25 | return &ViewPath{View: Homepage, Sub: []ViewPath{ 26 | media.ViewPath("media"), 27 | {Name: "style.css", View: CSS}, 28 | {Name: "admin", View: Admin, Auth: Admin_Auth, Sub: []ViewPath{ 29 | {Name: "users", View: Admin_Users, Auth: Admin_Auth, Sub: []ViewPath{ 30 | {Name: "export-emails", View: Admin_ExportEmails, Auth: Admin_Auth}, 31 | }}, 32 | {Name: "user", Args: 1, View: Admin_UserX, Auth: Admin_Auth}, 33 | {Name: "images", View: Admin_Images, Auth: Admin_Auth}, 34 | }}, 35 | {Name: "login", View: LoginSignup, Sub: []ViewPath{ 36 | {Name: "confirm", View: ConfirmEmail}, 37 | }}, 38 | {Name: "logout", View: Logout}, 39 | {Name: "profile", View: Profile}, 40 | }} 41 | } 42 | -------------------------------------------------------------------------------- /mongo/mongoiterator.go: -------------------------------------------------------------------------------- 1 | package mongo 2 | 3 | import ( 4 | "github.com/ungerik/go-start/model" 5 | "labix.org/v2/mgo" 6 | ) 7 | 8 | /////////////////////////////////////////////////////////////////////////////// 9 | // MongoIterator 10 | 11 | func NewMongoIterator(query Query) model.Iterator { 12 | mgoQuery, err := query.mongoQuery() 13 | if err != nil { 14 | return model.NewErrorOnlyIterator(err) 15 | } 16 | return &MongoIterator{collection: query.Collection(), iter: mgoQuery.Iter()} 17 | // mgoIter := mgoQuery.Iter() 18 | // collection, selectors := collectionAndSubDocumentSelectors(query) 19 | // return &MongoIterator{collection: collection, selectors: selectors, iter: mgoIter} 20 | } 21 | 22 | type MongoIterator struct { 23 | collection *Collection 24 | // selectors []string 25 | iter *mgo.Iter 26 | } 27 | 28 | func (self *MongoIterator) Next(resultRef interface{}) bool { 29 | if self.iter.Err() != nil { 30 | return false 31 | } 32 | if !self.iter.Next(resultRef) { 33 | return false 34 | } 35 | // resultRef has to be initialized again, 36 | // because mgo zeros the struct while unmarshalling. 37 | // Newly created slice elements need to be initialized too 38 | self.collection.InitDocument(documentFromResultPtr(resultRef)) 39 | return true 40 | } 41 | 42 | func (self *MongoIterator) Err() error { 43 | return self.iter.Err() 44 | } 45 | -------------------------------------------------------------------------------- /model/sortiterator.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "github.com/ungerik/go-start/reflection" 5 | "reflect" 6 | ) 7 | 8 | func NewSortIterator(iterator Iterator, compareFunc interface{}) *SortIterator { 9 | return &SortIterator{Iterator: iterator, CompareFunc: compareFunc} 10 | } 11 | 12 | type SortIterator struct { 13 | Iterator Iterator 14 | CompareFunc interface{} 15 | sliceIterator *SliceIterator 16 | err error 17 | } 18 | 19 | func (self *SortIterator) Next(resultRef interface{}) bool { 20 | if self.err != nil { 21 | return false 22 | } 23 | 24 | if self.sliceIterator == nil { 25 | f, err := reflection.NewSortCompareFunc(self.CompareFunc) 26 | if err != nil { 27 | self.err = err 28 | return false 29 | } 30 | 31 | slice := make([]interface{}, 0, 16) 32 | result := reflect.New(f.ArgType).Interface() 33 | for self.Iterator.Next(result) { 34 | slice = append(slice, result) 35 | result = reflect.New(f.ArgType).Interface() 36 | } 37 | self.err = self.Iterator.Err() 38 | if self.err != nil { 39 | return false 40 | } 41 | 42 | self.err = f.Sort(slice) 43 | if self.err != nil { 44 | return false 45 | } 46 | 47 | self.sliceIterator = NewSliceIterator(slice) 48 | } 49 | 50 | return self.sliceIterator.Next(resultRef) 51 | } 52 | 53 | func (self *SortIterator) Err() error { 54 | return self.err 55 | } 56 | -------------------------------------------------------------------------------- /view/tag.go: -------------------------------------------------------------------------------- 1 | package view 2 | 3 | /////////////////////////////////////////////////////////////////////////////// 4 | // Tag 5 | 6 | // Tag represents an arbitrary HTML element. 7 | type Tag struct { 8 | ViewBaseWithId 9 | Tag string 10 | Content View 11 | Class string 12 | Attribs map[string]string 13 | ExtraClose bool 14 | } 15 | 16 | func (self *Tag) IterateChildren(callback IterateChildrenCallback) { 17 | if self.Content != nil { 18 | callback(self, self.Content) 19 | } 20 | } 21 | 22 | func (self *Tag) Render(ctx *Context) (err error) { 23 | ctx.Response.XML.OpenTag(self.Tag) 24 | ctx.Response.XML.AttribIfNotDefault("id", self.id) 25 | ctx.Response.XML.AttribIfNotDefault("class", self.Class) 26 | for key, value := range self.Attribs { 27 | ctx.Response.XML.Attrib(key, value) 28 | } 29 | if self.Content != nil { 30 | err = self.Content.Render(ctx) 31 | } 32 | if self.ExtraClose { 33 | ctx.Response.XML.CloseTagAlways() 34 | } else { 35 | ctx.Response.XML.CloseTag() 36 | } 37 | return err 38 | } 39 | 40 | //func (self *Tag) SetClass(class string) { 41 | // self.Class = class 42 | // ViewChanged(self) 43 | //} 44 | // 45 | //func (self *Tag) SetContent(content View) { 46 | // self.Content = content 47 | // ViewChanged(self) 48 | //} 49 | // 50 | //func (self *Tag) SetTag(tag string) { 51 | // self.Tag = tag 52 | // ViewChanged(self) 53 | //} 54 | -------------------------------------------------------------------------------- /utils/stringwriter.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "strconv" 7 | ) 8 | 9 | type StringWriter struct { 10 | writer io.Writer 11 | } 12 | 13 | func (self *StringWriter) Write(strings ...string) *StringWriter { 14 | for _, str := range strings { 15 | self.writer.Write([]byte(str)) 16 | } 17 | return self 18 | } 19 | 20 | func (self *StringWriter) Printf(format string, args ...interface{}) *StringWriter { 21 | fmt.Fprintf(self.writer, format, args...) 22 | return self 23 | } 24 | 25 | func (self *StringWriter) Byte(value byte) *StringWriter { 26 | self.writer.Write([]byte{value}) 27 | return self 28 | } 29 | 30 | func (self *StringWriter) WriteBytes(bytes []byte) *StringWriter { 31 | self.writer.Write(bytes) 32 | return self 33 | } 34 | 35 | func (self *StringWriter) Int(value int) *StringWriter { 36 | self.writer.Write([]byte(strconv.Itoa(value))) 37 | return self 38 | } 39 | 40 | func (self *StringWriter) Uint(value uint) *StringWriter { 41 | self.writer.Write([]byte(strconv.FormatUint(uint64(value), 10))) 42 | return self 43 | } 44 | 45 | func (self *StringWriter) Float(value float64) *StringWriter { 46 | self.writer.Write([]byte(strconv.FormatFloat(value, 'f', -1, 64))) 47 | return self 48 | } 49 | 50 | func (self *StringWriter) Bool(value bool) *StringWriter { 51 | self.writer.Write([]byte(strconv.FormatBool(value))) 52 | return self 53 | } 54 | -------------------------------------------------------------------------------- /templates/404.html: -------------------------------------------------------------------------------- 1 | 2 | Page Not Found 3 | 15 | 16 |

17 |

Not found :(

18 |
19 |

Sorry, but the page you were trying to view does not exist.

20 |

It looks like this was the result of either:

21 |
    22 |
  • a mistyped address
  • 23 |
  • an out-of-date link
  • 24 |
25 |
26 | 27 | 31 | 32 |
33 | -------------------------------------------------------------------------------- /user/config.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/ungerik/go-start/mongo" 7 | ) 8 | 9 | var Config = Configuration{ 10 | ConfirmationMessage: ConfirmationMessage{ 11 | EmailSubject: "Please confirm your email address for %s", 12 | EmailMessage: "Please confirm your email address for %s by opening the following link:\n\n%s", 13 | Sent: "We sent you an email with a verification link. It might some time to show up, but when it does you will be ready to use this site.", 14 | }, 15 | } 16 | 17 | func Init(collection *mongo.Collection) { 18 | Config.Collection = collection 19 | Config.CollectionName = collection.Name 20 | } 21 | 22 | type ConfirmationMessage struct { 23 | EmailSubject string 24 | EmailMessage string 25 | Sent string 26 | } 27 | 28 | type Configuration struct { 29 | ConfirmationMessage ConfirmationMessage 30 | CollectionName string 31 | Collection *mongo.Collection 32 | } 33 | 34 | func (self *Configuration) Name() string { 35 | return "user" 36 | } 37 | 38 | func (self *Configuration) Init() error { 39 | collection, found := mongo.CollectionByName(self.CollectionName) 40 | if !found { 41 | return fmt.Errorf("Can't find mongo collection with name '%s'", self.CollectionName) 42 | } 43 | self.Collection = collection 44 | return nil 45 | } 46 | 47 | func (self *Configuration) Close() error { 48 | return nil 49 | } 50 | -------------------------------------------------------------------------------- /utils/datetime.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import "time" 4 | 5 | func ConvertTimeString(value, formatIn, formatOut string) (result string, err error) { 6 | if value == "" { 7 | return "", nil 8 | } 9 | t, err := time.Parse(formatIn, value) 10 | if err != nil { 11 | return "", err 12 | } 13 | return t.Format(formatOut), nil 14 | } 15 | 16 | func DayTimeRange(someTimeOfTheDay time.Time) (from, until time.Time) { 17 | from = DayBeginningTime(someTimeOfTheDay) 18 | until = from.Add(time.Hour * 24) 19 | return from, until 20 | } 21 | 22 | func TimeInRange(t, from, until time.Time) bool { 23 | return (t.Equal(from) || t.After(from)) && (t.Before(until) || t.Equal(until)) 24 | } 25 | 26 | func DayBeginningTime(someTimeOfTheDay time.Time) time.Time { 27 | year, month, day := someTimeOfTheDay.Date() 28 | return time.Date(year, month, day, 0, 0, 0, 0, someTimeOfTheDay.Location()) 29 | } 30 | 31 | type SortableTimeSlice []time.Time 32 | 33 | // Len is the number of elements in the collection. 34 | func (self SortableTimeSlice) Len() int { 35 | return len(self) 36 | } 37 | 38 | // Less returns whether the element with index i should sort 39 | // before the element with index j. 40 | func (self SortableTimeSlice) Less(i, j int) bool { 41 | return self[i].Before(self[j]) 42 | } 43 | 44 | // Swap swaps the elements with indexes i and j. 45 | func (self SortableTimeSlice) Swap(i, j int) { 46 | self[i], self[j] = self[j], self[i] 47 | } 48 | -------------------------------------------------------------------------------- /model/email.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import "github.com/ungerik/go-mail" 4 | 5 | func NewEmail(value string) *Email { 6 | return (*Email)(&value) 7 | } 8 | 9 | type Email string 10 | 11 | func (self *Email) Get() string { 12 | return string(*self) 13 | } 14 | 15 | func (self *Email) Set(value string) (err error) { 16 | *self = Email(value) // set value in any case, so that user can see wrong value in form 17 | 18 | if value != "" { 19 | if value, err = email.ValidateAddress(value); err != nil { 20 | return err 21 | } 22 | } 23 | *self = Email(value) 24 | return nil 25 | } 26 | 27 | func (self *Email) IsEmpty() bool { 28 | return *self == "" 29 | } 30 | 31 | func (self *Email) String() string { 32 | return self.Get() 33 | } 34 | 35 | func (self *Email) EqualsCaseinsensitive(address string) bool { 36 | return email.CompareAddressesCaseinsensitive(self.Get(), address) 37 | } 38 | 39 | func (self *Email) SetString(str string) (err error) { 40 | return self.Set(str) 41 | } 42 | 43 | func (self *Email) FixValue(metaData *MetaData) { 44 | } 45 | 46 | func (self *Email) Validate(metaData *MetaData) error { 47 | str := self.Get() 48 | if self.Required(metaData) || str != "" { 49 | if _, err := email.ValidateAddress(str); err != nil { 50 | return err 51 | } 52 | } 53 | return nil 54 | } 55 | 56 | func (self *Email) Required(metaData *MetaData) bool { 57 | return metaData.BoolAttrib(StructTagKey, "required") 58 | } 59 | -------------------------------------------------------------------------------- /view/contactform.go: -------------------------------------------------------------------------------- 1 | package view 2 | 3 | import ( 4 | "fmt" 5 | "github.com/ungerik/go-mail" 6 | "github.com/ungerik/go-start/model" 7 | ) 8 | 9 | // ContactFormModel is a default form model for contact forms. 10 | type ContactFormModel struct { 11 | Name model.String `view:"label=Your name" model:"maxlen=40"` 12 | Email model.Email `view:"label=Your email address" model:"required|maxlen=40"` 13 | Subject model.String `view:"label=Subject" model:"maxlen=40"` 14 | Message model.Text `view:"label=Your message|cols=40|rows=10" model:"required"` 15 | } 16 | 17 | // NewContactForm creates a new contact form that sends submitted data to recipientEmail. 18 | func NewContactForm(recipientEmail, subjectPrefix, formClass, buttonClass, formID string) *Form { 19 | return &Form{ 20 | Class: formClass, 21 | SubmitButtonClass: buttonClass, 22 | SubmitButtonText: "Send", 23 | SuccessMessage: "Message sent", 24 | FormID: formID, 25 | GetModel: func(form *Form, ctx *Context) (interface{}, error) { 26 | return &ContactFormModel{}, nil 27 | }, 28 | OnSubmit: func(form *Form, formModel interface{}, ctx *Context) (string, URL, error) { 29 | model := formModel.(*ContactFormModel) 30 | subject := fmt.Sprintf("%sFrom %s <%s>: %s", subjectPrefix, model.Name, model.Email, model.Subject) 31 | err := email.NewBriefMessage(subject, model.Message.Get(), recipientEmail).Send() 32 | return "", nil, err 33 | }, 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /view/modaldialog.go: -------------------------------------------------------------------------------- 1 | package view 2 | 3 | import "fmt" 4 | 5 | type ModalDialog struct { 6 | ViewBaseWithId 7 | Class string 8 | Style string 9 | Content View 10 | } 11 | 12 | func (self *ModalDialog) IterateChildren(callback IterateChildrenCallback) { 13 | if self.Content != nil { 14 | callback(self, self.Content) 15 | } 16 | } 17 | 18 | func (self *ModalDialog) Render(ctx *Context) (err error) { 19 | ctx.Response.RequireStyleURL("/css/avgrund.css", 0) 20 | ctx.Response.RequireScriptURL("/js/libs/avgrund.js", 0) 21 | ctx.Response.RequireScript(`jQuery(function(){jQuery("body").append("
");});`, 0) 22 | 23 | ctx.Response.XML.OpenTag("aside") 24 | ctx.Response.XML.Attrib("id", self.ID()) 25 | ctx.Response.XML.Attrib("class", "avgrund-popup "+self.Class) 26 | ctx.Response.XML.AttribIfNotDefault("style", self.Style) 27 | if self.Content != nil { 28 | err = self.Content.Render(ctx) 29 | } 30 | ctx.Response.XML.CloseTagAlways() 31 | return err 32 | } 33 | 34 | func (self *ModalDialog) OpenScript() string { 35 | return fmt.Sprintf("Avgrund.show('#%s');", self.ID()) 36 | } 37 | 38 | func (self *ModalDialog) OpenButton(text string) *Button { 39 | return &Button{Content: Escape(text), OnClick: self.OpenScript()} 40 | } 41 | 42 | const ModalDialogCloseScript = "Avgrund.hide();" 43 | 44 | func ModalDialogCloseButton(text string) *Button { 45 | return &Button{Content: Escape(text), OnClick: ModalDialogCloseScript} 46 | } 47 | -------------------------------------------------------------------------------- /modelext/name.go: -------------------------------------------------------------------------------- 1 | package modelext 2 | 3 | import ( 4 | "github.com/ungerik/go-start/model" 5 | "github.com/ungerik/go-start/utils" 6 | "strings" 7 | ) 8 | 9 | var NameDocLabelSelectors = []string{"Name.Prefix", "Name.First", "Name.Middle", "Name.Last", "Name.Postfix", "Name.Organization"} 10 | 11 | type Name struct { 12 | Prefix model.String `view:"size=10"` 13 | First model.String `view:"size=20|label=Given"` 14 | Middle model.String `view:"size=20"` 15 | Last model.String `view:"size=20|label=Family"` 16 | Postfix model.String `view:"size=10"` 17 | Organization model.String `view:"size=40"` 18 | } 19 | 20 | func (self *Name) SetForPerson(prefix, first, middle, last, postfix string) { 21 | self.Prefix.Set(prefix) 22 | self.First.Set(strings.Title(strings.ToLower(first))) 23 | self.Middle.Set(strings.Title(strings.ToLower(middle))) 24 | self.Last.Set(strings.Title(strings.ToLower(last))) 25 | self.Postfix.Set(postfix) 26 | self.Organization = "" 27 | } 28 | 29 | func (self *Name) SetForOrganization(organization string) { 30 | self.Prefix = "" 31 | self.First = "" 32 | self.Middle = "" 33 | self.Last = "" 34 | self.Postfix = "" 35 | self.Organization.Set(organization) 36 | } 37 | 38 | func (self *Name) String() string { 39 | if self.Organization != "" { 40 | return self.Organization.Get() 41 | } 42 | return utils.JoinNonEmptyStrings(" ", self.Prefix.Get(), self.First.Get(), self.Middle.Get(), self.Last.Get(), self.Postfix.Get()) 43 | } 44 | -------------------------------------------------------------------------------- /states/statemachine.go: -------------------------------------------------------------------------------- 1 | // A state machine - not ready yet. 2 | package states 3 | 4 | 5 | type StateMachine struct { 6 | name string 7 | States []State 8 | Transitions []*Transition 9 | currentState State 10 | parent *StateMachine 11 | } 12 | 13 | 14 | func (self *StateMachine) SetStateMachine(parent *StateMachine) { 15 | self.parent = parent 16 | } 17 | 18 | 19 | func (self *StateMachine) StateMachine() *StateMachine { 20 | return self.parent 21 | } 22 | 23 | 24 | func (self *StateMachine) Name() string { 25 | return self.name 26 | } 27 | 28 | 29 | func (self *StateMachine) Enter(transition *Transition) { 30 | if self.currentState != nil { 31 | panic("Current state machine already entered") 32 | } 33 | 34 | transition = &Transition{} 35 | for _, state := range self.States { 36 | transition.To = state 37 | if !transition.Veto() { 38 | transition.Do() 39 | return 40 | } 41 | } 42 | 43 | transition.From = self 44 | transition.To = nil 45 | transition.Do() 46 | } 47 | 48 | 49 | func (self *StateMachine) Leave(transition *Transition) { 50 | } 51 | 52 | 53 | func (self *StateMachine) CurrentState() State { 54 | return self.currentState 55 | } 56 | 57 | 58 | func (self *StateMachine) Event(event string) { 59 | if self.currentState == nil { 60 | panic("State machine can't handle event because it's not active") 61 | } 62 | 63 | for _, tr := range self.Transitions { 64 | if tr.From == self.currentState && tr.Event == event && !tr.Veto() { 65 | tr.Do() 66 | return 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /user/linkedinidentity.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | import ( 4 | "github.com/ungerik/go-start/model" 5 | "github.com/ungerik/go-start/view" 6 | "strings" 7 | ) 8 | 9 | /////////////////////////////////////////////////////////////////////////////// 10 | // LinkedInIdentity 11 | 12 | type LinkedInIdentity struct { 13 | ID model.String 14 | Name model.String 15 | Confirmed model.DateTime 16 | AccessToken model.String 17 | } 18 | 19 | func (self *LinkedInIdentity) ProfileURL() string { 20 | if self == nil { 21 | return "" 22 | } 23 | if !self.Name.IsEmpty() { 24 | name := self.Name.Get() 25 | if strings.IndexRune(name, '/') == -1 { 26 | return "http://linkedin.com/in/" + name 27 | } else { 28 | return "http://linkedin.com/pub/" + name 29 | } 30 | } 31 | if !self.ID.IsEmpty() { 32 | return "http://linkedin.com/profile/view?id=" + self.ID.Get() 33 | } 34 | return "" 35 | } 36 | 37 | func (self *LinkedInIdentity) URL(ctx *view.Context) string { 38 | return self.ProfileURL() 39 | } 40 | 41 | func (self *LinkedInIdentity) LinkContent(ctx *view.Context) view.View { 42 | if self == nil { 43 | return nil 44 | } 45 | return view.Escape(self.LinkTitle(ctx)) 46 | } 47 | 48 | func (self *LinkedInIdentity) LinkTitle(ctx *view.Context) string { 49 | if self == nil { 50 | return "" 51 | } 52 | name := self.Name.Get() 53 | if name == "" { 54 | name = self.ID.Get() 55 | if name == "" { 56 | return "" 57 | } 58 | } 59 | return name 60 | } 61 | 62 | func (self *LinkedInIdentity) LinkRel(ctx *view.Context) string { 63 | return "" 64 | } 65 | -------------------------------------------------------------------------------- /user/facebookidentity.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/ungerik/go-start/model" 7 | "github.com/ungerik/go-start/view" 8 | ) 9 | 10 | /////////////////////////////////////////////////////////////////////////////// 11 | // FacebookIdentity 12 | 13 | type FacebookIdentity struct { 14 | ID model.String 15 | Name model.String `view:"placeholder=your facebook url"` 16 | Confirmed model.DateTime 17 | AccessToken model.String 18 | } 19 | 20 | func (self *FacebookIdentity) NameOrID() string { 21 | if self == nil { 22 | return "" 23 | } 24 | if !self.Name.IsEmpty() { 25 | return self.Name.Get() 26 | } 27 | return self.ID.Get() 28 | } 29 | 30 | func (self *FacebookIdentity) ProfileURL() string { 31 | if self == nil { 32 | return "" 33 | } 34 | return "http://facebook.com/" + self.NameOrID() 35 | } 36 | 37 | func (self *FacebookIdentity) ProfileImageURL() string { 38 | name := self.NameOrID() 39 | if name == "" { 40 | return "" 41 | } 42 | return fmt.Sprintf("http://graph.facebook.com/%s/picture?type=large", name) 43 | } 44 | 45 | func (self *FacebookIdentity) URL(ctx *view.Context) string { 46 | return self.ProfileURL() 47 | } 48 | 49 | func (self *FacebookIdentity) LinkContent(ctx *view.Context) view.View { 50 | if self == nil { 51 | return nil 52 | } 53 | return view.Escape(self.LinkTitle(ctx)) 54 | } 55 | 56 | func (self *FacebookIdentity) LinkTitle(ctx *view.Context) string { 57 | return self.NameOrID() 58 | } 59 | 60 | func (self *FacebookIdentity) LinkRel(ctx *view.Context) string { 61 | return "" 62 | } 63 | -------------------------------------------------------------------------------- /view/textpreview.go: -------------------------------------------------------------------------------- 1 | package view 2 | 3 | import ( 4 | "unicode" 5 | ) 6 | 7 | /////////////////////////////////////////////////////////////////////////////// 8 | // TextPreview 9 | 10 | type TextPreview struct { 11 | ViewBase 12 | PlainText string 13 | MaxLength int 14 | ShortLength int // Shortened length if len(Text) > MaxLength. If zero, MaxLength will be used 15 | MoreLink LinkModel 16 | } 17 | 18 | func (self *TextPreview) Render(ctx *Context) (err error) { 19 | if len(self.PlainText) < self.MaxLength { 20 | ctx.Response.XML.Content(self.PlainText) 21 | } else { 22 | shortLength := self.ShortLength 23 | if shortLength == 0 { 24 | shortLength = self.MaxLength 25 | } 26 | 27 | // If in the middle of a word, go back to space before it 28 | for shortLength > 0 && !unicode.IsSpace(rune(self.PlainText[shortLength-1])) { 29 | shortLength-- 30 | } 31 | 32 | // If in the middle of space, go back to word before it 33 | for shortLength > 0 && unicode.IsSpace(rune(self.PlainText[shortLength-1])) { 34 | shortLength-- 35 | } 36 | 37 | ctx.Response.XML.Content(self.PlainText[:shortLength]) 38 | ctx.Response.XML.Content("... ") 39 | if self.MoreLink != nil { 40 | ctx.Response.XML.OpenTag("a") 41 | ctx.Response.XML.Attrib("href", self.MoreLink.URL(ctx)) 42 | ctx.Response.XML.AttribIfNotDefault("title", self.MoreLink.LinkTitle(ctx)) 43 | content := self.MoreLink.LinkContent(ctx) 44 | if content != nil { 45 | err = content.Render(ctx) 46 | } 47 | ctx.Response.XML.CloseTagAlways() // a 48 | } 49 | } 50 | return err 51 | } 52 | -------------------------------------------------------------------------------- /view/video.go: -------------------------------------------------------------------------------- 1 | package view 2 | 3 | import ( 4 | "github.com/ungerik/go-start/errs" 5 | "strings" 6 | ) 7 | 8 | /////////////////////////////////////////////////////////////////////////////// 9 | // Video 10 | 11 | // Video shows a Youtube Video, other formats to come. 12 | type Video struct { 13 | ViewBaseWithId 14 | Class string 15 | URL string 16 | Width int 17 | Height int 18 | //Description string 19 | } 20 | 21 | func (self *Video) Render(ctx *Context) (err error) { 22 | youtubeId := "" 23 | 24 | switch { 25 | case strings.HasPrefix(self.URL, "http://youtu.be/"): 26 | i := len("http://youtu.be/") 27 | youtubeId = self.URL[i : i+11] 28 | 29 | case strings.HasPrefix(self.URL, "http://www.youtube.com/watch?v="): 30 | i := len("http://www.youtube.com/watch?v=") 31 | youtubeId = self.URL[i : i+11] 32 | } 33 | 34 | if youtubeId != "" { 35 | ctx.Response.XML.OpenTag("iframe") 36 | ctx.Response.XML.Attrib("id", self.ID()) 37 | ctx.Response.XML.AttribIfNotDefault("class", self.Class) 38 | width := self.Width 39 | if width == 0 { 40 | width = 640 41 | } 42 | height := self.Height 43 | if height == 0 { 44 | height = 390 45 | } 46 | ctx.Response.XML.Attrib("src", "http://www.youtube.com/embed/", youtubeId) 47 | ctx.Response.XML.Attrib("width", width) 48 | ctx.Response.XML.Attrib("height", height) 49 | ctx.Response.XML.Attrib("frameborder", "0") 50 | ctx.Response.XML.Attrib("allowfullscreen", "allowfullscreen") 51 | ctx.Response.XML.CloseTag() 52 | return nil 53 | } 54 | 55 | return errs.Format("Unsupported video URL: %s", self.URL) 56 | } 57 | -------------------------------------------------------------------------------- /model/multiplechoice.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/ungerik/go-start/errs" 7 | "github.com/ungerik/go-start/utils" 8 | ) 9 | 10 | type MultipleChoice []string 11 | 12 | func (self *MultipleChoice) Get() []string { 13 | return []string(*self) 14 | } 15 | 16 | func (self *MultipleChoice) Set(value ...string) { 17 | *self = MultipleChoice(value) 18 | } 19 | 20 | func (self *MultipleChoice) String() string { 21 | return strings.Join([]string(*self), "|") 22 | } 23 | 24 | func (self *MultipleChoice) SetString(str string) error { 25 | *self = MultipleChoice(strings.Split(str, "|")) 26 | return nil 27 | } 28 | 29 | func (self *MultipleChoice) IsSet(option string) bool { 30 | return utils.StringIn(option, self.Get()) 31 | } 32 | 33 | func (self *MultipleChoice) IsEmpty() bool { 34 | return len(*self) == 0 35 | } 36 | 37 | func (self *MultipleChoice) Options(metaData *MetaData) []string { 38 | return choiceOptions(metaData) 39 | } 40 | 41 | func (self *MultipleChoice) Required(metaData *MetaData) bool { 42 | return false 43 | } 44 | 45 | func (self *MultipleChoice) Validate(metaData *MetaData) error { 46 | options := self.Options(metaData) 47 | if len(options) == 0 { 48 | return errs.Format("model.MultipleChoice needs options") 49 | } 50 | for _, option := range options { 51 | if option == "" { 52 | return errs.Format("model.MultipleChoice options must not be empty strings") 53 | } 54 | } 55 | for _, str := range *self { 56 | if !utils.StringIn(str, options) { 57 | return &InvalidChoice{str, options} 58 | } 59 | } 60 | return nil 61 | } 62 | -------------------------------------------------------------------------------- /view/modeliteratorview.go: -------------------------------------------------------------------------------- 1 | package view 2 | 3 | import ( 4 | "github.com/ungerik/go-start/model" 5 | // "github.com/ungerik/go-start/utils" 6 | ) 7 | 8 | /////////////////////////////////////////////////////////////////////////////// 9 | // ModelIteratorView 10 | 11 | type GetModelIteratorFunc func(ctx *Context) model.Iterator 12 | type GetModelIteratorViewFunc func(ctx *Context, model interface{}) (view View, err error) 13 | 14 | func ModelIterator(iter model.Iterator) GetModelIteratorFunc { 15 | return func(ctx *Context) model.Iterator { 16 | return iter 17 | } 18 | } 19 | 20 | type ModelIteratorView struct { 21 | ViewBase 22 | GetModelIterator GetModelIteratorFunc 23 | // GetModel returns the model used GetModelIteratorView. 24 | // Only return the same object if you know exactly what you are doing. 25 | // To be on the safe side, return a new model instance at every call. 26 | GetModel func(ctx *Context) (model interface{}, err error) 27 | GetModelView GetModelIteratorViewFunc // nil Views will be ignored 28 | } 29 | 30 | func (self *ModelIteratorView) Render(ctx *Context) (err error) { 31 | iter := self.GetModelIterator(ctx) 32 | model, err := self.GetModel(ctx) 33 | if err != nil { 34 | return err 35 | } 36 | for iter.Next(model) { 37 | view, err := self.GetModelView(ctx, model) 38 | if err != nil { 39 | return err 40 | } 41 | if view != nil { 42 | view.Init(view) 43 | err = view.Render(ctx) 44 | if err != nil { 45 | return err 46 | } 47 | } 48 | model, err = self.GetModel(ctx) 49 | if err != nil { 50 | return err 51 | } 52 | } 53 | return iter.Err() 54 | } 55 | -------------------------------------------------------------------------------- /errs/functions.go: -------------------------------------------------------------------------------- 1 | package errs 2 | 3 | import ( 4 | "fmt" 5 | "github.com/ungerik/go-start/debug" 6 | ) 7 | 8 | // FormatSkipStackFrames formats an error with call stack information if FormatWithCallStack is true. 9 | // It skips skip stack frames. 10 | func FormatSkipStackFrames(skip int, format string, args ...interface{}) error { 11 | if Config.FormatWithCallStack { 12 | format += "\n" + debug.CallStackInfo(skip+1) 13 | } 14 | return fmt.Errorf(format, args...) 15 | } 16 | 17 | // Format formats an error with call stack information if FormatWithCallStack is true 18 | func Format(format string, args ...interface{}) error { 19 | return FormatSkipStackFrames(2, format, args...) 20 | } 21 | 22 | func Assert(condition bool, description string, args ...interface{}) { 23 | if !condition { 24 | panic(FormatSkipStackFrames(2, "Failed assertion: "+description, args...)) 25 | } 26 | } 27 | 28 | // Panic if any of the args is a non nil error 29 | func PanicOnError(args ...interface{}) { 30 | for _, arg := range args { 31 | if arg != nil { 32 | if err, ok := arg.(error); ok { 33 | panic(err) 34 | } 35 | } 36 | } 37 | } 38 | 39 | // Panic if the last element of args is a non nil error 40 | func LastPanicOnError(args ...interface{}) { 41 | arg := args[len(args)-1] 42 | if arg != nil { 43 | if err, ok := arg.(error); ok { 44 | panic(err) 45 | } 46 | } 47 | } 48 | 49 | // Returns the first error in args 50 | func First(args ...interface{}) error { 51 | for _, arg := range args { 52 | if arg != nil { 53 | if err, ok := arg.(error); ok { 54 | return err 55 | } 56 | } 57 | } 58 | return nil 59 | } 60 | -------------------------------------------------------------------------------- /model/phone.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "strings" 5 | ) 6 | 7 | func NewPhone(value string) *Phone { 8 | p := new(Phone) 9 | p.Set(value) 10 | return p 11 | } 12 | 13 | type Phone string 14 | 15 | func (self *Phone) Get() string { 16 | return string(*self) 17 | } 18 | 19 | func (self *Phone) Set(value string) { 20 | *self = Phone(NormalizePhoneNumber(value)) 21 | } 22 | 23 | func (self *Phone) IsEmpty() bool { 24 | return len(*self) == 0 25 | } 26 | 27 | func (self *Phone) String() string { 28 | return self.Get() 29 | } 30 | 31 | func (self *Phone) SetString(str string) error { 32 | self.Set(str) 33 | return nil 34 | } 35 | 36 | func (self *Phone) FixValue(metaData *MetaData) { 37 | } 38 | 39 | func (self *Phone) Required(metaData *MetaData) bool { 40 | return metaData.BoolAttrib(StructTagKey, "required") 41 | } 42 | 43 | func (self *Phone) Validate(metaData *MetaData) error { 44 | if self.Required(metaData) && self.IsEmpty() { 45 | return NewRequiredError(metaData) 46 | } 47 | return nil 48 | } 49 | 50 | func NormalizePhoneNumber(number string) string { 51 | if number == "" { 52 | return "" 53 | } 54 | number = strings.Replace(number, " ", "", -1) 55 | number = strings.Replace(number, "/", "", -1) 56 | number = strings.Replace(number, "-", "", -1) 57 | number = strings.Replace(number, ".", "", -1) 58 | number = strings.Replace(number, "(0)", "", -1) 59 | number = strings.Replace(number, ")", "", -1) 60 | number = strings.Replace(number, "(", "", -1) 61 | if strings.HasPrefix(number, "++") { 62 | return "00" + number[2:] 63 | } 64 | if strings.HasPrefix(number, "+") { 65 | return "00" + number[1:] 66 | } 67 | return number 68 | } 69 | -------------------------------------------------------------------------------- /model/randomiterator.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "math/rand" 5 | "reflect" 6 | "time" 7 | ) 8 | 9 | func NewRandomIterator(iterator Iterator) *RandomIterator { 10 | return &RandomIterator{Iterator: iterator} 11 | } 12 | 13 | // RandomIterator stores all values from Iterator in a slice and 14 | // iterates them in random order. 15 | // LessFunc will always be called with a pointer to the struct if Next 16 | // is called with a pointer to a struct or the address of a 17 | // pointer to a stract. 18 | // For all other types LessFunc will be called with the type that 19 | // that the argument of Next points to. 20 | type RandomIterator struct { 21 | Iterator 22 | indexedSliceIterator *IndexedSliceIterator 23 | } 24 | 25 | func (self *RandomIterator) Next(resultRef interface{}) bool { 26 | if self.Err() != nil { 27 | return false 28 | } 29 | if self.indexedSliceIterator == nil { 30 | resultType := reflect.ValueOf(resultRef).Elem().Type() 31 | resultKind := resultType.Kind() 32 | slice := make([]interface{}, 0, 16) 33 | for self.Iterator.Next(resultRef) { 34 | resultVal := reflect.ValueOf(resultRef).Elem() 35 | if resultKind == reflect.Struct { 36 | resultCopy := reflect.New(resultType) 37 | resultCopy.Elem().Set(resultVal) 38 | slice = append(slice, resultCopy.Interface()) 39 | } else { 40 | slice = append(slice, resultVal.Interface()) 41 | } 42 | } 43 | if self.Err() != nil { 44 | return false 45 | } 46 | r := rand.New(rand.NewSource(time.Now().UnixNano())) 47 | indices := r.Perm(len(slice)) 48 | self.indexedSliceIterator = NewIndexedSliceIterator(slice, indices) 49 | } 50 | return self.indexedSliceIterator.Next(resultRef) 51 | } 52 | -------------------------------------------------------------------------------- /model/url.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "net/url" 5 | "strings" 6 | ) 7 | 8 | func NewUrl(value string) *Url { 9 | return (*Url)(&value) 10 | } 11 | 12 | type Url string 13 | 14 | func (self *Url) Get() string { 15 | return string(*self) 16 | } 17 | 18 | func (self *Url) Set(value string) error { 19 | if value == "" { 20 | *self = "" 21 | return nil 22 | } 23 | if !strings.HasPrefix(value, "http") { 24 | value = "http://" + value 25 | } 26 | _, err := url.Parse(value) 27 | if err == nil { 28 | *self = Url(value) 29 | } 30 | return err 31 | } 32 | 33 | func (self *Url) SetDataUrl(value string) error { 34 | if value == "" { 35 | *self = "" 36 | return nil 37 | } 38 | _, err := url.Parse(value) 39 | if err == nil { 40 | *self = Url(value) 41 | } 42 | return err 43 | } 44 | 45 | func (self *Url) IsEmpty() bool { 46 | return *self == "" 47 | } 48 | 49 | func (self *Url) GetOrDefault(defaultURL string) string { 50 | if self.IsEmpty() { 51 | return defaultURL 52 | } 53 | return self.Get() 54 | } 55 | 56 | func (self *Url) String() string { 57 | return self.Get() 58 | } 59 | 60 | func (self *Url) SetString(str string) error { 61 | return self.Set(str) 62 | } 63 | 64 | func (self *Url) FixValue(metaData *MetaData) { 65 | } 66 | 67 | func (self *Url) Validate(metaData *MetaData) error { 68 | if self.IsEmpty() { 69 | if self.Required(metaData) { 70 | return NewRequiredError(metaData) 71 | } 72 | } else { 73 | if _, err := url.Parse(self.Get()); err != nil { 74 | return err 75 | } 76 | } 77 | return nil 78 | } 79 | 80 | func (self *Url) Required(metaData *MetaData) bool { 81 | return metaData.BoolAttrib(StructTagKey, "required") 82 | } 83 | -------------------------------------------------------------------------------- /model/country.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "fmt" 5 | "github.com/ungerik/go-start/i18n" 6 | ) 7 | 8 | func NewCountry(value string) *Country { 9 | return (*Country)(&value) 10 | } 11 | 12 | type Country string 13 | 14 | func (self *Country) Get() string { 15 | return string(*self) 16 | } 17 | 18 | func (self *Country) Set(value string) error { 19 | if value != "" { 20 | // if _, ok := i18n.Countries()[value]; !ok { 21 | // return &InvalidCountryCode{str} 22 | // } 23 | } 24 | *self = Country(value) 25 | return nil 26 | } 27 | 28 | func (self *Country) IsEmpty() bool { 29 | return *self == "" 30 | } 31 | 32 | func (self *Country) String() string { 33 | return self.Get() 34 | } 35 | 36 | func (self *Country) SetString(str string) error { 37 | return self.Set(str) 38 | } 39 | 40 | func (self *Country) EnglishName() string { 41 | return i18n.EnglishCountryName(self.Get()) 42 | } 43 | 44 | func (self *Country) FixValue(metaData *MetaData) { 45 | } 46 | 47 | func (self *Country) Validate(metaData *MetaData) error { 48 | str := self.Get() 49 | if self.Required(metaData) || str != "" { 50 | // if _, ok := i18n.Countries()[value]; !ok { 51 | // return NewValidationErrors(&InvalidCountryCode{str}, metaData) 52 | // } 53 | } 54 | if self.Required(metaData) && self.IsEmpty() { 55 | return NewRequiredError(metaData) 56 | } 57 | return nil 58 | } 59 | 60 | func (self *Country) Required(metaData *MetaData) bool { 61 | return metaData.BoolAttrib(StructTagKey, "required") 62 | } 63 | 64 | type InvalidCountryCode struct { 65 | CountryCode string 66 | } 67 | 68 | func (self *InvalidCountryCode) String() string { 69 | return fmt.Sprintf("Ivalid country code ''", self.CountryCode) 70 | } 71 | -------------------------------------------------------------------------------- /utils/stringbuilder.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "strconv" 8 | ) 9 | 10 | type StringBuilder struct { 11 | buffer bytes.Buffer 12 | } 13 | 14 | func (self *StringBuilder) Write(strings ...string) *StringBuilder { 15 | for _, str := range strings { 16 | self.buffer.WriteString(str) 17 | } 18 | return self 19 | } 20 | 21 | func (self *StringBuilder) Printf(format string, args ...interface{}) *StringBuilder { 22 | fmt.Fprintf(&self.buffer, format, args...) 23 | return self 24 | } 25 | 26 | func (self *StringBuilder) Byte(value byte) *StringBuilder { 27 | self.buffer.WriteByte(value) 28 | return self 29 | } 30 | 31 | func (self *StringBuilder) WriteBytes(bytes []byte) *StringBuilder { 32 | self.buffer.Write(bytes) 33 | return self 34 | } 35 | 36 | func (self *StringBuilder) Int(value int) *StringBuilder { 37 | self.buffer.WriteString(strconv.Itoa(value)) 38 | return self 39 | } 40 | 41 | func (self *StringBuilder) Uint(value uint) *StringBuilder { 42 | self.buffer.WriteString(strconv.FormatUint(uint64(value), 10)) 43 | return self 44 | } 45 | 46 | func (self *StringBuilder) Float(value float64) *StringBuilder { 47 | self.buffer.WriteString(strconv.FormatFloat(value, 'f', -1, 64)) 48 | return self 49 | } 50 | 51 | func (self *StringBuilder) Bool(value bool) *StringBuilder { 52 | self.buffer.WriteString(strconv.FormatBool(value)) 53 | return self 54 | } 55 | 56 | func (self *StringBuilder) WriteTo(writer io.Writer) (n int64, err error) { 57 | return self.buffer.WriteTo(writer) 58 | } 59 | 60 | func (self *StringBuilder) Bytes() []byte { 61 | return self.buffer.Bytes() 62 | } 63 | 64 | func (self *StringBuilder) String() string { 65 | return self.buffer.String() 66 | } 67 | -------------------------------------------------------------------------------- /view/dummyimage.go: -------------------------------------------------------------------------------- 1 | package view 2 | 3 | import ( 4 | "fmt" 5 | "net/url" 6 | ) 7 | 8 | /////////////////////////////////////////////////////////////////////////////// 9 | // DummyImage 10 | 11 | func NewDummyImage(width, height int) *DummyImage { 12 | return &DummyImage{Width: width, Height: height} 13 | } 14 | 15 | // DummyImage represents a HTML img element with src utilizing http://dummyimage.com. 16 | type DummyImage struct { 17 | ViewBaseWithId 18 | Class string 19 | Width int 20 | Height int 21 | BackgroundColor string 22 | ForegroundColor string 23 | Text string 24 | } 25 | 26 | func (self *DummyImage) Render(ctx *Context) (err error) { 27 | src := fmt.Sprintf("http://dummyimage.com/%dx%d", self.Width, self.Height) 28 | 29 | if self.BackgroundColor != "" || self.ForegroundColor != "" { 30 | if self.BackgroundColor != "" { 31 | src += "/" + self.BackgroundColor 32 | } else { 33 | src += "/ccc" 34 | } 35 | 36 | if self.ForegroundColor != "" { 37 | src += "/" + self.ForegroundColor 38 | } 39 | } 40 | 41 | src += ".png" 42 | 43 | if self.Text != "" { 44 | src += "&text=" + url.QueryEscape(self.Text) 45 | } 46 | 47 | ctx.Response.XML.OpenTag("img") 48 | ctx.Response.XML.AttribIfNotDefault("id", self.id) 49 | ctx.Response.XML.AttribIfNotDefault("class", self.Class) 50 | ctx.Response.XML.Attrib("src", src) 51 | ctx.Response.XML.AttribIfNotDefault("width", self.Width) 52 | ctx.Response.XML.AttribIfNotDefault("height", self.Height) 53 | ctx.Response.XML.AttribIfNotDefault("alt", self.Text) 54 | ctx.Response.XML.CloseTag() 55 | return nil 56 | } 57 | 58 | //func (self *DummyImage) SetClass(class string) { 59 | // self.Class = class 60 | // ViewChanged(self) 61 | //} 62 | -------------------------------------------------------------------------------- /view/frontcontroller.go: -------------------------------------------------------------------------------- 1 | package view 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | ) 7 | 8 | type FrontController func(content View) ViewWithURL 9 | 10 | func (self FrontController) ContentFunc(contentFunc interface{}) ViewWithURL { 11 | v := reflect.ValueOf(contentFunc) 12 | t := v.Type() 13 | if t.Kind() != reflect.Func { 14 | panic(fmt.Errorf("FrontController.ForContent: contentFunc must be a function, got %s", t)) 15 | } 16 | if t.NumIn() != 2 { 17 | panic(fmt.Errorf("FrontController.ForContent: contentFunc must have two arguments, got %d", t.NumIn())) 18 | } 19 | if t.In(0) != reflect.TypeOf((*Context)(nil)) { 20 | panic(fmt.Errorf("FrontController.ForContent: contentFunc's first argument must be of type *Context, got %s", t.In(0))) 21 | } 22 | if t.NumOut() != 2 { 23 | panic(fmt.Errorf("FrontController.ForContent: contentFunc must have two results, got %d", t.NumOut())) 24 | } 25 | if t.Out(0) != reflect.TypeOf((*View)(nil)).Elem() { 26 | panic(fmt.Errorf("FrontController.ForContent: contentFunc's first result must be of type View, got %s", t.Out(0))) 27 | } 28 | if t.Out(1) != reflect.TypeOf((*error)(nil)).Elem() { 29 | panic(fmt.Errorf("FrontController.ForContent: contentFunc's second result must be of type error, got %s", t.Out(1))) 30 | } 31 | content := DynamicView( 32 | func(ctx *Context) (View, error) { 33 | if reflect.TypeOf(ctx.Data) != t.In(1) { 34 | panic(fmt.Errorf("FrontController: Context.Data must be of type %s, got %T", t.In(1), ctx.Data)) 35 | } 36 | args := []reflect.Value{reflect.ValueOf(ctx), reflect.ValueOf(ctx.Data)} 37 | results := v.Call(args) 38 | view, _ := results[0].Interface().(View) 39 | err, _ := results[1].Interface().(error) 40 | return view, err 41 | }, 42 | ) 43 | return self(content) 44 | } 45 | -------------------------------------------------------------------------------- /model/language.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "fmt" 5 | "github.com/ungerik/go-start/i18n" 6 | ) 7 | 8 | func NewLanguage(value string) *Language { 9 | return (*Language)(&value) 10 | } 11 | 12 | /* 13 | Attributes: 14 | label 15 | required 16 | */ 17 | type Language string 18 | 19 | func (self *Language) Get() string { 20 | return self.String() 21 | } 22 | 23 | func (self *Language) Set(value string) error { 24 | return self.SetString(value) 25 | } 26 | 27 | func (self *Language) IsEmpty() bool { 28 | return *self == "" 29 | } 30 | 31 | func (self *Language) String() string { 32 | return string(*self) 33 | } 34 | 35 | func (self *Language) SetString(str string) error { 36 | if str != "" { 37 | // if _, ok := i18n.Languages()[str]; !ok { 38 | // return &InvalidLanguageCode{str} 39 | // } 40 | } 41 | *self = Language(str) 42 | return nil 43 | } 44 | 45 | func (self *Language) EnglishName() string { 46 | return i18n.EnglishLanguageName(self.Get()) 47 | } 48 | 49 | func (self *Language) FixValue(metaData *MetaData) { 50 | } 51 | 52 | func (self *Language) Validate(metaData *MetaData) error { 53 | str := self.Get() 54 | if self.Required(metaData) || str != "" { 55 | // if _, ok := i18n.Languages()[str]; !ok { 56 | // errors = append(errors, &InvalidLanguageCode{str}) 57 | // } 58 | } 59 | if self.Required(metaData) && self.IsEmpty() { 60 | return NewRequiredError(metaData) 61 | } 62 | return nil 63 | } 64 | 65 | func (self *Language) Required(metaData *MetaData) bool { 66 | return metaData.BoolAttrib(StructTagKey, "required") 67 | } 68 | 69 | type InvalidLanguageCode struct { 70 | Language string 71 | } 72 | 73 | func (self *InvalidLanguageCode) String() string { 74 | return fmt.Sprintf("Ivalid language code ''", self.Language) 75 | } 76 | -------------------------------------------------------------------------------- /mongoadmin/removeinvalidrefsbutton.go: -------------------------------------------------------------------------------- 1 | package mongoadmin 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/ungerik/go-start/mongo" 7 | "github.com/ungerik/go-start/view" 8 | ) 9 | 10 | // RemoveInvalidRefsButton returns a form with a button to remove all 11 | // invalid mongo.Ref instances in collections. 12 | // If len(collections) is zero, then all collections will be used. 13 | func RemoveInvalidRefsButton(class string, collections ...*mongo.Collection) *view.Form { 14 | confirmation := "Are you sure you want to remove all invalid mongo refs in all collections?" 15 | if len(collections) > 0 { 16 | confirmation = "Are you sure you want to remove all invalid mongo refs in the collections " 17 | for i, c := range collections { 18 | if i > 0 { 19 | confirmation += ", " 20 | } 21 | confirmation += c.Name 22 | } 23 | confirmation += "?" 24 | } 25 | return &view.Form{ 26 | FormID: "mongoadmin.RemoveInvalidRefsButton", 27 | SubmitButtonText: "Remove invalid mongo refs", 28 | SubmitButtonClass: class, 29 | SubmitButtonConfirm: confirmation, 30 | OnSubmit: func(form *view.Form, formModel interface{}, ctx *view.Context) (message string, redirect view.URL, err error) { 31 | if len(collections) == 0 { 32 | for _, c := range mongo.Collections { 33 | collections = append(collections, c) 34 | } 35 | } 36 | for _, c := range collections { 37 | refs, err := c.RemoveInvalidRefs() 38 | if err != nil { 39 | return "", nil, err 40 | } 41 | if len(refs) > 0 { 42 | message = fmt.Sprintf("Removed %d invalid refs from collection %s. %s", len(refs), c.Name, message) 43 | } 44 | } 45 | if message == "" { 46 | message = "No invalid refs found" 47 | } 48 | return message, nil, nil 49 | }, 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /view/context.go: -------------------------------------------------------------------------------- 1 | package view 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/ungerik/web.go" 7 | ) 8 | 9 | func newContext(webContext *web.Context, respondingView View, urlArgs []string) *Context { 10 | ctx := &Context{ 11 | URLArgs: urlArgs, 12 | Request: newRequest(webContext), 13 | Response: newResponse(webContext), 14 | RespondingView: respondingView, 15 | } 16 | ctx.Session = newSession(ctx) 17 | return ctx 18 | } 19 | 20 | type Context struct { 21 | Request *Request 22 | Response *Response 23 | Session *Session 24 | 25 | // View that responds to the HTTP request 26 | RespondingView View 27 | 28 | // Arguments parsed from the URL path 29 | URLArgs []string 30 | 31 | // Authenticators can set the authenticated user name here 32 | AuthUser string 33 | 34 | // Custom response wide data that can be set by the application 35 | Data interface{} 36 | DebugData interface{} 37 | } 38 | 39 | /* 40 | ForURLArgs returns an altered Context copy where 41 | Context.URLArgs is set to urlArgs. 42 | Can be used for calling the the URL() method of a URL interface 43 | to get the URL of another view, defined by urlArgs. 44 | 45 | The following example gets the URL of MyPage with the first 46 | URL argument is that of the current page and the second 47 | URL argument is "second-arg": 48 | 49 | MyPage.URL(ctx.ForURLArgs(ctx.URLArgs[0], "second-arg")) 50 | */ 51 | func (self *Context) ForURLArgs(urlArgs ...string) *Context { 52 | clone := *self 53 | clone.URLArgs = urlArgs 54 | return &clone 55 | } 56 | 57 | func (self *Context) ForURLArgsConvert(urlArgs ...interface{}) *Context { 58 | stringArgs := make([]string, len(urlArgs)) 59 | for i := range urlArgs { 60 | stringArgs[i] = fmt.Sprint(urlArgs[i]) 61 | } 62 | return self.ForURLArgs(stringArgs...) 63 | } 64 | -------------------------------------------------------------------------------- /examples/FullTutorial/templates/css/common.css: -------------------------------------------------------------------------------- 1 | /* === Common === */ 2 | 3 | .clear { 4 | clear:both; 5 | } 6 | 7 | .center { 8 | margin: 0 auto; 9 | width: 990px; 10 | height: 100%; 11 | } 12 | 13 | .menu { 14 | display: inline; 15 | padding: 0; 16 | } 17 | 18 | .menu li { 19 | list-style: none; 20 | display: inline; 21 | } 22 | 23 | .menu a { 24 | padding: 0; 25 | text-decoration: none; 26 | } 27 | 28 | ::-moz-selection { 29 | background: {{{Primary}}}; 30 | } 31 | ::selection { 32 | background: {{{Primary}}}; 33 | } 34 | 35 | a:link, a:active, a:visited, a:hover, a:focus { 36 | text-decoration: none; 37 | outline: none; 38 | color: {{{Primary}}}; 39 | } 40 | 41 | /* === Forms === */ 42 | 43 | form label:after { 44 | content: ":"; 45 | } 46 | 47 | form input[type=checkbox] + label:after { 48 | content: ""; 49 | } 50 | 51 | form label { 52 | display: block; 53 | margin-right: 2em; 54 | } 55 | 56 | form input[type=checkbox] + label { 57 | display: inline; 58 | } 59 | 60 | form input[type=checkbox] { 61 | margin-right: 0.33em; 62 | } 63 | 64 | form > div, 65 | form > table { 66 | margin-bottom: 10px; 67 | } 68 | 69 | form table > caption { 70 | text-align: left; 71 | font-weight: bold; 72 | } 73 | 74 | form table td, 75 | form table th { 76 | text-align: left; 77 | font-weight: normal; 78 | vertical-align:bottom; 79 | } 80 | 81 | form table th div.table-actions { 82 | width:80px; 83 | } 84 | 85 | form .required { 86 | color: red; 87 | } 88 | 89 | form .error { 90 | display: block; 91 | padding: 5px; 92 | margin: 5px 0; 93 | color: white; 94 | background-color: red; 95 | } 96 | 97 | form .success { 98 | display: block; 99 | padding: 5px; 100 | margin: 5px 0; 101 | color: white; 102 | background-color: green; 103 | } 104 | -------------------------------------------------------------------------------- /mongo/config.go: -------------------------------------------------------------------------------- 1 | package mongo 2 | 3 | import ( 4 | "fmt" 5 | "labix.org/v2/mgo" 6 | ) 7 | 8 | var Config = Configuration{ 9 | Safe: mgo.Safe{ // be conservative... 10 | W: 1, 11 | FSync: false, 12 | J: true, 13 | }, 14 | CheckQuerySelectors: true, 15 | } 16 | 17 | var Database *mgo.Database 18 | 19 | var Collections = map[string]*Collection{} 20 | 21 | type Configuration struct { 22 | Host string 23 | Database string 24 | User string 25 | Password string 26 | Safe mgo.Safe 27 | CheckQuerySelectors bool 28 | } 29 | 30 | func (self *Configuration) Name() string { 31 | return "mongo" 32 | } 33 | 34 | func (self *Configuration) Init() error { 35 | login := "" 36 | if Config.User != "" { 37 | login = Config.User + ":" + Config.Password + "@" 38 | } 39 | 40 | host := "localhost" 41 | if Config.Host != "" { 42 | host = Config.Host 43 | } 44 | 45 | // http://goneat.org/pkg/labix.org/v2/mgo/#Session.Mongo 46 | // [mongodb://][user:pass@]host1[:port1][,host2[:port2],...][/database][?options] 47 | url := fmt.Sprintf("mongodb://%s%s/%s", login, host, Config.Database) 48 | 49 | session, err := mgo.Dial(url) 50 | if err != nil { 51 | return err 52 | } 53 | session.SetSafe(&Config.Safe) 54 | 55 | Database = session.DB(Config.Database) 56 | 57 | for _, collection := range Collections { 58 | collection.collection = Database.C(collection.Name) 59 | } 60 | 61 | return nil 62 | } 63 | 64 | func (self *Configuration) Close() error { 65 | if Database != nil && Database.Session != nil { 66 | Database.Session.Close() 67 | Database.Session = nil 68 | Database = nil 69 | } 70 | return nil 71 | } 72 | 73 | func InitLocalhost(database, user, password string) (err error) { 74 | Config.Database = database 75 | Config.User = user 76 | Config.Password = password 77 | return Config.Init() 78 | } 79 | -------------------------------------------------------------------------------- /view/concatstaticfiles.go: -------------------------------------------------------------------------------- 1 | package view 2 | 3 | import ( 4 | "bytes" 5 | "io/ioutil" 6 | "path" 7 | "time" 8 | 9 | "github.com/ungerik/go-start/errs" 10 | ) 11 | 12 | func NewConcatStaticFiles(filenames ...string) *ConcatStaticFiles { 13 | return &ConcatStaticFiles{Filenames: filenames} 14 | } 15 | 16 | // ConcatStaticFiles renders multiple static files concatenated as single file. 17 | // The output is cached in memory but changes to the files on the filesystem 18 | // cause the the cache to be rebuilt. 19 | type ConcatStaticFiles struct { 20 | ViewWithURLBase 21 | Filenames []string 22 | // Will be set automatically from Filenames[0] if empty 23 | ContentTypeExt string 24 | modifiedTimes []time.Time 25 | cachedFilesData []byte 26 | } 27 | 28 | func (self *ConcatStaticFiles) Render(ctx *Context) (err error) { 29 | if self.ContentTypeExt == "" && len(self.Filenames) > 0 { 30 | self.ContentTypeExt = path.Ext(self.Filenames[0]) 31 | } 32 | if len(self.Filenames) != len(self.modifiedTimes) { 33 | self.modifiedTimes = make([]time.Time, len(self.Filenames)) 34 | } 35 | 36 | for i, filename := range self.Filenames { 37 | _, found, modified := FindStaticFile(filename) 38 | if !found { 39 | return errs.Format("Static file not found: %s", filename) 40 | } 41 | if modified != self.modifiedTimes[i] { 42 | self.cachedFilesData = nil 43 | } 44 | } 45 | 46 | if self.cachedFilesData == nil { 47 | var buf bytes.Buffer 48 | for i, filename := range self.Filenames { 49 | filePath, _, modified := FindStaticFile(filename) 50 | data, err := ioutil.ReadFile(filePath) 51 | if err != nil { 52 | return err 53 | } 54 | buf.Write(data) 55 | self.modifiedTimes[i] = modified 56 | } 57 | self.cachedFilesData = buf.Bytes() 58 | } 59 | 60 | ctx.Response.SetContentTypeByExt(self.ContentTypeExt) 61 | ctx.Response.Write(self.cachedFilesData) 62 | return nil 63 | } 64 | -------------------------------------------------------------------------------- /examples/FullTutorial/views/root/loginsignup.go: -------------------------------------------------------------------------------- 1 | package root 2 | 3 | import ( 4 | // "github.com/ungerik/go-start/model" 5 | gostartuser "github.com/ungerik/go-start/user" 6 | . "github.com/ungerik/go-start/view" 7 | 8 | // "github.com/ungerik/go-start/examples/FullTutorial/models" 9 | . "github.com/ungerik/go-start/examples/FullTutorial/views" 10 | ) 11 | 12 | func init() { 13 | Logout = NewViewURLWrapper(gostartuser.LogoutView(nil)) 14 | 15 | ConfirmEmail = NewPublicPage("Email Confirmation | go-start Tutorial", 16 | DIV("public-content", 17 | DIV("main", 18 | TitleBar("Email confirmation"), 19 | DIV("main-content", gostartuser.EmailConfirmationView(IndirectURL(&Profile))), 20 | ), 21 | ), 22 | ) 23 | 24 | LoginSignup = NewPublicPage("Login or Sign up | go-start Tutorial", 25 | DIV("public-content", 26 | DynamicView( 27 | func(ctx *Context) (view View, err error) { 28 | _, hasFrom := ctx.Request.Params["from"] 29 | id := ctx.Session.ID() 30 | if id == "" && hasFrom { 31 | view = DIV("main", 32 | DIV("main-content", 33 | H3("Your account doesn't have sufficient rights to view this page"), 34 | Printf("You may
logout and login with a different account", Logout.URL(ctx)), 35 | ), 36 | ) 37 | } else { 38 | view = DIV("row", 39 | DIV("cell left", 40 | TitleBar("Log in"), 41 | DIV("main-content", 42 | gostartuser.NewLoginForm("Log in", "login", "error", "success", IndirectURL(&Homepage)), 43 | ), 44 | ), 45 | DIV("cell right", 46 | TitleBarRight("Sign up"), 47 | DIV("main-content", 48 | gostartuser.NewSignupForm("Sign up", "signup", "error", "success", IndirectURL(&ConfirmEmail), nil), 49 | ), 50 | ), 51 | DivClearBoth(), 52 | ) 53 | } 54 | return view, nil 55 | }, 56 | ), 57 | ), 58 | ) 59 | } 60 | -------------------------------------------------------------------------------- /user/twitteridentity.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/ungerik/go-start/model" 7 | "github.com/ungerik/go-start/view" 8 | ) 9 | 10 | /////////////////////////////////////////////////////////////////////////////// 11 | // TwitterIdentity 12 | 13 | type TwitterIdentity struct { 14 | ID model.String 15 | Name model.String 16 | Confirmed model.DateTime 17 | AccessToken model.String 18 | } 19 | 20 | func (self *TwitterIdentity) NameOrID() string { 21 | name := self.Name.Get() 22 | if name == "" { 23 | name = self.ID.Get() 24 | if name == "" { 25 | return "" 26 | } 27 | } 28 | if name[0] == '@' { 29 | name = name[1:] 30 | if name == "" { 31 | return "" 32 | } 33 | } 34 | return name 35 | } 36 | 37 | func (self *TwitterIdentity) ProfileURL() string { 38 | if self == nil { 39 | return "" 40 | } 41 | return "http://twitter.com/" + self.NameOrID() 42 | } 43 | 44 | func (self *TwitterIdentity) ProfileImageURL() string { 45 | if self == nil { 46 | return "" 47 | } 48 | name := self.NameOrID() 49 | if name == "" { 50 | return "" 51 | } 52 | return fmt.Sprintf("https://api.twitter.com/1/users/profile_image/%s?size=bigger", name) 53 | } 54 | 55 | func (self *TwitterIdentity) URL(ctx *view.Context) string { 56 | return self.ProfileURL() 57 | } 58 | 59 | func (self *TwitterIdentity) LinkContent(ctx *view.Context) view.View { 60 | if self == nil { 61 | return nil 62 | } 63 | return view.Escape(self.LinkTitle(ctx)) 64 | } 65 | 66 | func (self *TwitterIdentity) LinkTitle(ctx *view.Context) string { 67 | if self == nil { 68 | return "" 69 | } 70 | name := self.Name.Get() 71 | if name == "" { 72 | name = self.ID.Get() 73 | if name == "" { 74 | return "" 75 | } 76 | } 77 | if name[0] != '@' { 78 | name = "@" + name 79 | } 80 | return name 81 | } 82 | 83 | func (self *TwitterIdentity) LinkRel(ctx *view.Context) string { 84 | return "" 85 | } 86 | -------------------------------------------------------------------------------- /mongo/query_filterquery.go: -------------------------------------------------------------------------------- 1 | package mongo 2 | 3 | import ( 4 | "labix.org/v2/mgo" 5 | "labix.org/v2/mgo/bson" 6 | 7 | "github.com/ungerik/go-start/errs" 8 | 9 | // "github.com/ungerik/go-start/debug" 10 | ) 11 | 12 | /////////////////////////////////////////////////////////////////////////////// 13 | // query_filterBase 14 | 15 | type query_filterBase struct { 16 | query_base 17 | } 18 | 19 | func bsonQuery(query Query) (bsonQuery bson.M, err error) { 20 | chainedFilters := []Query{query} 21 | 22 | orChained := false 23 | for parent := query.ParentQuery(); parent != nil; parent = parent.ParentQuery() { 24 | if parent.IsFilter() { 25 | chainedFilters = append(chainedFilters, parent) 26 | } 27 | if _, or := parent.(*query_or); or { 28 | // todo check if filter and or interleave 29 | orChained = true 30 | } 31 | } 32 | 33 | if len(chainedFilters) == 1 { 34 | bsonQuery = query.bsonSelector() 35 | } else if orChained { 36 | bsonQueries := make([]bson.M, len(chainedFilters)) 37 | for i, filter := range chainedFilters { 38 | bsonQueries[i] = filter.bsonSelector() 39 | } 40 | bsonQuery = bson.M{"$or": bsonQueries} 41 | } else { 42 | bsonQuery = bson.M{} 43 | for _, filter := range chainedFilters { 44 | for key, value := range filter.bsonSelector() { 45 | if existingValue, hasKey := bsonQuery[key]; hasKey { 46 | return nil, errs.Format("Can't filter %s for %v and %v", key, existingValue, value) 47 | } 48 | bsonQuery[key] = value 49 | } 50 | } 51 | } 52 | 53 | return bsonQuery, nil 54 | } 55 | 56 | func (self *query_filterBase) mongoQuery() (q *mgo.Query, err error) { 57 | bsonQuery, err := bsonQuery(self.thisQuery) 58 | if err != nil { 59 | return nil, err 60 | } 61 | collection := self.Collection() 62 | collection.checkDBConnection() 63 | return collection.collection.Find(bsonQuery), nil 64 | } 65 | 66 | func (self *query_filterBase) IsFilter() bool { 67 | return true 68 | } 69 | -------------------------------------------------------------------------------- /examples/FullTutorial/views/shared.go: -------------------------------------------------------------------------------- 1 | package views 2 | 3 | import ( 4 | "github.com/ungerik/go-start/user" 5 | . "github.com/ungerik/go-start/view" 6 | ) 7 | 8 | func HeaderTopNav() View { 9 | return DIV("center", 10 | HeaderUserNav(), 11 | DynamicView( 12 | func(ctx *Context) (View, error) { 13 | if ctx.Request.RequestURI == "/" { 14 | // return nil, so nothing will be rendered 15 | return nil, nil 16 | } 17 | return A("/", HTML("← Back to the homepage")), nil 18 | }, 19 | ), 20 | ) 21 | } 22 | 23 | func HeaderMenu() *Menu { 24 | return &Menu{ 25 | Class: "menu", 26 | ItemClass: "menu-item", 27 | ActiveItemClass: "active", 28 | Items: []LinkModel{ 29 | NewLinkModel(&Homepage, "Home"), 30 | }, 31 | } 32 | } 33 | 34 | func HeaderUserNav() View { 35 | return DIV("login-nav", 36 | user.Nav( 37 | A(&LoginSignup, "Login / Sign up"), 38 | nil, 39 | A(&Logout, "Logout"), 40 | A(&Profile, "My profile"), 41 | HTML("  |  "), 42 | ), 43 | ) 44 | } 45 | 46 | func Footer() View { 47 | return DIV("footer") 48 | } 49 | 50 | func TitleBar(title string) View { 51 | return &Div{Class: "title-bar", Content: Escape(title)} 52 | } 53 | 54 | func TitleBarRight(title string) View { 55 | return &Div{Class: "title-bar right", Content: Escape(title)} 56 | } 57 | 58 | func NewPublicPage(title string, main View) *Page { 59 | return &Page{ 60 | Title: Escape(title), 61 | Scripts: Renderers{ 62 | JQuery, 63 | }, 64 | Content: Views{ 65 | &Div{ 66 | Class: "header", 67 | Content: Views{ 68 | HeaderTopNav(), 69 | DIV("menu-area", 70 | DIV("center", 71 | DIV("logo-container", IMG("/images/gopher.png")), 72 | HeaderMenu(), 73 | ), 74 | ), 75 | }, 76 | }, 77 | DIV("content", 78 | DIV("center", 79 | DIV("main", main), 80 | DivClearBoth(), 81 | ), 82 | ), 83 | Footer(), 84 | }, 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /reflection/modifyslicestructvisitor.go: -------------------------------------------------------------------------------- 1 | package reflection 2 | 3 | import "reflect" 4 | 5 | // ModifySliceStructVisitor is a StructVisitor that calls its self function 6 | // value in BeginSlice() and ignores all other StructVisitor methos. 7 | // It can be used to modify the length of slices in complex structs. 8 | type ModifySliceStructVisitor func(depth int, v reflect.Value) (reflect.Value, error) 9 | 10 | func (self ModifySliceStructVisitor) BeginStruct(depth int, v reflect.Value) error { 11 | return nil 12 | } 13 | 14 | func (self ModifySliceStructVisitor) StructField(depth int, v reflect.Value, f reflect.StructField, index int) error { 15 | return nil 16 | } 17 | 18 | func (self ModifySliceStructVisitor) EndStruct(depth int, v reflect.Value) error { 19 | return nil 20 | } 21 | 22 | func (self ModifySliceStructVisitor) ModifySlice(depth int, v reflect.Value) (reflect.Value, error) { 23 | return self(depth, v) 24 | } 25 | 26 | func (self ModifySliceStructVisitor) BeginSlice(depth int, v reflect.Value) error { 27 | return nil 28 | } 29 | 30 | func (self ModifySliceStructVisitor) SliceField(depth int, v reflect.Value, index int) error { 31 | return nil 32 | } 33 | 34 | func (self ModifySliceStructVisitor) EndSlice(depth int, v reflect.Value) error { 35 | return nil 36 | } 37 | 38 | func (self ModifySliceStructVisitor) BeginArray(depth int, v reflect.Value) error { 39 | return nil 40 | } 41 | 42 | func (self ModifySliceStructVisitor) ArrayField(depth int, v reflect.Value, index int) error { 43 | return nil 44 | } 45 | 46 | func (self ModifySliceStructVisitor) EndArray(depth int, v reflect.Value) error { 47 | return nil 48 | } 49 | 50 | func (self ModifySliceStructVisitor) BeginMap(depth int, v reflect.Value) error { 51 | return nil 52 | } 53 | 54 | func (self ModifySliceStructVisitor) MapField(depth int, v reflect.Value, key string, index int) error { 55 | return nil 56 | } 57 | 58 | func (self ModifySliceStructVisitor) EndMap(depth int, v reflect.Value) error { 59 | return nil 60 | } 61 | -------------------------------------------------------------------------------- /media/api.go: -------------------------------------------------------------------------------- 1 | package media 2 | 3 | import ( 4 | // "strings" 5 | 6 | // "github.com/ungerik/go-start/utils" 7 | "github.com/ungerik/go-start/view" 8 | ) 9 | 10 | type Api struct { 11 | AllThumbnails view.ViewWithURL 12 | // AllBlobs view.ViewWithURL 13 | } 14 | 15 | var API = Api{ 16 | 17 | AllThumbnails: view.RenderViewWithURLBindURLArgs( 18 | func(ctx *view.Context, thumbnailSize int) error { 19 | ctx.Response.Header().Set("Content-Type", "application/json") 20 | ctx.Response.WriteString("[\n") 21 | first := true 22 | i := Config.Backend.ImageIterator() 23 | var image Image 24 | for i.Next(&image) { 25 | thumbnail, err := image.Thumbnail(thumbnailSize) 26 | if err != nil { 27 | return err 28 | } 29 | if first { 30 | first = false 31 | } else { 32 | ctx.Response.WriteString(",\n") 33 | } 34 | ctx.Response.Printf( 35 | `{"id": "%s", "title": "%s", "url": "%s"}`, 36 | image.ID, image.Title, thumbnail.FileURL().URL(ctx), 37 | ) 38 | } 39 | if i.Err() != nil { 40 | return i.Err() 41 | } 42 | ctx.Response.WriteString("\n]") 43 | return nil 44 | }, 45 | ), 46 | 47 | // AllBlobs: view.RenderViewWithURL( 48 | // func(ctx *view.Context) error { 49 | // if !Config.NoDynamicStyleAndScript { 50 | // ctx.Response.RequireScript(string(view.JQuery), 0) 51 | // ctx.Response.RequireScript(string(view.JQueryUI), 1) 52 | // } 53 | // searchTerm, _ := ctx.Request.Params["term"] 54 | // searchTerm = strings.ToLower(searchTerm) 55 | 56 | // ctx.Response.Header().Set("Content-Type", "application/json") 57 | // ctx.Response.WriteByte('[') 58 | // first := true 59 | // i := BlobIterator() 60 | // var blob Blob 61 | // for i.Next(&blob) { 62 | // if first { 63 | // first = false 64 | // } else { 65 | // ctx.Response.WriteByte(',') 66 | // } 67 | // ctx.Response.Printf(`{"id": "%s", "title": "%s"}`, blob.ID, blob.Title) 68 | // } 69 | // ctx.Response.WriteByte(']') 70 | // return i.Err() 71 | // }, 72 | // ), 73 | } 74 | -------------------------------------------------------------------------------- /media/blobref.go: -------------------------------------------------------------------------------- 1 | package media 2 | 3 | import ( 4 | "github.com/ungerik/go-start/model" 5 | "github.com/ungerik/go-start/view" 6 | ) 7 | 8 | func NewBlobRef(blob *Blob) *BlobRef { 9 | blobRef := new(BlobRef) 10 | blobRef.Set(blob) 11 | return blobRef 12 | } 13 | 14 | type BlobRef string 15 | 16 | func (self *BlobRef) String() string { 17 | return string(*self) 18 | } 19 | 20 | func (self *BlobRef) SetString(str string) error { 21 | *self = BlobRef(str) 22 | return nil 23 | } 24 | 25 | // Blob loads the referenced blob, or returns nil if the reference is empty. 26 | func (self *BlobRef) Get() (*Blob, error) { 27 | if self.IsEmpty() { 28 | return nil, nil 29 | } 30 | return Config.Backend.LoadBlob(self.String()) 31 | } 32 | 33 | func (self *BlobRef) TryGet() (*Blob, bool, error) { 34 | if self.IsEmpty() { 35 | return nil, false, nil 36 | } 37 | blob, err := Config.Backend.LoadBlob(self.String()) 38 | if _, notFound := err.(ErrNotFound); notFound { 39 | return nil, false, nil 40 | } 41 | return blob, err == nil, err 42 | } 43 | 44 | // SetBlob sets the ID of blob, or an empty reference if blob is nil. 45 | func (self *BlobRef) Set(blob *Blob) { 46 | if blob != nil { 47 | self.SetString(blob.ID.String()) 48 | } else { 49 | *self = "" 50 | } 51 | } 52 | 53 | func (self *BlobRef) IsEmpty() bool { 54 | return *self == "" 55 | } 56 | 57 | func (self *BlobRef) Required(metaData *model.MetaData) bool { 58 | return metaData.BoolAttrib(model.StructTagKey, "required") 59 | } 60 | 61 | func (self *BlobRef) Validate(metaData *model.MetaData) error { 62 | if self.Required(metaData) && self.IsEmpty() { 63 | return model.NewRequiredError(metaData) 64 | } 65 | return nil 66 | } 67 | 68 | func (self *BlobRef) FileURL() (view.URL, error) { 69 | blob, err := self.Get() 70 | if err != nil { 71 | return nil, err 72 | } 73 | return blob.FileURL(), nil 74 | } 75 | 76 | func (self *BlobRef) FileLink(class string) (*view.Link, error) { 77 | blob, err := self.Get() 78 | if err != nil { 79 | return nil, err 80 | } 81 | return blob.FileLink(class), nil 82 | } 83 | -------------------------------------------------------------------------------- /media/backend.go: -------------------------------------------------------------------------------- 1 | package media 2 | 3 | import ( 4 | "io" 5 | 6 | "github.com/ungerik/go-start/model" 7 | ) 8 | 9 | type Backend interface { 10 | // General file methods: 11 | 12 | FileWriter(filename, contentType string) (writer io.WriteCloser, id string, err error) 13 | // Returns ErrNotFound if no file with id is found. 14 | FileReader(id string) (reader io.ReadCloser, filename, contentType string, err error) 15 | // Returns ErrNotFound if no file with id is found. 16 | DeleteFile(id string) error 17 | 18 | // Blob methods: 19 | 20 | LoadBlob(id string) (*Blob, error) 21 | SaveBlob(blob *Blob) error 22 | // DeleteBlob does not delete the file associated with it, also use DeleteFile(). 23 | DeleteBlob(blob *Blob) error 24 | 25 | // BlobIterator returns an iterator that iterates 26 | // all blobs as Blob structs. 27 | BlobIterator() model.Iterator 28 | 29 | // CountBlobRefs counts all BlobRef occurrences with blobID 30 | // in all known databases 31 | CountBlobRefs(blobID string) (count int, err error) 32 | 33 | // RemoveAllBlobRefs removes all BlobRef occurrences with blobID 34 | // in all known databases. 35 | RemoveAllBlobRefs(blobID string) (count int, err error) 36 | 37 | // Image methods: 38 | 39 | // Returns ErrNotFound if no image with id is found. 40 | LoadImage(id string) (*Image, error) 41 | 42 | // SaveImage saves image and updates its ID if it is empty. 43 | SaveImage(image *Image) error 44 | 45 | DeleteImage(image *Image) error 46 | 47 | // ImageIterator returns an iterator that iterates 48 | // all images as Image structs. 49 | ImageIterator() model.Iterator 50 | 51 | // CountImageRefs counts all ImageRef occurrences with imageID 52 | // in all known databases. 53 | CountImageRefs(imageID string) (count int, err error) 54 | 55 | // RemoveAllImageRefs removes all ImageRef occurrences with imageID 56 | // in all known databases. 57 | RemoveAllImageRefs(imageID string) (count int, err error) 58 | } 59 | 60 | type ErrNotFound string 61 | 62 | func (self ErrNotFound) Error() string { 63 | return "Media not found: \"" + string(self) + "\"" 64 | } 65 | -------------------------------------------------------------------------------- /reflection/slices.go: -------------------------------------------------------------------------------- 1 | package reflection 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | ) 7 | 8 | // AppendDefaultSliceElement appends a field to slice with the 9 | // value returned by GetDefaultValue for the former last slice 10 | // element, or the zero value of the type if the slice was empty. 11 | func AppendDefaultSliceElement(slice reflect.Value) reflect.Value { 12 | if slice.Kind() != reflect.Slice { 13 | panic(fmt.Errorf("Expected slice type, got %T", slice.Interface())) 14 | } 15 | var newField reflect.Value 16 | if slice.Len() > 0 { 17 | newField = GetDefaultValue(slice.Index(slice.Len() - 1)) 18 | } else { 19 | newField = reflect.Zero(slice.Type().Elem()) 20 | } 21 | return reflect.Append(slice, newField) 22 | } 23 | 24 | // SetSliceLengh sets the length of a slice by sub-slicing a slice that's too long, 25 | // or appending empty fields with the result of AppendDefaultSliceElement. 26 | func SetSliceLengh(slice reflect.Value, length int) reflect.Value { 27 | if length > slice.Len() { 28 | for i := slice.Len(); i < length; i++ { 29 | slice = AppendDefaultSliceElement(slice) 30 | } 31 | } else if length < slice.Len() { 32 | slice = slice.Slice(0, length) 33 | } 34 | return slice 35 | } 36 | 37 | // DeleteDefaultSliceElementsVal deletes slice elements where IsDefaultValue 38 | // returns true. 39 | func DeleteDefaultSliceElementsVal(slice reflect.Value) reflect.Value { 40 | if slice.Kind() != reflect.Slice { 41 | panic(fmt.Errorf("Expected slice type, got %T", slice.Interface())) 42 | } 43 | for i := slice.Len() - 1; i >= 0; i-- { 44 | if IsDefaultValue(slice.Index(i)) { 45 | before := slice.Slice(0, i) 46 | if i == slice.Len()-1 { 47 | slice = before 48 | } else { 49 | after := slice.Slice(i+1, slice.Len()) 50 | slice = reflect.AppendSlice(before, after) 51 | } 52 | } 53 | } 54 | return slice 55 | } 56 | 57 | // DeleteDefaultSliceElements deletes slice elements where IsDefaultValue 58 | // returns true. 59 | func DeleteDefaultSliceElements(slice interface{}) interface{} { 60 | return DeleteDefaultSliceElementsVal(reflect.ValueOf(slice)).Interface() 61 | } 62 | -------------------------------------------------------------------------------- /view/google.go: -------------------------------------------------------------------------------- 1 | package view 2 | 3 | import ( 4 | // "os" 5 | "fmt" 6 | "net/url" 7 | // "github.com/ungerik/go-start/utils" 8 | ) 9 | 10 | func GoogleAnalytics(trackingID string) HTML { 11 | return Printf("", trackingID) 12 | } 13 | 14 | // todo: replace http with https if necessary 15 | func GoogleMaps(apiKey string, sensor bool, callback string) HTML { 16 | return Printf("", apiKey, sensor, callback) 17 | } 18 | 19 | func GoogleMapsIframe(width, height int, location string) *Iframe { 20 | location = url.QueryEscape(location) 21 | URL := fmt.Sprintf("http://maps.google.com/maps?q=%s&output=embed", location) 22 | //URL = url.QueryEscape(URL) 23 | return &Iframe{ 24 | Width: width, 25 | Height: height, 26 | URL: URL, 27 | } 28 | } 29 | 30 | //type GoogleMapType string 31 | // 32 | //const ( 33 | // GoogleMapTypeHybrid GoogleMapType = "google.maps.MapTypeId.HYBRID" //This map type displays a transparent layer of major streets on satellite images. 34 | // GoogleMapTypeRoadmap GoogleMapType = "google.maps.MapTypeId.ROADMAP" //This map type displays a normal street map. 35 | // GoogleMapTypeSatellite GoogleMapType = "google.maps.MapTypeId.SATELLITE" //This map type displays satellite images. 36 | // GoogleMapTypeTerrain GoogleMapType = "google.maps.MapTypeId.TERRAIN" //This map type displays maps with physical features such as terrain and vegetation. 37 | //) 38 | // 39 | ///////////////////////////////////////////////////////////////////////////////// 40 | //// GoogleMap 41 | // 42 | //type GoogleMap struct { 43 | // ViewBaseWithId 44 | // Class string 45 | // Width int 46 | // Height int 47 | // Type GoogleMapType 48 | // CenterLat float64 49 | // CenterLng float64 50 | // Zoom float64 51 | //} 52 | // 53 | //func (self *GoogleMap) Render(ctx *Context) (err error) { 54 | // return nil 55 | //} 56 | -------------------------------------------------------------------------------- /view/renderer.go: -------------------------------------------------------------------------------- 1 | package view 2 | 3 | type Renderer interface { 4 | Render(ctx *Context) error 5 | } 6 | 7 | type Renderers []Renderer 8 | 9 | func (self Renderers) Render(ctx *Context) error { 10 | for _, r := range self { 11 | if r != nil { 12 | if err := r.Render(ctx); err != nil { 13 | return err 14 | } 15 | } 16 | } 17 | return nil 18 | } 19 | 20 | type Render func(ctx *Context) error 21 | 22 | func (self Render) Render(ctx *Context) error { 23 | return self(ctx) 24 | } 25 | 26 | // type ResponseRenderFunc func(ctx *Context) error 27 | 28 | // func (self ResponseRenderFunc) Render(ctx *Context) error { 29 | // return self(ctx) 30 | // } 31 | 32 | // IndirectRenderer takes the pointer to a Renderer variable 33 | // and dereferences it when the returned Renderer's Render method is called. 34 | // Used to break dependency cycles of variable initializations by 35 | // using a pointer to a variable instead of its value. 36 | func IndirectRenderer(rendererPtr *Renderer) Renderer { 37 | return Render( 38 | func(ctx *Context) (err error) { 39 | return (*rendererPtr).Render(ctx) 40 | }, 41 | ) 42 | } 43 | 44 | // FilterPortRenderer calls renderer.Render only 45 | // if the request is made to a specific port 46 | func FilterPortRenderer(port uint16, renderer Renderer) Renderer { 47 | if renderer == nil { 48 | return nil 49 | } 50 | return Render( 51 | func(ctx *Context) (err error) { 52 | if ctx.Request.Port() != port { 53 | return nil 54 | } 55 | return renderer.Render(ctx) 56 | }, 57 | ) 58 | } 59 | 60 | // ProductionServerRenderer returns renderer if view.Config.IsProductionServer 61 | // is true, else nil which is a valid value for a Renderer. 62 | func ProductionServerRenderer(renderer Renderer) Renderer { 63 | if !Config.IsProductionServer { 64 | return nil 65 | } 66 | return renderer 67 | } 68 | 69 | // NotProductionServerRenderer returns renderer if view.Config.IsProductionServer 70 | // is false, else nil which is a valid value for a Renderer. 71 | func NonProductionServerRenderer(renderer Renderer) Renderer { 72 | if Config.IsProductionServer { 73 | return nil 74 | } 75 | return renderer 76 | } 77 | -------------------------------------------------------------------------------- /utils/filesystem.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "os" 5 | "path" 6 | "path/filepath" 7 | "time" 8 | ) 9 | 10 | func DirExists(dirname string) bool { 11 | info, err := os.Stat(dirname) 12 | return err == nil && info.IsDir() 13 | } 14 | 15 | func FileExists(filename string) bool { 16 | info, err := os.Stat(filename) 17 | return err == nil && !info.IsDir() 18 | } 19 | 20 | func FileModifiedTime(filename string) (time.Time, error) { 21 | info, err := os.Stat(filename) 22 | if err != nil { 23 | return time.Time{}, err 24 | } 25 | return info.ModTime(), nil 26 | } 27 | 28 | func JoinAbs(elem ...string) (string, error) { 29 | return filepath.Abs(path.Join(elem...)) 30 | } 31 | 32 | func FindFile(searchDirs []string, filename string) (filePath string, found bool) { 33 | for _, searchDir := range searchDirs { 34 | filePath = path.Join(searchDir, filename) 35 | if FileExists(filePath) { 36 | return filePath, true 37 | } 38 | } 39 | return "", false 40 | } 41 | 42 | func FindFile2(baseDirs []string, searchDirs []string, filename string) (filePath string, found bool) { 43 | for _, baseDir := range baseDirs { 44 | for _, searchDir := range searchDirs { 45 | filePath = path.Join(baseDir, searchDir, filename) 46 | if FileExists(filePath) { 47 | return filePath, true 48 | } 49 | } 50 | } 51 | return "", false 52 | } 53 | 54 | func FindFile2ModifiedTime(baseDirs []string, searchDirs []string, filename string) (filePath string, found bool, modifiedTime time.Time) { 55 | for _, baseDir := range baseDirs { 56 | for _, searchDir := range searchDirs { 57 | filePath = path.Join(baseDir, searchDir, filename) 58 | if modifiedTime, err := FileModifiedTime(filePath); err == nil { 59 | return filePath, true, modifiedTime 60 | } 61 | } 62 | } 63 | return "", false, time.Time{} 64 | } 65 | 66 | func CombineDirs(baseDirs []string, searchDirs []string) []string { 67 | combinedDirs := make([]string, len(baseDirs)*len(searchDirs)) 68 | for i, baseDir := range baseDirs { 69 | for j, searchDir := range searchDirs { 70 | combinedDirs[i*len(searchDirs)+j] = path.Join(baseDir, searchDir) 71 | } 72 | } 73 | return combinedDirs 74 | } 75 | -------------------------------------------------------------------------------- /model/text.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import "strconv" 4 | 5 | func NewText(value string) *Text { 6 | return (*Text)(&value) 7 | } 8 | 9 | type Text string 10 | 11 | func (self *Text) Get() string { 12 | return string(*self) 13 | } 14 | 15 | func (self *Text) Set(value string) { 16 | *self = Text(value) 17 | } 18 | 19 | func (self *Text) IsEmpty() bool { 20 | return len(*self) == 0 21 | } 22 | 23 | func (self *Text) GetOrDefault(defaultText string) string { 24 | if self.IsEmpty() { 25 | return defaultText 26 | } 27 | return self.Get() 28 | } 29 | 30 | func (self *Text) String() string { 31 | return self.Get() 32 | } 33 | 34 | func (self *Text) SetString(str string) error { 35 | self.Set(str) 36 | return nil 37 | } 38 | 39 | func (self *Text) FixValue(metaData *MetaData) { 40 | } 41 | 42 | func (self *Text) Required(metaData *MetaData) bool { 43 | if minlen, ok, _ := self.Minlen(metaData); ok { 44 | if minlen > 0 { 45 | return true 46 | } 47 | } 48 | return metaData.BoolAttrib(StructTagKey, "required") 49 | } 50 | 51 | func (self *Text) Validate(metaData *MetaData) error { 52 | value := string(*self) 53 | 54 | if self.Required(metaData) && self.IsEmpty() { 55 | return NewRequiredError(metaData) 56 | } 57 | 58 | minlen, ok, err := self.Minlen(metaData) 59 | if ok && len(value) < minlen { 60 | err = &StringTooShort{value, minlen} 61 | } 62 | if err != nil { 63 | return err 64 | } 65 | 66 | maxlen, ok, err := self.Maxlen(metaData) 67 | if ok && len(value) > maxlen { 68 | err = &StringTooLong{value, maxlen} 69 | } 70 | if err != nil { 71 | return err 72 | } 73 | 74 | return nil 75 | } 76 | 77 | func (self *Text) Minlen(metaData *MetaData) (minlen int, ok bool, err error) { 78 | var str string 79 | if str, ok = metaData.Attrib(StructTagKey, "minlen"); ok { 80 | minlen, err = strconv.Atoi(str) 81 | ok = err == nil 82 | } 83 | return minlen, ok, err 84 | } 85 | 86 | func (self *Text) Maxlen(metaData *MetaData) (maxlen int, ok bool, err error) { 87 | var str string 88 | if str, ok = metaData.Attrib(StructTagKey, "maxlen"); ok { 89 | maxlen, err = strconv.Atoi(str) 90 | ok = err == nil 91 | } 92 | return maxlen, ok, err 93 | } 94 | -------------------------------------------------------------------------------- /view/textfield.go: -------------------------------------------------------------------------------- 1 | package view 2 | 3 | type TextFieldType int 4 | 5 | const ( 6 | NormalTextField TextFieldType = iota 7 | PasswordTextField 8 | EmailTextField 9 | SearchTextField 10 | ) 11 | 12 | /////////////////////////////////////////////////////////////////////////////// 13 | // TextField 14 | 15 | type TextField struct { 16 | ViewBaseWithId 17 | Text string 18 | Name string 19 | Size int 20 | MaxLength int 21 | Type TextFieldType 22 | TabIndex int 23 | Class string 24 | Placeholder string 25 | Title string 26 | Readonly bool 27 | Disabled bool 28 | Required bool // HTML5 29 | Autofocus bool // HTML5 30 | Pattern string // HTML5 31 | } 32 | 33 | func (self *TextField) Render(ctx *Context) (err error) { 34 | ctx.Response.XML.OpenTag("input") 35 | ctx.Response.XML.AttribIfNotDefault("id", self.id) 36 | ctx.Response.XML.AttribIfNotDefault("class", self.Class) 37 | ctx.Response.XML.AttribIfNotDefault("title", self.Title) 38 | 39 | ctx.Response.XML.Attrib("name", self.Name) 40 | ctx.Response.XML.AttribIfNotDefault("tabindex", self.TabIndex) 41 | ctx.Response.XML.AttribFlag("readonly", self.Readonly) 42 | ctx.Response.XML.AttribFlag("disabled", self.Disabled) 43 | ctx.Response.XML.AttribFlag("required", self.Required) 44 | ctx.Response.XML.AttribFlag("autofocus", self.Autofocus) 45 | ctx.Response.XML.AttribIfNotDefault("pattern", self.Pattern) 46 | 47 | switch self.Type { 48 | case PasswordTextField: 49 | ctx.Response.XML.Attrib("type", "password") 50 | case EmailTextField: 51 | ctx.Response.XML.Attrib("type", "email") 52 | case SearchTextField: 53 | ctx.Response.XML.Attrib("type", "search") 54 | default: 55 | ctx.Response.XML.Attrib("type", "text") 56 | } 57 | 58 | ctx.Response.XML.AttribIfNotDefault("size", self.Size) 59 | ctx.Response.XML.AttribIfNotDefault("maxlength", self.MaxLength) 60 | ctx.Response.XML.AttribIfNotDefault("placeholder", self.Placeholder) 61 | 62 | ctx.Response.XML.Attrib("value", self.Text) 63 | 64 | ctx.Response.XML.CloseTag() 65 | return nil 66 | } 67 | 68 | func (self *TextField) SetRequired(required bool) { 69 | self.Required = required 70 | } 71 | -------------------------------------------------------------------------------- /model/date.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import "time" 4 | 5 | const DateFormat = "2006-01-02" 6 | 7 | func NewDate(value string) *Date { 8 | return (*Date)(&value) 9 | } 10 | 11 | type Date string 12 | 13 | func (self *Date) Get() string { 14 | return string(*self) 15 | } 16 | 17 | func (self *Date) Set(value string) error { 18 | *self = Date(value) // set value in any case, so that user can see wrong value in form 19 | 20 | if value != "" { 21 | if _, err := time.Parse(DateFormat, value); err != nil { 22 | return err 23 | } 24 | } 25 | *self = Date(value) 26 | return nil 27 | } 28 | 29 | func (self *Date) SetTodayUTC() { 30 | self.SetTime(time.Now().UTC()) 31 | } 32 | 33 | func (self *Date) Time() time.Time { 34 | time, err := time.Parse(DateFormat, self.Get()) 35 | if err != nil { 36 | panic(err) 37 | } 38 | return time 39 | } 40 | 41 | func (self *Date) SetTime(t time.Time) { 42 | *self = Date(t.Format(DateFormat)) 43 | } 44 | 45 | func (self *Date) UnixNanoseconds() int64 { 46 | return self.Time().UnixNano() 47 | } 48 | 49 | func (self *Date) SetUnixNanoseconds(nanos int64) { 50 | self.SetTime(time.Unix(0, nanos).UTC()) 51 | } 52 | 53 | func (self *Date) Format(format string) string { 54 | if *self == "" { 55 | return "" 56 | } 57 | return self.Time().Format(format) 58 | } 59 | 60 | func (self *Date) IsEmpty() bool { 61 | return len(*self) == 0 62 | } 63 | 64 | func (self *Date) SetEmpty() { 65 | *self = "" 66 | } 67 | 68 | func (self *Date) String() string { 69 | return self.Get() 70 | } 71 | 72 | func (self *Date) SetString(str string) error { 73 | return self.Set(str) 74 | } 75 | 76 | func (self *Date) FixValue(metaData *MetaData) { 77 | } 78 | 79 | // todo min max 80 | func (self *Date) Validate(metaData *MetaData) error { 81 | value := self.Get() 82 | if self.Required(metaData) && self.IsEmpty() { 83 | return NewRequiredError(metaData) 84 | } 85 | if self.Required(metaData) || value != "" { 86 | if _, err := time.Parse(DateFormat, value); err != nil { 87 | return err 88 | } 89 | } 90 | return nil 91 | } 92 | 93 | func (self *Date) Required(metaData *MetaData) bool { 94 | return metaData.BoolAttrib(StructTagKey, "required") 95 | } 96 | -------------------------------------------------------------------------------- /examples/FullTutorial/views/root/profile.go: -------------------------------------------------------------------------------- 1 | package root 2 | 3 | import ( 4 | "github.com/ungerik/go-start/user" 5 | . "github.com/ungerik/go-start/view" 6 | 7 | "github.com/ungerik/go-start/examples/FullTutorial/models" 8 | . "github.com/ungerik/go-start/examples/FullTutorial/views" 9 | ) 10 | 11 | func init() { 12 | Profile = NewPublicPage("My Profile | go-start Tutorial", 13 | DIV("public-content", 14 | DynamicView( 15 | func(ctx *Context) (view View, err error) { 16 | var usr models.User 17 | found, err := user.OfSession(ctx.Session, &usr) 18 | if err != nil { 19 | return nil, err 20 | } 21 | if !found { 22 | return H1("You have to be logged in to edit your startup"), nil 23 | } 24 | email := usr.PrimaryEmail() 25 | 26 | view = DIV("row", 27 | DIV("cell right-border", 28 | TitleBar("My Profile"), 29 | DIV("main-content", 30 | H3("Email: ", email), 31 | H3("Name:"), 32 | P(&Form{ 33 | SubmitButtonText: "Save name", 34 | SubmitButtonClass: "button", 35 | FormID: "profile", 36 | GetModel: func(form *Form, ctx *Context) (interface{}, error) { 37 | return &usr.Name, nil 38 | }, 39 | OnSubmit: func(form *Form, formModel interface{}, ctx *Context) (string, URL, error) { 40 | return "", StringURL("."), usr.Save() 41 | }, 42 | }), 43 | H3("Password:"), 44 | P(&Form{ 45 | SubmitButtonText: "Save password", 46 | SubmitButtonClass: "button", 47 | FormID: "password", 48 | GetModel: func(form *Form, ctx *Context) (interface{}, error) { 49 | return new(user.PasswordFormModel), nil 50 | }, 51 | OnSubmit: func(form *Form, formModel interface{}, ctx *Context) (string, URL, error) { 52 | m := formModel.(*user.PasswordFormModel) 53 | usr.Password.SetHashed(m.Password1.Get()) 54 | return "", StringURL("."), usr.Save() 55 | }, 56 | }), 57 | ), 58 | ), 59 | DivClearBoth(), 60 | ) 61 | return view, nil 62 | }, 63 | ), 64 | ), 65 | ) 66 | } 67 | -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "io" 7 | "io/ioutil" 8 | "log" 9 | "os" 10 | "path" 11 | 12 | "github.com/ungerik/go-start/debug" 13 | ) 14 | 15 | var Logger = log.New(os.Stderr, "", log.LstdFlags) 16 | 17 | type Package interface { 18 | Name() string 19 | Init() error 20 | io.Closer 21 | } 22 | 23 | var Packages []Package 24 | 25 | func Load(configFile string, packages ...Package) { 26 | debug.Nop() 27 | data, err := ioutil.ReadFile(configFile) 28 | if err != nil { 29 | Logger.Panicf("Error while reading config file %s: %s", configFile, err) 30 | } 31 | Packages = packages 32 | switch path.Ext(configFile) { 33 | case ".json": 34 | // Compact JSON to make it easier to extract JSON per package 35 | var buf bytes.Buffer 36 | err = json.Compact(&buf, data) 37 | if err != nil { 38 | Logger.Panicf("Error in JSON config file %s: %s", configFile, err) 39 | } 40 | data = buf.Bytes() 41 | 42 | // Unmarshal packages in given order 43 | for _, pkg := range packages { 44 | // Extract JSON only for this package 45 | key := []byte(`"` + pkg.Name() + `":{`) 46 | begin := bytes.Index(data, key) 47 | if begin != -1 { 48 | begin += len(key) - 1 49 | end := 0 50 | braceCounter := 0 51 | for i := begin; i < len(data); i++ { 52 | switch data[i] { 53 | case '{': 54 | braceCounter++ 55 | case '}': 56 | braceCounter-- 57 | } 58 | if braceCounter == 0 { 59 | end = i + 1 60 | break 61 | } 62 | } 63 | 64 | err = json.Unmarshal(data[begin:end], pkg) 65 | if err != nil { 66 | Logger.Panicf("Error while unmarshalling JSON from config file %s: %s", configFile, err) 67 | } 68 | } 69 | err := pkg.Init() 70 | if err != nil { 71 | Logger.Panicf("Error while initializing package %s: %s", pkg.Name(), err) 72 | } 73 | } 74 | 75 | default: 76 | panic("Unsupported config file: " + configFile) 77 | } 78 | } 79 | 80 | func Close() { 81 | for i := len(Packages) - 1; i >= 0; i-- { 82 | err := Packages[i].Close() 83 | if err != nil { 84 | Logger.Println("Error while closing package %s: %s", Packages[i].Name(), err) 85 | } 86 | } 87 | } 88 | --------------------------------------------------------------------------------