├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── binder.go ├── binder_test.go ├── cache ├── cache.go ├── cache_test.go ├── init.go ├── inmemory.go ├── inmemory_test.go ├── memcached.go ├── memcached_test.go ├── redis.go ├── redis_test.go ├── serialization.go └── serialization_test.go ├── compress.go ├── compress_test.go ├── conf └── mime-types.conf ├── config.go ├── controller.go ├── errors.go ├── fakeapp_test.go ├── field.go ├── filter.go ├── filterconfig.go ├── filterconfig_test.go ├── flash.go ├── harness ├── app.go ├── build.go ├── harness.go ├── reflect.go └── reflect_test.go ├── http.go ├── i18n.go ├── i18n_test.go ├── intercept.go ├── intercept_test.go ├── invoker.go ├── invoker_test.go ├── libs.go ├── mail ├── connection.go ├── mailer.go ├── mailer_test.go ├── message.go ├── message_test.go └── testdata │ ├── testTemplate.html │ └── testTemplate.txt ├── modules ├── db │ └── app │ │ └── db.go ├── jobs │ ├── app │ │ ├── controllers │ │ │ └── status.go │ │ ├── jobs │ │ │ ├── job.go │ │ │ ├── jobrunner.go │ │ │ └── plugin.go │ │ └── views │ │ │ └── Jobs │ │ │ └── Status.html │ └── conf │ │ └── routes ├── pprof │ ├── README.md │ ├── app │ │ └── controllers │ │ │ └── pprof.go │ └── conf │ │ └── routes ├── static │ └── app │ │ └── controllers │ │ └── static.go └── testrunner │ ├── app │ ├── controllers │ │ └── testrunner.go │ ├── plugin.go │ └── views │ │ └── TestRunner │ │ ├── FailureDetail.html │ │ ├── Index.html │ │ └── SuiteResult.html │ ├── conf │ └── routes │ └── public │ ├── css │ └── bootstrap.css │ ├── images │ └── favicon.png │ └── js │ └── jquery-1.9.1.min.js ├── panic.go ├── params.go ├── params_test.go ├── results.go ├── results_test.go ├── revel.go ├── revel ├── build.go ├── clean.go ├── new.go ├── package.go ├── package_run.bat.template ├── package_run.sh.template ├── rev.go ├── run.go ├── test.go └── util.go ├── router.go ├── router_test.go ├── samples ├── booking │ ├── app │ │ ├── controllers │ │ │ ├── app.go │ │ │ ├── gorp.go │ │ │ ├── hotels.go │ │ │ └── init.go │ │ ├── init.go │ │ ├── jobs │ │ │ └── count.go │ │ ├── models │ │ │ ├── booking.go │ │ │ ├── hotel.go │ │ │ └── user.go │ │ └── views │ │ │ ├── application │ │ │ ├── index.html │ │ │ └── register.html │ │ │ ├── footer.html │ │ │ ├── header.html │ │ │ └── hotels │ │ │ ├── book.html │ │ │ ├── confirmbooking.html │ │ │ ├── index.html │ │ │ ├── list.html │ │ │ ├── settings.html │ │ │ └── show.html │ ├── conf │ │ ├── app.conf │ │ └── routes │ ├── public │ │ ├── css │ │ │ └── main.css │ │ ├── img │ │ │ ├── favicon.png │ │ │ └── hotel.jpg │ │ ├── js │ │ │ ├── jquery-1.3.2.min.js │ │ │ ├── jquery-ui-1.7.2.custom.min.js │ │ │ └── sessvars.js │ │ └── ui-lightness │ │ │ ├── images │ │ │ ├── ui-bg_diagonals-thick_18_b81900_40x40.png │ │ │ ├── ui-bg_diagonals-thick_20_666666_40x40.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_highlight-soft_100_eeeeee_1x100.png │ │ │ ├── ui-bg_highlight-soft_75_ffe45c_1x100.png │ │ │ ├── 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 │ │ │ └── jquery-ui-1.7.2.custom.css │ └── tests │ │ └── apptest.go ├── chat │ ├── app │ │ ├── chatroom │ │ │ └── chatroom.go │ │ ├── controllers │ │ │ ├── app.go │ │ │ ├── longpolling.go │ │ │ ├── refresh.go │ │ │ └── websocket.go │ │ └── views │ │ │ ├── Application │ │ │ └── Index.html │ │ │ ├── LongPolling │ │ │ └── Room.html │ │ │ ├── Refresh │ │ │ └── Room.html │ │ │ ├── WebSocket │ │ │ └── Room.html │ │ │ ├── footer.html │ │ │ └── header.html │ ├── conf │ │ ├── app.conf │ │ └── routes │ ├── public │ │ ├── images │ │ │ └── favicon.png │ │ ├── javascripts │ │ │ ├── jquery-1.5.min.js │ │ │ ├── jquery.scrollTo-min.js │ │ │ └── templating.js │ │ └── stylesheets │ │ │ └── main.css │ └── tests │ │ └── apptest.go ├── facebook-oauth2 │ ├── app │ │ ├── controllers │ │ │ └── app.go │ │ ├── models │ │ │ └── user.go │ │ └── views │ │ │ ├── Application │ │ │ └── Index.html │ │ │ ├── footer.html │ │ │ └── header.html │ ├── conf │ │ ├── app.conf │ │ └── routes │ ├── public │ │ ├── css │ │ │ └── main.css │ │ ├── images │ │ │ └── favicon.png │ │ └── js │ │ │ └── jquery-1.4.2.min.js │ └── tests │ │ └── apptest.go ├── i18n │ ├── app │ │ ├── controllers │ │ │ └── app.go │ │ └── views │ │ │ ├── Application │ │ │ └── Index.html │ │ │ ├── errors │ │ │ ├── 404.html │ │ │ └── 500.html │ │ │ ├── footer.html │ │ │ └── header.html │ ├── conf │ │ ├── app.conf │ │ └── routes │ ├── messages │ │ ├── sample.en │ │ └── sample2.en │ ├── public │ │ ├── css │ │ │ └── bootstrap.css │ │ ├── images │ │ │ └── favicon.png │ │ └── js │ │ │ └── jquery-1.5.2.min.js │ └── tests │ │ └── apptest.go ├── persona │ ├── .gitignore │ ├── app │ │ ├── controllers │ │ │ └── app.go │ │ └── views │ │ │ ├── App │ │ │ └── Index.html │ │ │ ├── debug.html │ │ │ ├── flash.html │ │ │ ├── footer.html │ │ │ └── header.html │ ├── conf │ │ ├── app.conf │ │ └── routes │ ├── messages │ │ └── sample.en │ ├── public │ │ ├── css │ │ │ ├── bootstrap.css │ │ │ └── common.css │ │ ├── img │ │ │ ├── favicon.png │ │ │ ├── glyphicons-halflings-white.png │ │ │ ├── glyphicons-halflings.png │ │ │ └── persona-signin.png │ │ └── js │ │ │ └── jquery-1.9.1.min.js │ └── tests │ │ └── apptest.go ├── twitter-oauth │ ├── app │ │ ├── controllers │ │ │ └── app.go │ │ ├── models │ │ │ └── user.go │ │ └── views │ │ │ ├── Application │ │ │ └── Index.html │ │ │ ├── footer.html │ │ │ └── header.html │ ├── conf │ │ ├── app.conf │ │ └── routes │ ├── public │ │ ├── css │ │ │ └── main.css │ │ ├── images │ │ │ └── favicon.png │ │ └── js │ │ │ └── jquery-1.4.2.min.js │ └── tests │ │ └── apptest.go └── validation │ ├── app │ ├── controllers │ │ ├── app.go │ │ ├── sample1.go │ │ ├── sample2.go │ │ ├── sample3.go │ │ └── sample4.go │ ├── models │ │ └── user.go │ └── views │ │ ├── Application │ │ └── Index.html │ │ ├── Sample1 │ │ ├── HandleSubmit.html │ │ └── Index.html │ │ ├── Sample2 │ │ ├── HandleSubmit.html │ │ └── Index.html │ │ ├── Sample3 │ │ ├── HandleSubmit.html │ │ └── Index.html │ │ ├── Sample4 │ │ ├── HandleSubmit.html │ │ └── Index.html │ │ ├── footer.html │ │ └── header.html │ ├── conf │ ├── app.conf │ └── routes │ ├── public │ ├── css │ │ └── main.css │ └── images │ │ └── favicon.png │ └── tests │ └── apptest.go ├── server.go ├── server_test.go ├── session.go ├── session_test.go ├── skeleton ├── .gitignore ├── app │ ├── controllers │ │ └── app.go │ ├── init.go │ └── views │ │ ├── App │ │ └── Index.html │ │ ├── debug.html │ │ ├── errors │ │ ├── 404.html │ │ └── 500.html │ │ ├── flash.html │ │ ├── footer.html │ │ └── header.html ├── conf │ ├── app.conf.template │ └── routes ├── messages │ └── sample.en ├── public │ ├── css │ │ └── bootstrap.css │ ├── img │ │ ├── favicon.png │ │ ├── glyphicons-halflings-white.png │ │ └── glyphicons-halflings.png │ └── js │ │ └── jquery-1.9.1.min.js └── tests │ └── apptest.go ├── template.go ├── templates └── errors │ ├── 403.html │ ├── 403.json │ ├── 403.txt │ ├── 403.xml │ ├── 404-dev.html │ ├── 404.html │ ├── 404.json │ ├── 404.txt │ ├── 404.xml │ ├── 500-dev.html │ ├── 500.html │ ├── 500.json │ ├── 500.txt │ └── 500.xml ├── testdata └── i18n │ ├── config │ └── test_app.conf │ └── messages │ ├── dutch_messages.nl │ ├── english_messages.en │ ├── english_messages2.en │ └── invalid_message_file_name.txt ├── tests.go ├── util.go ├── util_test.go ├── validation.go ├── validation_test.go ├── validators.go └── watcher.go /.gitignore: -------------------------------------------------------------------------------- 1 | tmp/ 2 | routes/ 3 | test-results/ 4 | revel/revel 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 1.2 3 | services: 4 | - memcache # github.com/robfig/revel/cache 5 | - redis-server 6 | install: 7 | - export PATH=$PATH:$HOME/gopath/bin 8 | # Annoyingly, we can not use go get revel/... because references to app/routes package fail 9 | - go get -v github.com/robfig/revel/revel 10 | - go get -v github.com/robfig/revel/cache 11 | - go get -v github.com/robfig/revel/harness 12 | - go get -v github.com/coopernurse/gorp 13 | - go get -v code.google.com/p/go.crypto/bcrypt 14 | - go get -v github.com/mattn/go-sqlite3 15 | - go get -v github.com/robfig/cron 16 | - go get -v github.com/robfig/goauth2/oauth 17 | - go get -v github.com/mrjones/oauth 18 | script: 19 | - go test github.com/robfig/revel 20 | - go test github.com/robfig/revel/cache 21 | - go test github.com/robfig/revel/harness 22 | 23 | # Ensure the new-app flow works (plus the other commands). 24 | - revel new my/testapp 25 | - revel test my/testapp 26 | - revel clean my/testapp 27 | - revel build my/testapp build/testapp 28 | - revel package my/testapp 29 | 30 | # Build & run the sample apps 31 | # Sleep between tests to avoid spurious "address already in use" failures. 32 | - revel test github.com/robfig/revel/samples/booking 33 | - sleep 30 34 | - revel test github.com/robfig/revel/samples/chat 35 | - sleep 30 36 | - revel test github.com/robfig/revel/samples/facebook-oauth2 37 | - sleep 30 38 | - revel test github.com/robfig/revel/samples/twitter-oauth 39 | - sleep 30 40 | - revel test github.com/robfig/revel/samples/validation 41 | - sleep 30 42 | - revel test github.com/robfig/revel/samples/chat 43 | 44 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Revel Changelog 2 | 3 | Legend: 4 | 5 | * [FTR] New feature 6 | * [ENH] Enhancement to current functionality 7 | * [BUG] Fixed bug 8 | * [HOT] Hotfix 9 | 10 | 11 | ## Revel v0.9.2 (Feb 25, 2014) 12 | 13 | * [HOT] - `revel new` bug (#522) 14 | 15 | 16 | ## Revel v0.9.1 (Feb 24, 2014) 17 | 18 | * [HOT] - Booking sample error 19 | 20 | 21 | ## Revel v0.9.0 (Feb 24, 2014) 22 | 23 | * [BUG] #504 - File access via URL security issue 24 | * [BUG] #489 - Email validator bug 25 | * [BUG] #475 - File watcher infinite loop 26 | * [BUG] #333 - Extensions in routes break parameters 27 | * [FTR] #472 - Support for 3rd part app skeletons 28 | * [ENH] #512 - Per session expiration methods 29 | * [ENH] #496 - Type check renderArgs[CurrentLocalRenderArg] 30 | * [ENH] #490 - App.conf manual typo 31 | * [ENH] #487 - Make files executable on `revel build` 32 | * [ENH] #482 - Retain input values after form valdiation 33 | * [ENH] #473 - OnAppStart documentation 34 | * [ENH] #466 - JSON error template quoting fix 35 | * [ENH] #464 - Remove unneeded trace statement 36 | * [ENH] #457 - Remove unneeded trace 37 | 38 | 39 | ## Revel v0.8 (Jan 5, 2014) 40 | 41 | * [BUG] #379 - HTTP 500 error for not found public path files 42 | * [FTR] #424 - HTTP pprof support 43 | * [FTR] #346 - Redis Cache support 44 | * [FTR] #292 - SMTP Mailer 45 | * [ENH] #443 - Validator constructors to improve `v.Check()` usage 46 | * [ENH] #439 - Basic terminal output coloring 47 | * [ENH] #428 - Improve error message for missing `RenderArg` 48 | * [ENH] #422 - Route embedding for modules 49 | * [ENH] #413 - App version variable 50 | * [ENH] #153 - $GOPATH-wide file watching aka hot loading 51 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2012 Rob Figueiredo 2 | All Rights Reserved. 3 | 4 | MIT LICENSE 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy of 7 | this software and associated documentation files (the "Software"), to deal in 8 | the Software without restriction, including without limitation the rights to 9 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 10 | the Software, and to permit persons to whom the Software is furnished to do so, 11 | subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 18 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 19 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 20 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 21 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Revel Has Moved! 2 | 3 | This repo is here as a legacy archive. Please use the [new Revel repo](http://github.com/revel/revel). 4 | -------------------------------------------------------------------------------- /cache/init.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "github.com/robfig/revel" 5 | "strings" 6 | "time" 7 | ) 8 | 9 | func init() { 10 | revel.OnAppStart(func() { 11 | // Set the default expiration time. 12 | defaultExpiration := time.Hour // The default for the default is one hour. 13 | if expireStr, found := revel.Config.String("cache.expires"); found { 14 | var err error 15 | if defaultExpiration, err = time.ParseDuration(expireStr); err != nil { 16 | panic("Could not parse default cache expiration duration " + expireStr + ": " + err.Error()) 17 | } 18 | } 19 | 20 | // make sure you aren't trying to use both memcached and redis 21 | if revel.Config.BoolDefault("cache.memcached", false) && revel.Config.BoolDefault("cache.redis", false) { 22 | panic("You've configured both memcached and redis, please only include configuration for one cache!") 23 | } 24 | 25 | // Use memcached? 26 | if revel.Config.BoolDefault("cache.memcached", false) { 27 | hosts := strings.Split(revel.Config.StringDefault("cache.hosts", ""), ",") 28 | if len(hosts) == 0 { 29 | panic("Memcache enabled but no memcached hosts specified!") 30 | } 31 | 32 | Instance = NewMemcachedCache(hosts, defaultExpiration) 33 | return 34 | } 35 | 36 | // Use Redis (share same config as memcached)? 37 | if revel.Config.BoolDefault("cache.redis", false) { 38 | hosts := strings.Split(revel.Config.StringDefault("cache.hosts", ""), ",") 39 | if len(hosts) == 0 { 40 | panic("Redis enabled but no Redis hosts specified!") 41 | } 42 | if len(hosts) > 1 { 43 | panic("Redis currently only supports one host!") 44 | } 45 | password := revel.Config.StringDefault("cache.redis.password", "") 46 | Instance = NewRedisCache(hosts[0], password, defaultExpiration) 47 | return 48 | } 49 | 50 | // By default, use the in-memory cache. 51 | Instance = NewInMemoryCache(defaultExpiration) 52 | }) 53 | } 54 | -------------------------------------------------------------------------------- /cache/inmemory.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "fmt" 5 | "github.com/robfig/go-cache" 6 | "github.com/robfig/revel" 7 | "reflect" 8 | "time" 9 | ) 10 | 11 | type InMemoryCache struct { 12 | cache.Cache 13 | } 14 | 15 | func NewInMemoryCache(defaultExpiration time.Duration) InMemoryCache { 16 | return InMemoryCache{*cache.New(defaultExpiration, time.Minute)} 17 | } 18 | 19 | func (c InMemoryCache) Get(key string, ptrValue interface{}) error { 20 | value, found := c.Cache.Get(key) 21 | if !found { 22 | return ErrCacheMiss 23 | } 24 | 25 | v := reflect.ValueOf(ptrValue) 26 | if v.Type().Kind() == reflect.Ptr && v.Elem().CanSet() { 27 | v.Elem().Set(reflect.ValueOf(value)) 28 | return nil 29 | } 30 | 31 | err := fmt.Errorf("revel/cache: attempt to get %s, but can not set value %v", key, v) 32 | revel.ERROR.Println(err) 33 | return err 34 | } 35 | 36 | func (c InMemoryCache) GetMulti(keys ...string) (Getter, error) { 37 | return c, nil 38 | } 39 | 40 | func (c InMemoryCache) Set(key string, value interface{}, expires time.Duration) error { 41 | // NOTE: go-cache understands the values of DEFAULT and FOREVER 42 | c.Cache.Set(key, value, expires) 43 | return nil 44 | } 45 | 46 | func (c InMemoryCache) Add(key string, value interface{}, expires time.Duration) error { 47 | err := c.Cache.Add(key, value, expires) 48 | if err == cache.ErrKeyExists { 49 | return ErrNotStored 50 | } 51 | return err 52 | } 53 | 54 | func (c InMemoryCache) Replace(key string, value interface{}, expires time.Duration) error { 55 | if err := c.Cache.Replace(key, value, expires); err != nil { 56 | return ErrNotStored 57 | } 58 | return nil 59 | } 60 | 61 | func (c InMemoryCache) Delete(key string) error { 62 | if found := c.Cache.Delete(key); !found { 63 | return ErrCacheMiss 64 | } 65 | return nil 66 | } 67 | 68 | func (c InMemoryCache) Increment(key string, n uint64) (newValue uint64, err error) { 69 | newValue, err = c.Cache.Increment(key, n) 70 | if err == cache.ErrCacheMiss { 71 | return 0, ErrCacheMiss 72 | } 73 | return 74 | } 75 | 76 | func (c InMemoryCache) Decrement(key string, n uint64) (newValue uint64, err error) { 77 | newValue, err = c.Cache.Decrement(key, n) 78 | if err == cache.ErrCacheMiss { 79 | return 0, ErrCacheMiss 80 | } 81 | return 82 | } 83 | 84 | func (c InMemoryCache) Flush() error { 85 | c.Cache.Flush() 86 | return nil 87 | } 88 | -------------------------------------------------------------------------------- /cache/inmemory_test.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | ) 7 | 8 | var newInMemoryCache = func(_ *testing.T, defaultExpiration time.Duration) Cache { 9 | return NewInMemoryCache(defaultExpiration) 10 | } 11 | 12 | // Test typical cache interactions 13 | func TestInMemoryCache_TypicalGetSet(t *testing.T) { 14 | typicalGetSet(t, newInMemoryCache) 15 | } 16 | 17 | // Test the increment-decrement cases 18 | func TestInMemoryCache_IncrDecr(t *testing.T) { 19 | incrDecr(t, newInMemoryCache) 20 | } 21 | 22 | func TestInMemoryCache_Expiration(t *testing.T) { 23 | expiration(t, newInMemoryCache) 24 | } 25 | 26 | func TestInMemoryCache_EmptyCache(t *testing.T) { 27 | emptyCache(t, newInMemoryCache) 28 | } 29 | 30 | func TestInMemoryCache_Replace(t *testing.T) { 31 | testReplace(t, newInMemoryCache) 32 | } 33 | 34 | func TestInMemoryCache_Add(t *testing.T) { 35 | testAdd(t, newInMemoryCache) 36 | } 37 | -------------------------------------------------------------------------------- /cache/memcached_test.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "net" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | // These tests require memcached running on localhost:11211 (the default) 10 | const testServer = "localhost:11211" 11 | 12 | var newMemcachedCache = func(t *testing.T, defaultExpiration time.Duration) Cache { 13 | c, err := net.Dial("tcp", testServer) 14 | if err == nil { 15 | c.Write([]byte("flush_all\r\n")) 16 | c.Close() 17 | return NewMemcachedCache([]string{testServer}, defaultExpiration) 18 | } 19 | t.Errorf("couldn't connect to memcached on %s", testServer) 20 | t.FailNow() 21 | panic("") 22 | } 23 | 24 | func TestMemcachedCache_TypicalGetSet(t *testing.T) { 25 | typicalGetSet(t, newMemcachedCache) 26 | } 27 | 28 | func TestMemcachedCache_IncrDecr(t *testing.T) { 29 | incrDecr(t, newMemcachedCache) 30 | } 31 | 32 | func TestMemcachedCache_Expiration(t *testing.T) { 33 | expiration(t, newMemcachedCache) 34 | } 35 | 36 | func TestMemcachedCache_EmptyCache(t *testing.T) { 37 | emptyCache(t, newMemcachedCache) 38 | } 39 | 40 | func TestMemcachedCache_Replace(t *testing.T) { 41 | testReplace(t, newMemcachedCache) 42 | } 43 | 44 | func TestMemcachedCache_Add(t *testing.T) { 45 | testAdd(t, newMemcachedCache) 46 | } 47 | -------------------------------------------------------------------------------- /cache/redis_test.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "net" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | // These tests require redis server running on localhost:6379 (the default) 10 | const redisTestServer = "localhost:6379" 11 | 12 | var newRedisCache = func(t *testing.T, defaultExpiration time.Duration) Cache { 13 | c, err := net.Dial("tcp", redisTestServer) 14 | if err == nil { 15 | c.Write([]byte("flush_all\r\n")) 16 | c.Close() 17 | redisCache := NewRedisCache(redisTestServer, "", defaultExpiration) 18 | redisCache.Flush() 19 | return redisCache 20 | } 21 | t.Errorf("couldn't connect to redis on %s", redisTestServer) 22 | t.FailNow() 23 | panic("") 24 | } 25 | 26 | func TestRedisCache_TypicalGetSet(t *testing.T) { 27 | typicalGetSet(t, newRedisCache) 28 | } 29 | 30 | func TestRedisCache_IncrDecr(t *testing.T) { 31 | incrDecr(t, newRedisCache) 32 | } 33 | 34 | func TestRedisCache_Expiration(t *testing.T) { 35 | expiration(t, newRedisCache) 36 | } 37 | 38 | func TestRedisCache_EmptyCache(t *testing.T) { 39 | emptyCache(t, newRedisCache) 40 | } 41 | 42 | func TestRedisCache_Replace(t *testing.T) { 43 | testReplace(t, newRedisCache) 44 | } 45 | 46 | func TestRedisCache_Add(t *testing.T) { 47 | testAdd(t, newRedisCache) 48 | } 49 | -------------------------------------------------------------------------------- /cache/serialization.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "bytes" 5 | "encoding/gob" 6 | "github.com/robfig/revel" 7 | "reflect" 8 | "strconv" 9 | ) 10 | 11 | // Serialize transforms the given value into bytes following these rules: 12 | // - If value is a byte array, it is returned as-is. 13 | // - If value is an int or uint type, it is returned as the ASCII representation 14 | // - Else, encoding/gob is used to serialize 15 | func Serialize(value interface{}) ([]byte, error) { 16 | if bytes, ok := value.([]byte); ok { 17 | return bytes, nil 18 | } 19 | 20 | switch v := reflect.ValueOf(value); v.Kind() { 21 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 22 | return []byte(strconv.FormatInt(v.Int(), 10)), nil 23 | case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: 24 | return []byte(strconv.FormatUint(v.Uint(), 10)), nil 25 | } 26 | 27 | var b bytes.Buffer 28 | encoder := gob.NewEncoder(&b) 29 | if err := encoder.Encode(value); err != nil { 30 | revel.ERROR.Printf("revel/cache: gob encoding '%s' failed: %s", value, err) 31 | return nil, err 32 | } 33 | return b.Bytes(), nil 34 | } 35 | 36 | // Deserialize transforms bytes produced by Serialize back into a Go object, 37 | // storing it into "ptr", which must be a pointer to the value type. 38 | func Deserialize(byt []byte, ptr interface{}) (err error) { 39 | if bytes, ok := ptr.(*[]byte); ok { 40 | *bytes = byt 41 | return 42 | } 43 | 44 | if v := reflect.ValueOf(ptr); v.Kind() == reflect.Ptr { 45 | switch p := v.Elem(); p.Kind() { 46 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 47 | var i int64 48 | i, err = strconv.ParseInt(string(byt), 10, 64) 49 | if err != nil { 50 | revel.ERROR.Printf("revel/cache: failed to parse int '%s': %s", string(byt), err) 51 | } else { 52 | p.SetInt(i) 53 | } 54 | return 55 | 56 | case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: 57 | var i uint64 58 | i, err = strconv.ParseUint(string(byt), 10, 64) 59 | if err != nil { 60 | revel.ERROR.Printf("revel/cache: failed to parse uint '%s': %s", string(byt), err) 61 | } else { 62 | p.SetUint(i) 63 | } 64 | return 65 | } 66 | } 67 | 68 | b := bytes.NewBuffer(byt) 69 | decoder := gob.NewDecoder(b) 70 | if err = decoder.Decode(ptr); err != nil { 71 | revel.ERROR.Printf("revel/cache: gob decoding failed: %s", err) 72 | return 73 | } 74 | return 75 | } 76 | -------------------------------------------------------------------------------- /cache/serialization_test.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | type Struct1 struct { 9 | X int 10 | } 11 | 12 | func (s Struct1) Method1() {} 13 | 14 | type Interface1 interface { 15 | Method1() 16 | } 17 | 18 | var ( 19 | struct1 Struct1 = Struct1{1} 20 | ptrStruct *Struct1 = &Struct1{2} 21 | emptyIface interface{} = Struct1{3} 22 | iface1 Interface1 = Struct1{4} 23 | sliceStruct []Struct1 = []Struct1{{5}, {6}, {7}} 24 | ptrSliceStruct []*Struct1 = []*Struct1{{8}, {9}, {10}} 25 | 26 | VALUE_MAP = map[string]interface{}{ 27 | "bytes": []byte{0x61, 0x62, 0x63, 0x64}, 28 | "string": "string", 29 | "bool": true, 30 | "int": 5, 31 | "int8": int8(5), 32 | "int16": int16(5), 33 | "int32": int32(5), 34 | "int64": int64(5), 35 | "uint": uint(5), 36 | "uint8": uint8(5), 37 | "uint16": uint16(5), 38 | "uint32": uint32(5), 39 | "uint64": uint64(5), 40 | "float32": float32(5), 41 | "float64": float64(5), 42 | "array": [5]int{1, 2, 3, 4, 5}, 43 | "slice": []int{1, 2, 3, 4, 5}, 44 | "emptyIf": emptyIface, 45 | "Iface1": iface1, 46 | "map": map[string]string{"foo": "bar"}, 47 | "ptrStruct": ptrStruct, 48 | "struct1": struct1, 49 | "sliceStruct": sliceStruct, 50 | "ptrSliceStruct": ptrSliceStruct, 51 | } 52 | ) 53 | 54 | // Test passing all kinds of data between serialize and deserialize. 55 | func TestRoundTrip(t *testing.T) { 56 | for _, expected := range VALUE_MAP { 57 | bytes, err := Serialize(expected) 58 | if err != nil { 59 | t.Error(err) 60 | continue 61 | } 62 | 63 | ptrActual := reflect.New(reflect.TypeOf(expected)).Interface() 64 | err = Deserialize(bytes, ptrActual) 65 | if err != nil { 66 | t.Error(err) 67 | continue 68 | } 69 | 70 | actual := reflect.ValueOf(ptrActual).Elem().Interface() 71 | if !reflect.DeepEqual(expected, actual) { 72 | t.Errorf("(expected) %T %v != %T %v (actual)", expected, expected, actual, actual) 73 | } 74 | } 75 | } 76 | 77 | func zeroMap(arg map[string]interface{}) map[string]interface{} { 78 | result := map[string]interface{}{} 79 | for key, value := range arg { 80 | result[key] = reflect.Zero(reflect.TypeOf(value)).Interface() 81 | } 82 | return result 83 | } 84 | -------------------------------------------------------------------------------- /compress_test.go: -------------------------------------------------------------------------------- 1 | package revel 2 | 3 | import ( 4 | "net/http/httptest" 5 | "strings" 6 | "testing" 7 | ) 8 | 9 | // Test that the render response is as expected. 10 | func TestBenchmarkCompressed(t *testing.T) { 11 | startFakeBookingApp() 12 | resp := httptest.NewRecorder() 13 | c := NewController(NewRequest(showRequest), NewResponse(resp)) 14 | c.SetAction("Hotels", "Show") 15 | Config.SetOption("results.compressed", "true") 16 | result := Hotels{c}.Show(3) 17 | result.Apply(c.Request, c.Response) 18 | if !strings.Contains(resp.Body.String(), "300 Main St.") { 19 | t.Errorf("Failed to find hotel address in action response:\n%s", resp.Body) 20 | } 21 | } 22 | 23 | func BenchmarkRenderCompressed(b *testing.B) { 24 | startFakeBookingApp() 25 | resp := httptest.NewRecorder() 26 | resp.Body = nil 27 | c := NewController(NewRequest(showRequest), NewResponse(resp)) 28 | c.SetAction("Hotels", "Show") 29 | Config.SetOption("results.compressed", "true") 30 | b.ResetTimer() 31 | 32 | hotels := Hotels{c} 33 | for i := 0; i < b.N; i++ { 34 | hotels.Show(3).Apply(c.Request, c.Response) 35 | } 36 | } 37 | 38 | func BenchmarkRenderUnCompressed(b *testing.B) { 39 | startFakeBookingApp() 40 | resp := httptest.NewRecorder() 41 | resp.Body = nil 42 | c := NewController(NewRequest(showRequest), NewResponse(resp)) 43 | c.SetAction("Hotels", "Show") 44 | Config.SetOption("results.compressed", "false") 45 | b.ResetTimer() 46 | 47 | hotels := Hotels{c} 48 | for i := 0; i < b.N; i++ { 49 | hotels.Show(3).Apply(c.Request, c.Response) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /fakeapp_test.go: -------------------------------------------------------------------------------- 1 | package revel 2 | 3 | import ( 4 | "io/ioutil" 5 | "log" 6 | "os" 7 | "path" 8 | "path/filepath" 9 | "reflect" 10 | ) 11 | 12 | type Hotel struct { 13 | HotelId int 14 | Name, Address string 15 | City, State, Zip string 16 | Country string 17 | Price int 18 | } 19 | 20 | type Hotels struct { 21 | *Controller 22 | } 23 | 24 | type Static struct { 25 | *Controller 26 | } 27 | 28 | func (c Hotels) Show(id int) Result { 29 | title := "View Hotel" 30 | hotel := &Hotel{id, "A Hotel", "300 Main St.", "New York", "NY", "10010", "USA", 300} 31 | return c.Render(title, hotel) 32 | } 33 | 34 | func (c Hotels) Book(id int) Result { 35 | hotel := &Hotel{id, "A Hotel", "300 Main St.", "New York", "NY", "10010", "USA", 300} 36 | return c.RenderJson(hotel) 37 | } 38 | 39 | func (c Hotels) Index() Result { 40 | return c.RenderText("Hello, World!") 41 | } 42 | 43 | func (c Static) Serve(prefix, filepath string) Result { 44 | var basePath, dirName string 45 | 46 | if !path.IsAbs(dirName) { 47 | basePath = BasePath 48 | } 49 | 50 | fname := path.Join(basePath, prefix, filepath) 51 | file, err := os.Open(fname) 52 | if os.IsNotExist(err) { 53 | return c.NotFound("") 54 | } else if err != nil { 55 | WARN.Printf("Problem opening file (%s): %s ", fname, err) 56 | return c.NotFound("This was found but not sure why we couldn't open it.") 57 | } 58 | return c.RenderFile(file, "") 59 | } 60 | 61 | func startFakeBookingApp() { 62 | Init("prod", "github.com/robfig/revel/samples/booking", "") 63 | 64 | // Disable logging. 65 | TRACE = log.New(ioutil.Discard, "", 0) 66 | INFO = TRACE 67 | WARN = TRACE 68 | ERROR = TRACE 69 | 70 | runStartupHooks() 71 | 72 | MainRouter = NewRouter("") 73 | routesFile, _ := ioutil.ReadFile(filepath.Join(BasePath, "conf", "routes")) 74 | MainRouter.Routes, _ = parseRoutes("", "", string(routesFile), false) 75 | MainRouter.updateTree() 76 | MainTemplateLoader = NewTemplateLoader([]string{ViewsPath, path.Join(RevelPath, "templates")}) 77 | MainTemplateLoader.Refresh() 78 | 79 | RegisterController((*Hotels)(nil), 80 | []*MethodType{ 81 | &MethodType{ 82 | Name: "Index", 83 | }, 84 | &MethodType{ 85 | Name: "Show", 86 | Args: []*MethodArg{ 87 | {"id", reflect.TypeOf((*int)(nil))}, 88 | }, 89 | RenderArgNames: map[int][]string{31: []string{"title", "hotel"}}, 90 | }, 91 | &MethodType{ 92 | Name: "Book", 93 | Args: []*MethodArg{ 94 | {"id", reflect.TypeOf((*int)(nil))}, 95 | }, 96 | }, 97 | }) 98 | 99 | RegisterController((*Static)(nil), 100 | []*MethodType{ 101 | &MethodType{ 102 | Name: "Serve", 103 | Args: []*MethodArg{ 104 | &MethodArg{Name: "prefix", Type: reflect.TypeOf((*string)(nil))}, 105 | &MethodArg{Name: "filepath", Type: reflect.TypeOf((*string)(nil))}, 106 | }, 107 | RenderArgNames: map[int][]string{}, 108 | }, 109 | }) 110 | } 111 | -------------------------------------------------------------------------------- /field.go: -------------------------------------------------------------------------------- 1 | package revel 2 | 3 | import ( 4 | "reflect" 5 | "strings" 6 | ) 7 | 8 | // Field represents a data field that may be collected in a web form. 9 | type Field struct { 10 | Name string 11 | Error *ValidationError 12 | renderArgs map[string]interface{} 13 | } 14 | 15 | func NewField(name string, renderArgs map[string]interface{}) *Field { 16 | err, _ := renderArgs["errors"].(map[string]*ValidationError)[name] 17 | return &Field{ 18 | Name: name, 19 | Error: err, 20 | renderArgs: renderArgs, 21 | } 22 | } 23 | 24 | // Id returns an identifier suitable for use as an HTML id. 25 | func (f *Field) Id() string { 26 | return strings.Replace(f.Name, ".", "_", -1) 27 | } 28 | 29 | // Flash returns the flashed value of this Field. 30 | func (f *Field) Flash() string { 31 | v, _ := f.renderArgs["flash"].(map[string]string)[f.Name] 32 | return v 33 | } 34 | 35 | // FlashArray returns the flashed value of this Field as a list split on comma. 36 | func (f *Field) FlashArray() []string { 37 | v := f.Flash() 38 | if v == "" { 39 | return []string{} 40 | } 41 | return strings.Split(v, ",") 42 | } 43 | 44 | // Value returns the current value of this Field. 45 | func (f *Field) Value() interface{} { 46 | pieces := strings.Split(f.Name, ".") 47 | answer, ok := f.renderArgs[pieces[0]] 48 | if !ok { 49 | return "" 50 | } 51 | 52 | val := reflect.ValueOf(answer) 53 | for i := 1; i < len(pieces); i++ { 54 | if val.Kind() == reflect.Ptr { 55 | val = val.Elem() 56 | } 57 | val = val.FieldByName(pieces[i]) 58 | if !val.IsValid() { 59 | return "" 60 | } 61 | } 62 | 63 | return val.Interface() 64 | } 65 | 66 | // ErrorClass returns ERROR_CLASS if this field has a validation error, else empty string. 67 | func (f *Field) ErrorClass() string { 68 | if f.Error != nil { 69 | if errorClass, ok := f.renderArgs["ERROR_CLASS"]; ok { 70 | return errorClass.(string) 71 | } else { 72 | return ERROR_CLASS 73 | } 74 | } 75 | return "" 76 | } 77 | -------------------------------------------------------------------------------- /filter.go: -------------------------------------------------------------------------------- 1 | package revel 2 | 3 | type Filter func(c *Controller, filterChain []Filter) 4 | 5 | // Filters is the default set of global filters. 6 | // It may be set by the application on initialization. 7 | var Filters = []Filter{ 8 | PanicFilter, // Recover from panics and display an error page instead. 9 | RouterFilter, // Use the routing table to select the right Action. 10 | FilterConfiguringFilter, // A hook for adding or removing per-Action filters. 11 | ParamsFilter, // Parse parameters into Controller.Params. 12 | SessionFilter, // Restore and write the session cookie. 13 | FlashFilter, // Restore and write the flash cookie. 14 | ValidationFilter, // Restore kept validation errors and save new ones from cookie. 15 | I18nFilter, // Resolve the requested language. 16 | InterceptorFilter, // Run interceptors around the action. 17 | CompressFilter, // Compress the result. 18 | ActionInvoker, // Invoke the action. 19 | } 20 | 21 | // NilFilter and NilChain are helpful in writing filter tests. 22 | var ( 23 | NilFilter = func(_ *Controller, _ []Filter) {} 24 | NilChain = []Filter{NilFilter} 25 | ) 26 | -------------------------------------------------------------------------------- /flash.go: -------------------------------------------------------------------------------- 1 | package revel 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "net/url" 7 | ) 8 | 9 | // Flash represents a cookie that is overwritten on each request. 10 | // It allows data to be stored across one page at a time. 11 | // This is commonly used to implement success or error messages. 12 | // E.g. the Post/Redirect/Get pattern: 13 | // http://en.wikipedia.org/wiki/Post/Redirect/Get 14 | type Flash struct { 15 | Data, Out map[string]string 16 | } 17 | 18 | // Error serializes the given msg and args to an "error" key within 19 | // the Flash cookie. 20 | func (f Flash) Error(msg string, args ...interface{}) { 21 | if len(args) == 0 { 22 | f.Out["error"] = msg 23 | } else { 24 | f.Out["error"] = fmt.Sprintf(msg, args...) 25 | } 26 | } 27 | 28 | // Success serializes the given msg and args to a "success" key within 29 | // the Flash cookie. 30 | func (f Flash) Success(msg string, args ...interface{}) { 31 | if len(args) == 0 { 32 | f.Out["success"] = msg 33 | } else { 34 | f.Out["success"] = fmt.Sprintf(msg, args...) 35 | } 36 | } 37 | 38 | // FlashFilter is a Revel Filter that retrieves and sets the flash cookie. 39 | // Within Revel, it is available as a Flash attribute on Controller instances. 40 | // The name of the Flash cookie is set as CookiePrefix + "_FLASH". 41 | func FlashFilter(c *Controller, fc []Filter) { 42 | c.Flash = restoreFlash(c.Request.Request) 43 | c.RenderArgs["flash"] = c.Flash.Data 44 | 45 | fc[0](c, fc[1:]) 46 | 47 | // Store the flash. 48 | var flashValue string 49 | for key, value := range c.Flash.Out { 50 | flashValue += "\x00" + key + ":" + value + "\x00" 51 | } 52 | c.SetCookie(&http.Cookie{ 53 | Name: CookiePrefix + "_FLASH", 54 | Value: url.QueryEscape(flashValue), 55 | HttpOnly: CookieHttpOnly, 56 | Secure: CookieSecure, 57 | Path: "/", 58 | }) 59 | } 60 | 61 | // restoreFlash deserializes a Flash cookie struct from a request. 62 | func restoreFlash(req *http.Request) Flash { 63 | flash := Flash{ 64 | Data: make(map[string]string), 65 | Out: make(map[string]string), 66 | } 67 | if cookie, err := req.Cookie(CookiePrefix + "_FLASH"); err == nil { 68 | ParseKeyValueCookie(cookie.Value, func(key, val string) { 69 | flash.Data[key] = val 70 | }) 71 | } 72 | return flash 73 | } 74 | -------------------------------------------------------------------------------- /intercept_test.go: -------------------------------------------------------------------------------- 1 | package revel 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | var funcP = func(c *Controller) Result { return nil } 9 | var funcP2 = func(c *Controller) Result { return nil } 10 | 11 | type InterceptController struct{ *Controller } 12 | type InterceptControllerN struct{ InterceptController } 13 | type InterceptControllerP struct{ *InterceptController } 14 | type InterceptControllerNP struct { 15 | *Controller 16 | InterceptControllerN 17 | InterceptControllerP 18 | } 19 | 20 | func (c InterceptController) methN() Result { return nil } 21 | func (c *InterceptController) methP() Result { return nil } 22 | 23 | func (c InterceptControllerN) methNN() Result { return nil } 24 | func (c *InterceptControllerN) methNP() Result { return nil } 25 | func (c InterceptControllerP) methPN() Result { return nil } 26 | func (c *InterceptControllerP) methPP() Result { return nil } 27 | 28 | // Methods accessible from InterceptControllerN 29 | var METHODS_N = []interface{}{ 30 | InterceptController.methN, 31 | (*InterceptController).methP, 32 | InterceptControllerN.methNN, 33 | (*InterceptControllerN).methNP, 34 | } 35 | 36 | // Methods accessible from InterceptControllerP 37 | var METHODS_P = []interface{}{ 38 | InterceptController.methN, 39 | (*InterceptController).methP, 40 | InterceptControllerP.methPN, 41 | (*InterceptControllerP).methPP, 42 | } 43 | 44 | // This checks that all the various kinds of interceptor functions/methods are 45 | // properly invoked. 46 | func TestInvokeArgType(t *testing.T) { 47 | n := InterceptControllerN{InterceptController{&Controller{}}} 48 | p := InterceptControllerP{&InterceptController{&Controller{}}} 49 | np := InterceptControllerNP{&Controller{}, n, p} 50 | testInterceptorController(t, reflect.ValueOf(&n), METHODS_N) 51 | testInterceptorController(t, reflect.ValueOf(&p), METHODS_P) 52 | testInterceptorController(t, reflect.ValueOf(&np), METHODS_N) 53 | testInterceptorController(t, reflect.ValueOf(&np), METHODS_P) 54 | } 55 | 56 | func testInterceptorController(t *testing.T, appControllerPtr reflect.Value, methods []interface{}) { 57 | interceptors = []*Interception{} 58 | InterceptFunc(funcP, BEFORE, appControllerPtr.Elem().Interface()) 59 | InterceptFunc(funcP2, BEFORE, ALL_CONTROLLERS) 60 | for _, m := range methods { 61 | InterceptMethod(m, BEFORE) 62 | } 63 | ints := getInterceptors(BEFORE, appControllerPtr) 64 | 65 | if len(ints) != 6 { 66 | t.Fatalf("N: Expected 6 interceptors, got %d.", len(ints)) 67 | } 68 | 69 | testInterception(t, ints[0], reflect.ValueOf(&Controller{})) 70 | testInterception(t, ints[1], reflect.ValueOf(&Controller{})) 71 | for i := range methods { 72 | testInterception(t, ints[i+2], appControllerPtr) 73 | } 74 | } 75 | 76 | func testInterception(t *testing.T, intc *Interception, arg reflect.Value) { 77 | val := intc.Invoke(arg) 78 | if !val.IsNil() { 79 | t.Errorf("Failed (%s): Expected nil got %s", intc, val) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /invoker.go: -------------------------------------------------------------------------------- 1 | package revel 2 | 3 | import ( 4 | "code.google.com/p/go.net/websocket" 5 | "reflect" 6 | ) 7 | 8 | var ( 9 | controllerType = reflect.TypeOf(Controller{}) 10 | controllerPtrType = reflect.TypeOf(&Controller{}) 11 | websocketType = reflect.TypeOf((*websocket.Conn)(nil)) 12 | ) 13 | 14 | func ActionInvoker(c *Controller, _ []Filter) { 15 | // Instantiate the method. 16 | methodValue := reflect.ValueOf(c.AppController).MethodByName(c.MethodType.Name) 17 | 18 | // Collect the values for the method's arguments. 19 | var methodArgs []reflect.Value 20 | for _, arg := range c.MethodType.Args { 21 | // If they accept a websocket connection, treat that arg specially. 22 | var boundArg reflect.Value 23 | if arg.Type == websocketType { 24 | boundArg = reflect.ValueOf(c.Request.Websocket) 25 | } else { 26 | boundArg = Bind(c.Params, arg.Name, arg.Type) 27 | } 28 | methodArgs = append(methodArgs, boundArg) 29 | } 30 | 31 | var resultValue reflect.Value 32 | if methodValue.Type().IsVariadic() { 33 | resultValue = methodValue.CallSlice(methodArgs)[0] 34 | } else { 35 | resultValue = methodValue.Call(methodArgs)[0] 36 | } 37 | if resultValue.Kind() == reflect.Interface && !resultValue.IsNil() { 38 | c.Result = resultValue.Interface().(Result) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /libs.go: -------------------------------------------------------------------------------- 1 | package revel 2 | 3 | import ( 4 | "crypto/hmac" 5 | "crypto/sha1" 6 | "encoding/hex" 7 | "io" 8 | ) 9 | 10 | // Sign a given string with the app-configured secret key. 11 | // If no secret key is set, returns the empty string. 12 | // Return the signature in base64 (URLEncoding). 13 | func Sign(message string) string { 14 | if len(secretKey) == 0 { 15 | return "" 16 | } 17 | mac := hmac.New(sha1.New, secretKey) 18 | io.WriteString(mac, message) 19 | return hex.EncodeToString(mac.Sum(nil)) 20 | } 21 | 22 | // Verify returns true if the given signature is correct for the given message. 23 | // e.g. it matches what we generate with Sign() 24 | func Verify(message, sig string) bool { 25 | return hmac.Equal([]byte(sig), []byte(Sign(message))) 26 | } 27 | -------------------------------------------------------------------------------- /mail/connection.go: -------------------------------------------------------------------------------- 1 | package mail 2 | 3 | import ( 4 | "crypto/tls" 5 | "fmt" 6 | "net" 7 | "net/smtp" 8 | ) 9 | 10 | // Transport initialize the smtp client 11 | func Transport(address string, port int, host string, a smtp.Auth) (*smtp.Client, error) { 12 | addr := fmt.Sprintf("%s:%d", address, port) 13 | 14 | var conn net.Conn 15 | conn, err := tls.Dial("tcp", addr, nil) // some smtp servers require TLS handshake 16 | if err != nil { 17 | conn, err = net.Dial("tcp", addr) // fall back 18 | if err != nil { 19 | return nil, err 20 | } 21 | } 22 | 23 | c, err := smtp.NewClient(conn, address) 24 | if err != nil { 25 | return nil, err 26 | } 27 | 28 | if host != "" { 29 | if err := c.Hello(host); err != nil { 30 | return nil, err 31 | } 32 | } 33 | 34 | if ok, _ := c.Extension("STARTTLS"); ok { 35 | config := &tls.Config{ 36 | InsecureSkipVerify: true, 37 | } 38 | 39 | if err = c.StartTLS(config); err != nil { 40 | return nil, err 41 | } 42 | } 43 | if a != nil { 44 | if ok, _ := c.Extension("AUTH"); ok { 45 | if err = c.Auth(a); err != nil { 46 | return nil, err 47 | } 48 | } 49 | } 50 | return c, nil 51 | } 52 | 53 | // Send send message through the client 54 | func Send(c *smtp.Client, message *Message) (err error) { 55 | 56 | data, err := message.RenderData() 57 | if err != nil { 58 | return 59 | } 60 | 61 | if err = c.Reset(); err != nil { 62 | return 63 | } 64 | 65 | if err = c.Mail(message.From); err != nil { 66 | return 67 | } 68 | 69 | if err = addRcpt(c, message.To); err != nil { 70 | return 71 | } 72 | 73 | if err = addRcpt(c, message.Cc); err != nil { 74 | return 75 | } 76 | 77 | if err = addRcpt(c, message.Bcc); err != nil { 78 | return 79 | } 80 | 81 | w, err := c.Data() 82 | if err != nil { 83 | return 84 | } 85 | defer w.Close() 86 | 87 | _, err = w.Write(data) 88 | return 89 | } 90 | 91 | func addRcpt(c *smtp.Client, address []string) error { 92 | for _, addr := range address { 93 | if err := c.Rcpt(addr); err != nil { 94 | return err 95 | } 96 | } 97 | return nil 98 | } 99 | -------------------------------------------------------------------------------- /mail/mailer.go: -------------------------------------------------------------------------------- 1 | package mail 2 | 3 | import ( 4 | "net/smtp" 5 | ) 6 | 7 | type MailSender interface { 8 | SendMessage(messages ...*Message) (err error) 9 | } 10 | 11 | type Mailer struct { 12 | Server string 13 | Port int 14 | UserName string 15 | Password string 16 | Host string // This is optional, only used if you want to tell smtp server your hostname 17 | Auth smtp.Auth // This is optional, only used if Authentication is not plain 18 | Sender *Sender // This is optional, only used if the From/ReplyTo is not specified in the message 19 | } 20 | 21 | type Sender struct { 22 | From string 23 | ReplyTo string 24 | } 25 | 26 | // Send the given email messages using this Mailer. 27 | func (m *Mailer) SendMessage(messages ...*Message) (err error) { 28 | if m.Auth == nil { 29 | m.Auth = smtp.PlainAuth(m.UserName, m.UserName, m.Password, m.Server) 30 | } 31 | 32 | c, err := Transport(m.Server, m.Port, m.Host, m.Auth) 33 | if err != nil { 34 | return 35 | } 36 | defer c.Quit() 37 | 38 | for _, message := range messages { 39 | m.fillDefault(message) 40 | if err = Send(c, message); err != nil { 41 | return 42 | } 43 | } 44 | 45 | return 46 | } 47 | 48 | func (m *Mailer) fillDefault(message *Message) { 49 | if m.Sender == nil { 50 | return 51 | } 52 | if message.From == "" { 53 | message.From = m.Sender.From 54 | } 55 | 56 | if message.ReplyTo == "" { 57 | message.ReplyTo = m.Sender.ReplyTo 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /mail/message_test.go: -------------------------------------------------------------------------------- 1 | package mail 2 | 3 | import ( 4 | "bytes" 5 | "strings" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | func TestRenderRecipient(t *testing.T) { 11 | message := &Message{From: "foo@bar.com", To: []string{"bar@foo.com", "abc@test.com"}, ReplyTo: "none@arkxu.com", 12 | Subject: "from message1, single connection", PlainBody: bytes.NewBufferString("

你好 from message1, should show in plain text

")} 13 | 14 | var b bytes.Buffer 15 | message.writeRecipient(&b) 16 | recipient := b.String() 17 | 18 | if !strings.Contains(recipient, "From: foo@bar.com") { 19 | t.Error("Recipient should contain From") 20 | } 21 | 22 | if !strings.Contains(recipient, "Reply-To: none@arkxu.com") { 23 | t.Error("Recipient should contain Reply-To") 24 | } 25 | 26 | if !strings.Contains(recipient, "To: bar@foo.com, abc@test.com") { 27 | t.Error("Recipient should contain To") 28 | } 29 | 30 | if !strings.Contains(recipient, "Subject: from message1") { 31 | t.Error("Recipient should contain Subject") 32 | } 33 | } 34 | 35 | func TestRenderRecipientNoReply(t *testing.T) { 36 | message := &Message{From: "foo@bar.com", To: []string{"bar@foo.com", "abc@test.com"}, 37 | Subject: "这个是第11封from message1, single connection", PlainBody: bytes.NewBufferString("

你好 from message1, should show in plain text

")} 38 | 39 | var b bytes.Buffer 40 | message.writeRecipient(&b) 41 | recipient := b.String() 42 | 43 | if strings.Contains(recipient, "Reply-To") { 44 | t.Error("Recipient should not contains Reply-To") 45 | } 46 | 47 | if strings.Contains(recipient, "这个是第11封from message1, single connection") { 48 | t.Error("Subject should be encoded") 49 | } 50 | } 51 | 52 | func TestRenderPlainAndHtmlText(t *testing.T) { 53 | plainBody := "你好 from message1, should show in plain text" 54 | htmlBody := "

你好 from message1, should show in html text

" 55 | testDate, _ := time.Parse("2006-Jan-02", "2014-Feb-23") 56 | message := &Message{ 57 | From: "foo@bar.com", 58 | To: []string{"bar@foo.com", "abc@test.com"}, 59 | Subject: "这个是第11封from message1, single connection", 60 | PlainBody: bytes.NewBufferString(plainBody), 61 | HtmlBody: bytes.NewBufferString(htmlBody), 62 | Date: testDate, 63 | MessageId: "message-id@bar.com", 64 | } 65 | 66 | b, _ := message.RenderData() 67 | recipient := string(b) 68 | 69 | if !strings.Contains(recipient, plainBody) { 70 | t.Errorf("should have plain body: %s \n", plainBody) 71 | } 72 | 73 | if !strings.Contains(recipient, htmlBody) { 74 | t.Errorf("should have html body: %s \n", htmlBody) 75 | } 76 | 77 | if !strings.Contains(recipient, "Date: Sun, 23 Feb 2014 00:00:00 GMT") { 78 | t.Error("Message should have the Date header set") 79 | } 80 | 81 | if !strings.Contains(recipient, "Message-Id: ") { 82 | t.Error("Message should have the Message-Id header set") 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /mail/testdata/testTemplate.html: -------------------------------------------------------------------------------- 1 |

Hello {{.world}}

2 | 3 |

Welcome {{.user.Name}}, please click the link:

4 | {{.user.Link}} -------------------------------------------------------------------------------- /mail/testdata/testTemplate.txt: -------------------------------------------------------------------------------- 1 | Hello {{.world}} 2 | 3 | Welcome {{.user.Name}}, please click the link: 4 | {{.user.Link}} -------------------------------------------------------------------------------- /modules/db/app/db.go: -------------------------------------------------------------------------------- 1 | // This module configures a database connection for the application. 2 | // 3 | // Developers use this module by importing and calling db.Init(). 4 | // A "Transactional" controller type is provided as a way to import interceptors 5 | // that manage the transaction 6 | // 7 | // In particular, a transaction is begun before each request and committed on 8 | // success. If a panic occurred during the request, the transaction is rolled 9 | // back. (The application may also roll the transaction back itself.) 10 | package db 11 | 12 | import ( 13 | "database/sql" 14 | "github.com/robfig/revel" 15 | ) 16 | 17 | var ( 18 | Db *sql.DB 19 | Driver string 20 | Spec string 21 | ) 22 | 23 | func Init() { 24 | // Read configuration. 25 | var found bool 26 | if Driver, found = revel.Config.String("db.driver"); !found { 27 | revel.ERROR.Fatal("No db.driver found.") 28 | } 29 | if Spec, found = revel.Config.String("db.spec"); !found { 30 | revel.ERROR.Fatal("No db.spec found.") 31 | } 32 | 33 | // Open a connection. 34 | var err error 35 | Db, err = sql.Open(Driver, Spec) 36 | if err != nil { 37 | revel.ERROR.Fatal(err) 38 | } 39 | } 40 | 41 | type Transactional struct { 42 | *revel.Controller 43 | Txn *sql.Tx 44 | } 45 | 46 | // Begin a transaction 47 | func (c *Transactional) Begin() revel.Result { 48 | txn, err := Db.Begin() 49 | if err != nil { 50 | panic(err) 51 | } 52 | c.Txn = txn 53 | return nil 54 | } 55 | 56 | // Rollback if it's still going (must have panicked). 57 | func (c *Transactional) Rollback() revel.Result { 58 | if c.Txn != nil { 59 | if err := c.Txn.Rollback(); err != nil { 60 | if err != sql.ErrTxDone { 61 | panic(err) 62 | } 63 | } 64 | c.Txn = nil 65 | } 66 | return nil 67 | } 68 | 69 | // Commit the transaction. 70 | func (c *Transactional) Commit() revel.Result { 71 | if c.Txn != nil { 72 | if err := c.Txn.Commit(); err != nil { 73 | if err != sql.ErrTxDone { 74 | panic(err) 75 | } 76 | } 77 | c.Txn = nil 78 | } 79 | return nil 80 | } 81 | 82 | func init() { 83 | revel.InterceptMethod((*Transactional).Begin, revel.BEFORE) 84 | revel.InterceptMethod((*Transactional).Commit, revel.AFTER) 85 | revel.InterceptMethod((*Transactional).Rollback, revel.FINALLY) 86 | } 87 | -------------------------------------------------------------------------------- /modules/jobs/app/controllers/status.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "github.com/robfig/cron" 5 | "github.com/robfig/revel" 6 | "github.com/robfig/revel/modules/jobs/app/jobs" 7 | "strings" 8 | ) 9 | 10 | type Jobs struct { 11 | *revel.Controller 12 | } 13 | 14 | func (c Jobs) Status() revel.Result { 15 | if _, ok := c.Request.Header["X-Forwarded-For"]; ok || !strings.HasPrefix(c.Request.RemoteAddr, "127.0.0.1:") { 16 | return c.Forbidden("%s is not local", c.Request.RemoteAddr) 17 | } 18 | entries := jobs.MainCron.Entries() 19 | return c.Render(entries) 20 | } 21 | 22 | func init() { 23 | revel.TemplateFuncs["castjob"] = func(job cron.Job) *jobs.Job { 24 | return job.(*jobs.Job) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /modules/jobs/app/jobs/job.go: -------------------------------------------------------------------------------- 1 | package jobs 2 | 3 | import ( 4 | "github.com/robfig/cron" 5 | "github.com/robfig/revel" 6 | "reflect" 7 | "runtime/debug" 8 | "sync" 9 | "sync/atomic" 10 | ) 11 | 12 | type Job struct { 13 | Name string 14 | inner cron.Job 15 | status uint32 16 | running sync.Mutex 17 | } 18 | 19 | const UNNAMED = "(unnamed)" 20 | 21 | func New(job cron.Job) *Job { 22 | name := reflect.TypeOf(job).Name() 23 | if name == "Func" { 24 | name = UNNAMED 25 | } 26 | return &Job{ 27 | Name: name, 28 | inner: job, 29 | } 30 | } 31 | 32 | func (j *Job) Status() string { 33 | if atomic.LoadUint32(&j.status) > 0 { 34 | return "RUNNING" 35 | } 36 | return "IDLE" 37 | } 38 | 39 | func (j *Job) Run() { 40 | // If the job panics, just print a stack trace. 41 | // Don't let the whole process die. 42 | defer func() { 43 | if err := recover(); err != nil { 44 | if revelError := revel.NewErrorFromPanic(err); revelError != nil { 45 | revel.ERROR.Print(err, "\n", revelError.Stack) 46 | } else { 47 | revel.ERROR.Print(err, "\n", string(debug.Stack())) 48 | } 49 | } 50 | }() 51 | 52 | if !selfConcurrent { 53 | j.running.Lock() 54 | defer j.running.Unlock() 55 | } 56 | 57 | if workPermits != nil { 58 | workPermits <- struct{}{} 59 | defer func() { <-workPermits }() 60 | } 61 | 62 | atomic.StoreUint32(&j.status, 1) 63 | defer atomic.StoreUint32(&j.status, 0) 64 | 65 | j.inner.Run() 66 | } 67 | -------------------------------------------------------------------------------- /modules/jobs/app/jobs/jobrunner.go: -------------------------------------------------------------------------------- 1 | // A job runner for executing scheduled or ad-hoc tasks asynchronously from HTTP requests. 2 | // 3 | // It adds a couple of features on top of the cron package to make it play nicely with Revel: 4 | // 1. Protection against job panics. (They print to ERROR instead of take down the process) 5 | // 2. (Optional) Limit on the number of jobs that may run simulatenously, to 6 | // limit resource consumption. 7 | // 3. (Optional) Protection against multiple instances of a single job running 8 | // concurrently. If one execution runs into the next, the next will be queued. 9 | // 4. Cron expressions may be defined in app.conf and are reusable across jobs. 10 | // 5. Job status reporting. 11 | package jobs 12 | 13 | import ( 14 | "github.com/robfig/cron" 15 | "github.com/robfig/revel" 16 | "strings" 17 | "time" 18 | ) 19 | 20 | // Callers can use jobs.Func to wrap a raw func. 21 | // (Copying the type to this package makes it more visible) 22 | // 23 | // For example: 24 | // jobs.Schedule("cron.frequent", jobs.Func(myFunc)) 25 | type Func func() 26 | 27 | func (r Func) Run() { r() } 28 | 29 | func Schedule(spec string, job cron.Job) error { 30 | // Look to see if given spec is a key from the Config. 31 | if strings.HasPrefix(spec, "cron.") { 32 | confSpec, found := revel.Config.String(spec) 33 | if !found { 34 | panic("Cron spec not found: " + spec) 35 | } 36 | spec = confSpec 37 | } 38 | sched, err := cron.Parse(spec) 39 | if err != nil { 40 | return err 41 | } 42 | MainCron.Schedule(sched, New(job)) 43 | return nil 44 | } 45 | 46 | // Run the given job at a fixed interval. 47 | // The interval provided is the time between the job ending and the job being run again. 48 | // The time that the job takes to run is not included in the interval. 49 | func Every(duration time.Duration, job cron.Job) { 50 | MainCron.Schedule(cron.Every(duration), New(job)) 51 | } 52 | 53 | // Run the given job right now. 54 | func Now(job cron.Job) { 55 | go New(job).Run() 56 | } 57 | 58 | // Run the given job once, after the given delay. 59 | func In(duration time.Duration, job cron.Job) { 60 | go func() { 61 | time.Sleep(duration) 62 | New(job).Run() 63 | }() 64 | } 65 | -------------------------------------------------------------------------------- /modules/jobs/app/jobs/plugin.go: -------------------------------------------------------------------------------- 1 | package jobs 2 | 3 | import ( 4 | "fmt" 5 | "github.com/robfig/cron" 6 | "github.com/robfig/revel" 7 | ) 8 | 9 | const DEFAULT_JOB_POOL_SIZE = 10 10 | 11 | var ( 12 | // Singleton instance of the underlying job scheduler. 13 | MainCron *cron.Cron 14 | 15 | // This limits the number of jobs allowed to run concurrently. 16 | workPermits chan struct{} 17 | 18 | // Is a single job allowed to run concurrently with itself? 19 | selfConcurrent bool 20 | ) 21 | 22 | func init() { 23 | MainCron = cron.New() 24 | revel.OnAppStart(func() { 25 | if size := revel.Config.IntDefault("jobs.pool", DEFAULT_JOB_POOL_SIZE); size > 0 { 26 | workPermits = make(chan struct{}, size) 27 | } 28 | selfConcurrent = revel.Config.BoolDefault("jobs.selfconcurrent", false) 29 | MainCron.Start() 30 | fmt.Println("Go to /@jobs to see job status.") 31 | }) 32 | } 33 | -------------------------------------------------------------------------------- /modules/jobs/app/views/Jobs/Status.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 23 | 24 | 25 | 26 |

Scheduled Jobs

27 | 28 | 29 | 30 | {{range .entries}} 31 | {{$job := castjob .Job}} 32 | 33 | 34 | 35 | 36 | 37 | 38 | {{end}} 39 |
NameStatusLast runNext run
{{$job.Name}}{{$job.Status}}{{if not .Prev.IsZero}}{{.Prev.Format "2006-01-02 15:04:05"}}{{end}}{{if not .Next.IsZero}}{{.Next.Format "2006-01-02 15:04:05"}}{{end}}
40 | -------------------------------------------------------------------------------- /modules/jobs/conf/routes: -------------------------------------------------------------------------------- 1 | GET /@jobs Jobs.Status 2 | -------------------------------------------------------------------------------- /modules/pprof/README.md: -------------------------------------------------------------------------------- 1 | Revel pprof module 2 | ============ 3 | 4 | #### How to use: 5 | 6 | 1. Open your app.conf file and add the following line: 7 | `module.pprof=github.com/robfig/revel/modules/pprof` 8 | This will enable the pprof module. 9 | 10 | 2. Next, open your routes file and add: 11 | `module:pprof` **Note:** Do not change these routes. The pprof command-line tool by default assumes these routes. 12 | 13 | Congrats! You can now profile your application. To use the web interface, visit `http://:/debug/pprof`. You can also use the `go tool pprof` command to profile your application and get a little deeper. Use the command by running `go tool pprof http://:`. For example, if you modified the booking sample, you would run: `go tool pprof $GOPATH/bin/booking http://localhost:9000` (assuming you used the default `revel run` arguments. 14 | 15 | The command-line tool will take a 30-second CPU profile, and save the results to a temporary file in your `$HOME/pprof` directory. You can reference this file at a later time by using the same command as above, but by specifying the filename instead of the server address. 16 | 17 | In order to fully utilize the command-line tool, you may need the graphviz utilities. If you're on OS X, you can install these easily using Homebrew: `brew install graphviz`. 18 | 19 | To read more about profiling Go programs, here is some reading material: [The Go Blog](http://blog.golang.org/profiling-go-programs) : [net/pprof package documentation](http://golang.org/pkg/net/http/pprof/) -------------------------------------------------------------------------------- /modules/pprof/app/controllers/pprof.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "github.com/robfig/revel" 5 | "net/http" 6 | "net/http/pprof" 7 | ) 8 | 9 | type Pprof struct { 10 | *revel.Controller 11 | } 12 | 13 | // The PprofHandler type makes it easy to call the net/http/pprof handler methods 14 | // since they all have the same method signature 15 | type PprofHandler func(http.ResponseWriter, *http.Request) 16 | 17 | func (r PprofHandler) Apply(req *revel.Request, resp *revel.Response) { 18 | r(resp.Out, req.Request) 19 | } 20 | 21 | func (c Pprof) Profile() revel.Result { 22 | return PprofHandler(pprof.Profile) 23 | } 24 | 25 | func (c Pprof) Symbol() revel.Result { 26 | return PprofHandler(pprof.Symbol) 27 | } 28 | 29 | func (c Pprof) Cmdline() revel.Result { 30 | return PprofHandler(pprof.Cmdline) 31 | } 32 | 33 | func (c Pprof) Index() revel.Result { 34 | return PprofHandler(pprof.Index) 35 | } 36 | -------------------------------------------------------------------------------- /modules/pprof/conf/routes: -------------------------------------------------------------------------------- 1 | GET /pprof/profile Pprof.Profile 2 | GET /pprof/symbol Pprof.Symbol 3 | GET /debug/pprof/cmdline Pprof.Cmdline 4 | GET /debug/pprof/* Pprof.Index 5 | GET /debug/pprof/ Pprof.Index 6 | -------------------------------------------------------------------------------- /modules/testrunner/app/plugin.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "fmt" 5 | "github.com/robfig/revel" 6 | ) 7 | 8 | func init() { 9 | revel.OnAppStart(func() { 10 | fmt.Println("Go to /@tests to run the tests.") 11 | }) 12 | } 13 | -------------------------------------------------------------------------------- /modules/testrunner/app/views/TestRunner/FailureDetail.html: -------------------------------------------------------------------------------- 1 | {{.Description}}
2 | In {{.Path}} 3 | {{if .Line}} 4 | (around {{if .Line}}line {{.Line}}{{end}}{{if .Column}} column {{.Column}}{{end}}) 5 | {{end}}: 6 | {{range .ContextSource}}{{if .IsError}}{{.Source}}{{end}}{{end}}
7 | 9 | Show Stack
{{.Stack}}
-------------------------------------------------------------------------------- /modules/testrunner/app/views/TestRunner/Index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Revel Test Runner 5 | 6 | 7 | 8 | 15 | 16 | 17 |
18 |
19 |
20 |

Test Runner

21 |

Run all of your application's tests from here.

22 |
23 | 24 |
25 |
26 |
27 | 28 |
29 | {{range .testSuites}} 30 |

{{.Name}}

31 | 32 | {{range .Tests}} 33 | 34 | 35 | 37 | 38 | 39 | {{end}} 40 |
{{.Name}} 36 |
41 | {{end}} 42 |
43 | 44 | 82 | 83 | 84 | 85 | -------------------------------------------------------------------------------- /modules/testrunner/app/views/TestRunner/SuiteResult.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Revel Test Runner 5 | 6 | 7 | 26 | 27 | 28 | 29 |
30 |
31 |

{{.Name}}

32 |

{{if .Passed}}PASSED{{else}}FAILED{{end}}

33 |
34 |
35 | 36 |
37 | 38 | {{range .Results}} 39 | 40 | 41 | 42 | 43 | {{end}} 44 |
{{.Name}}{{if .ErrorHtml}}{{.ErrorHtml}}{{else}}PASSED{{end}}
45 |
46 | 47 | 48 | -------------------------------------------------------------------------------- /modules/testrunner/conf/routes: -------------------------------------------------------------------------------- 1 | GET /@tests TestRunner.Index 2 | GET /@tests.list TestRunner.List 3 | GET /@tests/public/*filepath Static.ServeModule(testrunner,public) 4 | GET /@tests/:suite/:test TestRunner.Run 5 | -------------------------------------------------------------------------------- /modules/testrunner/public/images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robfig/revel/e8aac16df9e93b17d309595b4ff57405dc567fdf/modules/testrunner/public/images/favicon.png -------------------------------------------------------------------------------- /panic.go: -------------------------------------------------------------------------------- 1 | package revel 2 | 3 | import ( 4 | "runtime/debug" 5 | ) 6 | 7 | // PanicFilter wraps the action invocation in a protective defer blanket that 8 | // converts panics into 500 error pages. 9 | func PanicFilter(c *Controller, fc []Filter) { 10 | defer func() { 11 | if err := recover(); err != nil { 12 | handleInvocationPanic(c, err) 13 | } 14 | }() 15 | fc[0](c, fc[1:]) 16 | } 17 | 18 | // This function handles a panic in an action invocation. 19 | // It cleans up the stack trace, logs it, and displays an error page. 20 | func handleInvocationPanic(c *Controller, err interface{}) { 21 | error := NewErrorFromPanic(err) 22 | if error == nil { 23 | ERROR.Print(err, "\n", string(debug.Stack())) 24 | c.Response.Out.WriteHeader(500) 25 | c.Response.Out.Write(debug.Stack()) 26 | return 27 | } 28 | 29 | ERROR.Print(err, "\n", error.Stack) 30 | c.Result = c.RenderError(error) 31 | } 32 | -------------------------------------------------------------------------------- /results_test.go: -------------------------------------------------------------------------------- 1 | package revel 2 | 3 | import ( 4 | "net/http/httptest" 5 | "strings" 6 | "testing" 7 | ) 8 | 9 | // Test that the render response is as expected. 10 | func TestBenchmarkRender(t *testing.T) { 11 | startFakeBookingApp() 12 | resp := httptest.NewRecorder() 13 | c := NewController(NewRequest(showRequest), NewResponse(resp)) 14 | c.SetAction("Hotels", "Show") 15 | result := Hotels{c}.Show(3) 16 | result.Apply(c.Request, c.Response) 17 | if !strings.Contains(resp.Body.String(), "300 Main St.") { 18 | t.Errorf("Failed to find hotel address in action response:\n%s", resp.Body) 19 | } 20 | } 21 | 22 | func BenchmarkRenderChunked(b *testing.B) { 23 | startFakeBookingApp() 24 | resp := httptest.NewRecorder() 25 | resp.Body = nil 26 | c := NewController(NewRequest(showRequest), NewResponse(resp)) 27 | c.SetAction("Hotels", "Show") 28 | Config.SetOption("results.chunked", "true") 29 | b.ResetTimer() 30 | 31 | hotels := Hotels{c} 32 | for i := 0; i < b.N; i++ { 33 | hotels.Show(3).Apply(c.Request, c.Response) 34 | } 35 | } 36 | 37 | func BenchmarkRenderNotChunked(b *testing.B) { 38 | startFakeBookingApp() 39 | resp := httptest.NewRecorder() 40 | resp.Body = nil 41 | c := NewController(NewRequest(showRequest), NewResponse(resp)) 42 | c.SetAction("Hotels", "Show") 43 | Config.SetOption("results.chunked", "false") 44 | b.ResetTimer() 45 | 46 | hotels := Hotels{c} 47 | for i := 0; i < b.N; i++ { 48 | hotels.Show(3).Apply(c.Request, c.Response) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /revel/clean.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "go/build" 6 | "os" 7 | "path" 8 | ) 9 | 10 | var cmdClean = &Command{ 11 | UsageLine: "clean [import path]", 12 | Short: "clean a Revel application's temp files", 13 | Long: ` 14 | Clean the Revel web application named by the given import path. 15 | 16 | For example: 17 | 18 | revel clean github.com/robfig/revel/samples/chat 19 | 20 | It removes the app/tmp directory. 21 | `, 22 | } 23 | 24 | func init() { 25 | cmdClean.Run = cleanApp 26 | } 27 | 28 | func cleanApp(args []string) { 29 | if len(args) == 0 { 30 | fmt.Fprintf(os.Stderr, cmdClean.Long) 31 | return 32 | } 33 | 34 | appPkg, err := build.Import(args[0], "", build.FindOnly) 35 | if err != nil { 36 | fmt.Fprintln(os.Stderr, "Abort: Failed to find import path:", err) 37 | return 38 | } 39 | 40 | // Remove the app/tmp directory. 41 | tmpDir := path.Join(appPkg.Dir, "app", "tmp") 42 | fmt.Println("Removing:", tmpDir) 43 | err = os.RemoveAll(tmpDir) 44 | if err != nil { 45 | fmt.Fprintln(os.Stderr, "Abort:", err) 46 | return 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /revel/package.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/robfig/revel" 6 | "io/ioutil" 7 | "os" 8 | "path/filepath" 9 | ) 10 | 11 | var cmdPackage = &Command{ 12 | UsageLine: "package [import path]", 13 | Short: "package a Revel application (e.g. for deployment)", 14 | Long: ` 15 | Package the Revel web application named by the given import path. 16 | This allows it to be deployed and run on a machine that lacks a Go installation. 17 | 18 | For example: 19 | 20 | revel package github.com/robfig/revel/samples/chat 21 | `, 22 | } 23 | 24 | func init() { 25 | cmdPackage.Run = packageApp 26 | } 27 | 28 | func packageApp(args []string) { 29 | if len(args) == 0 { 30 | fmt.Fprint(os.Stderr, cmdPackage.Long) 31 | return 32 | } 33 | 34 | appImportPath := args[0] 35 | revel.Init("", appImportPath, "") 36 | 37 | // Remove the archive if it already exists. 38 | destFile := filepath.Base(revel.BasePath) + ".tar.gz" 39 | os.Remove(destFile) 40 | 41 | // Collect stuff in a temp directory. 42 | tmpDir, err := ioutil.TempDir("", filepath.Base(revel.BasePath)) 43 | panicOnError(err, "Failed to get temp dir") 44 | 45 | buildApp([]string{args[0], tmpDir}) 46 | 47 | // Create the zip file. 48 | archiveName := mustTarGzDir(destFile, tmpDir) 49 | 50 | fmt.Println("Your archive is ready:", archiveName) 51 | } 52 | -------------------------------------------------------------------------------- /revel/package_run.bat.template: -------------------------------------------------------------------------------- 1 | @echo off 2 | {{.BinName}} -importPath {{.ImportPath}} -srcPath %CD%\src -runMode prod 3 | -------------------------------------------------------------------------------- /revel/package_run.sh.template: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | SCRIPTPATH=$(cd "$(dirname "$0")"; pwd) 3 | "$SCRIPTPATH/{{.BinName}}" -importPath {{.ImportPath}} -srcPath "$SCRIPTPATH/src" -runMode prod 4 | -------------------------------------------------------------------------------- /revel/rev.go: -------------------------------------------------------------------------------- 1 | // The command line tool for running Revel apps. 2 | package main 3 | 4 | import ( 5 | "flag" 6 | "fmt" 7 | "github.com/agtorre/gocolorize" 8 | "io" 9 | "math/rand" 10 | "os" 11 | "runtime" 12 | "strings" 13 | "text/template" 14 | "time" 15 | ) 16 | 17 | // Cribbed from the genius organization of the "go" command. 18 | type Command struct { 19 | Run func(args []string) 20 | UsageLine, Short, Long string 21 | } 22 | 23 | func (cmd *Command) Name() string { 24 | name := cmd.UsageLine 25 | i := strings.Index(name, " ") 26 | if i >= 0 { 27 | name = name[:i] 28 | } 29 | return name 30 | } 31 | 32 | var commands = []*Command{ 33 | cmdNew, 34 | cmdRun, 35 | cmdBuild, 36 | cmdPackage, 37 | cmdClean, 38 | cmdTest, 39 | } 40 | 41 | func main() { 42 | if runtime.GOOS == "windows" { 43 | gocolorize.SetPlain(true) 44 | } 45 | fmt.Fprintf(os.Stdout, gocolorize.NewColor("blue").Paint(header)) 46 | flag.Usage = func() { usage(1) } 47 | flag.Parse() 48 | args := flag.Args() 49 | 50 | if len(args) < 1 || args[0] == "help" { 51 | if len(args) == 1 { 52 | usage(0) 53 | } 54 | if len(args) > 1 { 55 | for _, cmd := range commands { 56 | if cmd.Name() == args[1] { 57 | tmpl(os.Stdout, helpTemplate, cmd) 58 | return 59 | } 60 | } 61 | } 62 | usage(2) 63 | } 64 | 65 | // Commands use panic to abort execution when something goes wrong. 66 | // Panics are logged at the point of error. Ignore those. 67 | defer func() { 68 | if err := recover(); err != nil { 69 | if _, ok := err.(LoggedError); !ok { 70 | // This panic was not expected / logged. 71 | panic(err) 72 | } 73 | os.Exit(1) 74 | } 75 | }() 76 | 77 | for _, cmd := range commands { 78 | if cmd.Name() == args[0] { 79 | cmd.Run(args[1:]) 80 | return 81 | } 82 | } 83 | 84 | errorf("unknown command %q\nRun 'revel help' for usage.\n", args[0]) 85 | } 86 | 87 | func errorf(format string, args ...interface{}) { 88 | // Ensure the user's command prompt starts on the next line. 89 | if !strings.HasSuffix(format, "\n") { 90 | format += "\n" 91 | } 92 | fmt.Fprintf(os.Stderr, format, args...) 93 | panic(LoggedError{}) // Panic instead of os.Exit so that deferred will run. 94 | } 95 | 96 | const header = `~ 97 | ~ revel! http://robfig.github.com/revel 98 | ~ 99 | ` 100 | 101 | const usageTemplate = `usage: revel command [arguments] 102 | 103 | The commands are: 104 | {{range .}} 105 | {{.Name | printf "%-11s"}} {{.Short}}{{end}} 106 | 107 | Use "revel help [command]" for more information. 108 | ` 109 | 110 | var helpTemplate = `usage: revel {{.UsageLine}} 111 | {{.Long}} 112 | ` 113 | 114 | func usage(exitCode int) { 115 | tmpl(os.Stderr, usageTemplate, commands) 116 | os.Exit(exitCode) 117 | } 118 | 119 | func tmpl(w io.Writer, text string, data interface{}) { 120 | t := template.New("top") 121 | template.Must(t.Parse(text)) 122 | if err := t.Execute(w, data); err != nil { 123 | panic(err) 124 | } 125 | } 126 | 127 | func init() { 128 | rand.Seed(time.Now().UnixNano()) 129 | } 130 | -------------------------------------------------------------------------------- /revel/run.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/robfig/revel" 5 | "github.com/robfig/revel/harness" 6 | "strconv" 7 | ) 8 | 9 | var cmdRun = &Command{ 10 | UsageLine: "run [import path] [run mode] [port]", 11 | Short: "run a Revel application", 12 | Long: ` 13 | Run the Revel web application named by the given import path. 14 | 15 | For example, to run the chat room sample application: 16 | 17 | revel run github.com/robfig/revel/samples/chat dev 18 | 19 | The run mode is used to select which set of app.conf configuration should 20 | apply and may be used to determine logic in the application itself. 21 | 22 | Run mode defaults to "dev". 23 | 24 | You can set a port as an optional third parameter. For example: 25 | 26 | revel run github.com/robfig/revel/samples/chat prod 8080`, 27 | } 28 | 29 | func init() { 30 | cmdRun.Run = runApp 31 | } 32 | 33 | func runApp(args []string) { 34 | if len(args) == 0 { 35 | errorf("No import path given.\nRun 'revel help run' for usage.\n") 36 | } 37 | 38 | // Determine the run mode. 39 | mode := "dev" 40 | if len(args) >= 2 { 41 | mode = args[1] 42 | } 43 | 44 | // Find and parse app.conf 45 | revel.Init(mode, args[0], "") 46 | revel.LoadMimeConfig() 47 | 48 | // Determine the override port, if any. 49 | port := revel.HttpPort 50 | if len(args) == 3 { 51 | var err error 52 | if port, err = strconv.Atoi(args[2]); err != nil { 53 | errorf("Failed to parse port as integer: %s", args[2]) 54 | } 55 | } 56 | 57 | revel.INFO.Printf("Running %s (%s) in %s mode\n", revel.AppName, revel.ImportPath, mode) 58 | revel.TRACE.Println("Base path:", revel.BasePath) 59 | 60 | // If the app is run in "watched" mode, use the harness to run it. 61 | if revel.Config.BoolDefault("watch", true) && revel.Config.BoolDefault("watch.code", true) { 62 | revel.HttpPort = port 63 | harness.NewHarness().Run() // Never returns. 64 | } 65 | 66 | // Else, just build and run the app. 67 | app, err := harness.Build() 68 | if err != nil { 69 | errorf("Failed to build app: %s", err) 70 | } 71 | app.Port = port 72 | app.Cmd().Run() 73 | } 74 | -------------------------------------------------------------------------------- /samples/booking/app/controllers/app.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "code.google.com/p/go.crypto/bcrypt" 5 | "github.com/robfig/revel" 6 | "github.com/robfig/revel/samples/booking/app/models" 7 | "github.com/robfig/revel/samples/booking/app/routes" 8 | ) 9 | 10 | type Application struct { 11 | GorpController 12 | } 13 | 14 | func (c Application) AddUser() revel.Result { 15 | if user := c.connected(); user != nil { 16 | c.RenderArgs["user"] = user 17 | } 18 | return nil 19 | } 20 | 21 | func (c Application) connected() *models.User { 22 | if c.RenderArgs["user"] != nil { 23 | return c.RenderArgs["user"].(*models.User) 24 | } 25 | if username, ok := c.Session["user"]; ok { 26 | return c.getUser(username) 27 | } 28 | return nil 29 | } 30 | 31 | func (c Application) getUser(username string) *models.User { 32 | users, err := c.Txn.Select(models.User{}, `select * from User where Username = ?`, username) 33 | if err != nil { 34 | panic(err) 35 | } 36 | if len(users) == 0 { 37 | return nil 38 | } 39 | return users[0].(*models.User) 40 | } 41 | 42 | func (c Application) Index() revel.Result { 43 | if c.connected() != nil { 44 | return c.Redirect(routes.Hotels.Index()) 45 | } 46 | c.Flash.Error("Please log in first") 47 | return c.Render() 48 | } 49 | 50 | func (c Application) Register() revel.Result { 51 | return c.Render() 52 | } 53 | 54 | func (c Application) SaveUser(user models.User, verifyPassword string) revel.Result { 55 | c.Validation.Required(verifyPassword) 56 | c.Validation.Required(verifyPassword == user.Password). 57 | Message("Password does not match") 58 | user.Validate(c.Validation) 59 | 60 | if c.Validation.HasErrors() { 61 | c.Validation.Keep() 62 | c.FlashParams() 63 | return c.Redirect(routes.Application.Register()) 64 | } 65 | 66 | user.HashedPassword, _ = bcrypt.GenerateFromPassword( 67 | []byte(user.Password), bcrypt.DefaultCost) 68 | err := c.Txn.Insert(&user) 69 | if err != nil { 70 | panic(err) 71 | } 72 | 73 | c.Session["user"] = user.Username 74 | c.Flash.Success("Welcome, " + user.Name) 75 | return c.Redirect(routes.Hotels.Index()) 76 | } 77 | 78 | func (c Application) Login(username, password string, remember bool) revel.Result { 79 | user := c.getUser(username) 80 | if user != nil { 81 | err := bcrypt.CompareHashAndPassword(user.HashedPassword, []byte(password)) 82 | if err == nil { 83 | c.Session["user"] = username 84 | if remember { 85 | c.Session.SetDefaultExpiration() 86 | } else { 87 | c.Session.SetNoExpiration() 88 | } 89 | c.Flash.Success("Welcome, " + username) 90 | return c.Redirect(routes.Hotels.Index()) 91 | } 92 | } 93 | 94 | c.Flash.Out["username"] = username 95 | c.Flash.Error("Login failed") 96 | return c.Redirect(routes.Application.Index()) 97 | } 98 | 99 | func (c Application) Logout() revel.Result { 100 | for k := range c.Session { 101 | delete(c.Session, k) 102 | } 103 | return c.Redirect(routes.Application.Index()) 104 | } 105 | -------------------------------------------------------------------------------- /samples/booking/app/controllers/gorp.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "code.google.com/p/go.crypto/bcrypt" 5 | "database/sql" 6 | "github.com/coopernurse/gorp" 7 | _ "github.com/mattn/go-sqlite3" 8 | r "github.com/robfig/revel" 9 | "github.com/robfig/revel/modules/db/app" 10 | "github.com/robfig/revel/samples/booking/app/models" 11 | ) 12 | 13 | var ( 14 | Dbm *gorp.DbMap 15 | ) 16 | 17 | func InitDB() { 18 | db.Init() 19 | Dbm = &gorp.DbMap{Db: db.Db, Dialect: gorp.SqliteDialect{}} 20 | 21 | setColumnSizes := func(t *gorp.TableMap, colSizes map[string]int) { 22 | for col, size := range colSizes { 23 | t.ColMap(col).MaxSize = size 24 | } 25 | } 26 | 27 | t := Dbm.AddTable(models.User{}).SetKeys(true, "UserId") 28 | t.ColMap("Password").Transient = true 29 | setColumnSizes(t, map[string]int{ 30 | "Username": 20, 31 | "Name": 100, 32 | }) 33 | 34 | t = Dbm.AddTable(models.Hotel{}).SetKeys(true, "HotelId") 35 | setColumnSizes(t, map[string]int{ 36 | "Name": 50, 37 | "Address": 100, 38 | "City": 40, 39 | "State": 6, 40 | "Zip": 6, 41 | "Country": 40, 42 | }) 43 | 44 | t = Dbm.AddTable(models.Booking{}).SetKeys(true, "BookingId") 45 | t.ColMap("User").Transient = true 46 | t.ColMap("Hotel").Transient = true 47 | t.ColMap("CheckInDate").Transient = true 48 | t.ColMap("CheckOutDate").Transient = true 49 | setColumnSizes(t, map[string]int{ 50 | "CardNumber": 16, 51 | "NameOnCard": 50, 52 | }) 53 | 54 | Dbm.TraceOn("[gorp]", r.INFO) 55 | Dbm.CreateTables() 56 | 57 | bcryptPassword, _ := bcrypt.GenerateFromPassword( 58 | []byte("demo"), bcrypt.DefaultCost) 59 | demoUser := &models.User{0, "Demo User", "demo", "demo", bcryptPassword} 60 | if err := Dbm.Insert(demoUser); err != nil { 61 | panic(err) 62 | } 63 | 64 | hotels := []*models.Hotel{ 65 | &models.Hotel{0, "Marriott Courtyard", "Tower Pl, Buckhead", "Atlanta", "GA", "30305", "USA", 120}, 66 | &models.Hotel{0, "W Hotel", "Union Square, Manhattan", "New York", "NY", "10011", "USA", 450}, 67 | &models.Hotel{0, "Hotel Rouge", "1315 16th St NW", "Washington", "DC", "20036", "USA", 250}, 68 | } 69 | for _, hotel := range hotels { 70 | if err := Dbm.Insert(hotel); err != nil { 71 | panic(err) 72 | } 73 | } 74 | } 75 | 76 | type GorpController struct { 77 | *r.Controller 78 | Txn *gorp.Transaction 79 | } 80 | 81 | func (c *GorpController) Begin() r.Result { 82 | txn, err := Dbm.Begin() 83 | if err != nil { 84 | panic(err) 85 | } 86 | c.Txn = txn 87 | return nil 88 | } 89 | 90 | func (c *GorpController) Commit() r.Result { 91 | if c.Txn == nil { 92 | return nil 93 | } 94 | if err := c.Txn.Commit(); err != nil && err != sql.ErrTxDone { 95 | panic(err) 96 | } 97 | c.Txn = nil 98 | return nil 99 | } 100 | 101 | func (c *GorpController) Rollback() r.Result { 102 | if c.Txn == nil { 103 | return nil 104 | } 105 | if err := c.Txn.Rollback(); err != nil && err != sql.ErrTxDone { 106 | panic(err) 107 | } 108 | c.Txn = nil 109 | return nil 110 | } 111 | -------------------------------------------------------------------------------- /samples/booking/app/controllers/init.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import "github.com/robfig/revel" 4 | 5 | func init() { 6 | revel.OnAppStart(InitDB) 7 | revel.InterceptMethod((*GorpController).Begin, revel.BEFORE) 8 | revel.InterceptMethod(Application.AddUser, revel.BEFORE) 9 | revel.InterceptMethod(Hotels.checkUser, revel.BEFORE) 10 | revel.InterceptMethod((*GorpController).Commit, revel.AFTER) 11 | revel.InterceptMethod((*GorpController).Rollback, revel.FINALLY) 12 | } 13 | -------------------------------------------------------------------------------- /samples/booking/app/init.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import "github.com/robfig/revel" 4 | 5 | func init() { 6 | // Filters is the default set of global filters. 7 | revel.Filters = []revel.Filter{ 8 | revel.PanicFilter, // Recover from panics and display an error page instead. 9 | revel.RouterFilter, // Use the routing table to select the right Action 10 | revel.FilterConfiguringFilter, // A hook for adding or removing per-Action filters. 11 | revel.ParamsFilter, // Parse parameters into Controller.Params. 12 | revel.SessionFilter, // Restore and write the session cookie. 13 | revel.FlashFilter, // Restore and write the flash cookie. 14 | revel.ValidationFilter, // Restore kept validation errors and save new ones from cookie. 15 | revel.I18nFilter, // Resolve the requested language 16 | HeaderFilter, // Add some security based headers 17 | revel.InterceptorFilter, // Run interceptors around the action. 18 | revel.CompressFilter, // Compress the result. 19 | revel.ActionInvoker, // Invoke the action. 20 | } 21 | } 22 | 23 | var HeaderFilter = func(c *revel.Controller, fc []revel.Filter) { 24 | // Add some common security headers 25 | c.Response.Out.Header().Add("X-Frame-Options", "SAMEORIGIN") 26 | c.Response.Out.Header().Add("X-XSS-Protection", "1; mode=block") 27 | c.Response.Out.Header().Add("X-Content-Type-Options", "nosniff") 28 | 29 | fc[0](c, fc[1:]) // Execute the next filter stage. 30 | } 31 | -------------------------------------------------------------------------------- /samples/booking/app/jobs/count.go: -------------------------------------------------------------------------------- 1 | package jobs 2 | 3 | import ( 4 | "fmt" 5 | "github.com/robfig/revel" 6 | "github.com/robfig/revel/modules/jobs/app/jobs" 7 | "github.com/robfig/revel/samples/booking/app/controllers" 8 | "github.com/robfig/revel/samples/booking/app/models" 9 | ) 10 | 11 | // Periodically count the bookings in the database. 12 | type BookingCounter struct{} 13 | 14 | func (c BookingCounter) Run() { 15 | bookings, err := controllers.Dbm.Select(models.Booking{}, 16 | `select * from Booking`) 17 | if err != nil { 18 | panic(err) 19 | } 20 | fmt.Printf("There are %d bookings.\n", len(bookings)) 21 | } 22 | 23 | func init() { 24 | revel.OnAppStart(func() { 25 | jobs.Schedule("@every 1m", BookingCounter{}) 26 | }) 27 | } 28 | -------------------------------------------------------------------------------- /samples/booking/app/models/booking.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "fmt" 5 | "github.com/coopernurse/gorp" 6 | "github.com/robfig/revel" 7 | "regexp" 8 | "time" 9 | ) 10 | 11 | type Booking struct { 12 | BookingId int 13 | UserId int 14 | HotelId int 15 | CheckInStr string 16 | CheckOutStr string 17 | CardNumber string 18 | NameOnCard string 19 | CardExpMonth int 20 | CardExpYear int 21 | Smoking bool 22 | Beds int 23 | 24 | // Transient 25 | CheckInDate time.Time 26 | CheckOutDate time.Time 27 | User *User 28 | Hotel *Hotel 29 | } 30 | 31 | // TODO: Make an interface for Validate() and then validation can pass in the 32 | // key prefix ("booking.") 33 | func (booking Booking) Validate(v *revel.Validation) { 34 | v.Required(booking.User) 35 | v.Required(booking.Hotel) 36 | v.Required(booking.CheckInDate) 37 | v.Required(booking.CheckOutDate) 38 | 39 | v.Match(booking.CardNumber, regexp.MustCompile(`\d{16}`)). 40 | Message("Credit card number must be numeric and 16 digits") 41 | 42 | v.Check(booking.NameOnCard, 43 | revel.Required{}, 44 | revel.MinSize{3}, 45 | revel.MaxSize{70}, 46 | ) 47 | } 48 | 49 | func (b Booking) Total() int { 50 | return b.Hotel.Price * b.Nights() 51 | } 52 | 53 | func (b Booking) Nights() int { 54 | return int((b.CheckOutDate.Unix() - b.CheckInDate.Unix()) / 60 / 60 / 24) 55 | } 56 | 57 | const ( 58 | DATE_FORMAT = "Jan _2, 2006" 59 | SQL_DATE_FORMAT = "2006-01-02" 60 | ) 61 | 62 | func (b Booking) Description() string { 63 | if b.Hotel == nil { 64 | return "" 65 | } 66 | 67 | return fmt.Sprintf("%s, %s to %s", 68 | b.Hotel.Name, 69 | b.CheckInDate.Format(DATE_FORMAT), 70 | b.CheckOutDate.Format(DATE_FORMAT)) 71 | } 72 | 73 | func (b Booking) String() string { 74 | return fmt.Sprintf("Booking(%s,%s)", b.User, b.Hotel) 75 | } 76 | 77 | // These hooks work around two things: 78 | // - Gorp's lack of support for loading relations automatically. 79 | // - Sqlite's lack of support for datetimes. 80 | 81 | func (b *Booking) PreInsert(_ gorp.SqlExecutor) error { 82 | b.UserId = b.User.UserId 83 | b.HotelId = b.Hotel.HotelId 84 | b.CheckInStr = b.CheckInDate.Format(SQL_DATE_FORMAT) 85 | b.CheckOutStr = b.CheckOutDate.Format(SQL_DATE_FORMAT) 86 | return nil 87 | } 88 | 89 | func (b *Booking) PostGet(exe gorp.SqlExecutor) error { 90 | var ( 91 | obj interface{} 92 | err error 93 | ) 94 | 95 | obj, err = exe.Get(User{}, b.UserId) 96 | if err != nil { 97 | return fmt.Errorf("Error loading a booking's user (%d): %s", b.UserId, err) 98 | } 99 | b.User = obj.(*User) 100 | 101 | obj, err = exe.Get(Hotel{}, b.HotelId) 102 | if err != nil { 103 | return fmt.Errorf("Error loading a booking's hotel (%d): %s", b.HotelId, err) 104 | } 105 | b.Hotel = obj.(*Hotel) 106 | 107 | if b.CheckInDate, err = time.Parse(SQL_DATE_FORMAT, b.CheckInStr); err != nil { 108 | return fmt.Errorf("Error parsing check in date '%s':", b.CheckInStr, err) 109 | } 110 | if b.CheckOutDate, err = time.Parse(SQL_DATE_FORMAT, b.CheckOutStr); err != nil { 111 | return fmt.Errorf("Error parsing check out date '%s':", b.CheckOutStr, err) 112 | } 113 | return nil 114 | } 115 | -------------------------------------------------------------------------------- /samples/booking/app/models/hotel.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "github.com/robfig/revel" 5 | ) 6 | 7 | type Hotel struct { 8 | HotelId int 9 | Name, Address string 10 | City, State, Zip string 11 | Country string 12 | Price int 13 | } 14 | 15 | func (hotel *Hotel) Validate(v *revel.Validation) { 16 | v.Check(hotel.Name, 17 | revel.Required{}, 18 | revel.MaxSize{50}, 19 | ) 20 | 21 | v.MaxSize(hotel.Address, 100) 22 | 23 | v.Check(hotel.City, 24 | revel.Required{}, 25 | revel.MaxSize{40}, 26 | ) 27 | 28 | v.Check(hotel.State, 29 | revel.Required{}, 30 | revel.MaxSize{6}, 31 | revel.MinSize{2}, 32 | ) 33 | 34 | v.Check(hotel.Zip, 35 | revel.Required{}, 36 | revel.MaxSize{6}, 37 | revel.MinSize{5}, 38 | ) 39 | 40 | v.Check(hotel.Country, 41 | revel.Required{}, 42 | revel.MaxSize{40}, 43 | revel.MinSize{2}, 44 | ) 45 | } 46 | -------------------------------------------------------------------------------- /samples/booking/app/models/user.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "fmt" 5 | "github.com/robfig/revel" 6 | "regexp" 7 | ) 8 | 9 | type User struct { 10 | UserId int 11 | Name string 12 | Username, Password string 13 | HashedPassword []byte 14 | } 15 | 16 | func (u *User) String() string { 17 | return fmt.Sprintf("User(%s)", u.Username) 18 | } 19 | 20 | var userRegex = regexp.MustCompile("^\\w*$") 21 | 22 | func (user *User) Validate(v *revel.Validation) { 23 | v.Check(user.Username, 24 | revel.Required{}, 25 | revel.MaxSize{15}, 26 | revel.MinSize{4}, 27 | revel.Match{userRegex}, 28 | ) 29 | 30 | ValidatePassword(v, user.Password). 31 | Key("user.Password") 32 | 33 | v.Check(user.Name, 34 | revel.Required{}, 35 | revel.MaxSize{100}, 36 | ) 37 | } 38 | 39 | func ValidatePassword(v *revel.Validation, password string) *revel.ValidationResult { 40 | return v.Check(password, 41 | revel.Required{}, 42 | revel.MaxSize{15}, 43 | revel.MinSize{5}, 44 | ) 45 | } 46 | -------------------------------------------------------------------------------- /samples/booking/app/views/application/index.html: -------------------------------------------------------------------------------- 1 | {{set . "title" "Home"}} 2 | {{template "header.html" .}} 3 | 4 |
5 | 6 | (try with demo/demo) 7 | 8 |
9 |

10 | 11 | 12 |

13 |

14 | 15 | 16 |

17 |

18 | 19 | 20 |

21 |

22 | 23 |

24 |
25 | 26 |

27 | Register New User 28 |

29 | 30 |
31 | 32 | {{template "footer.html" .}} 33 | -------------------------------------------------------------------------------- /samples/booking/app/views/application/register.html: -------------------------------------------------------------------------------- 1 | {{set . "title" "Register"}} 2 | {{template "header.html" .}} 3 | 4 |

Register:

5 | 6 |
7 | {{with $field := field "user.Username" .}} 8 |

9 | Username: 10 | * 11 | {{$field.Error}} 12 |

13 | {{end}} 14 | {{with $field := field "user.Name" .}} 15 |

16 | Real name: * 17 | {{$field.Error}} 18 |

19 | {{end}} 20 | {{with $field := field "user.Password" .}} 21 |

22 | Password: * 23 | {{$field.Error}} 24 |

25 | {{end}} 26 | {{with $field := field "verifyPassword" .}} 27 |

28 | Verify password: * 29 | {{$field.Error}} 30 |

31 | {{end}} 32 |

33 | Cancel 34 |

35 |
36 | 37 | {{template "footer.html" .}} 38 | -------------------------------------------------------------------------------- /samples/booking/app/views/footer.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /samples/booking/app/views/header.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{.title}} 6 | 7 | 8 | {{range .moreStyles}} 9 | 10 | {{end}} 11 | 12 | 13 | {{range .moreScripts}} 14 | 15 | {{end}} 16 | 17 | 18 | 19 | 33 | 34 |
35 | {{if .flash.error}} 36 |

37 | {{.flash.error}} 38 |

39 | {{end}} 40 | {{if .flash.success}} 41 |

42 | {{.flash.success}} 43 |

44 | {{end}} 45 | 46 | -------------------------------------------------------------------------------- /samples/booking/app/views/hotels/confirmbooking.html: -------------------------------------------------------------------------------- 1 | {{template "header.html" .}} 2 | 3 |

Confirm hotel booking

4 | 5 |
6 |

7 | Name: {{.hotel.Name}} 8 |

9 |

10 | Address: {{.hotel.Address}} 11 |

12 |

13 | City: {{.hotel.City}} 14 |

15 |

16 | State: {{.hotel.State}} 17 |

18 |

19 | Zip: {{.hotel.Zip}} 20 |

21 |

22 | Country: {{.hotel.Country}} 23 |

24 |

25 | Nightly rate: {{.hotel.Price}} 26 |

27 |

28 | Beds: {{.booking.Beds}} 29 | 30 |

31 |

32 | Total: {{.booking.Total}} {{/* .formatCurrency('USD') */}} 33 |

34 |

35 | Check in date: {{.booking.CheckInDate.Format "2006-01-02"}} 36 | 37 |

38 |

39 | Check out date: {{.booking.CheckOutDate.Format "2006-01-02"}} 40 | 41 |

42 |

43 | Credit card #: {{.booking.CardNumber}} 44 | 45 | 46 | 47 | 48 | 49 |

50 | 51 |

52 | 53 | 54 | Cancel 55 |

56 |
57 | 58 | {{template "footer.html" .}} 59 | -------------------------------------------------------------------------------- /samples/booking/app/views/hotels/index.html: -------------------------------------------------------------------------------- 1 | {{set . "title" "Search"}} 2 | {{template "header.html" .}} 3 | 4 |

Search Hotels

5 | 6 |

7 | 8 | 9 | 10 |
11 | 12 | Maximum results: 13 | 18 |

19 | 20 |
21 |
22 | 23 | 71 | 72 |

Current Hotel Bookings

73 | 74 | {{if not .bookings}} 75 |

76 | No Bookings Found 77 |

78 | {{else}} 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | {{range .bookings}} 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 105 | 106 | {{end}} 107 | 108 |
NameAddressCity, StateCheck inCheck outConfirmation numberAction
{{.Hotel.Name}}{{.Hotel.Address}}{{.Hotel.City}}, {{.Hotel.State}}, {{.Hotel.Country}}{{.CheckInDate.Format "2006-01-02"}}{{.CheckOutDate.Format "2006-01-02"}}{{.BookingId}} 101 |
102 | Cancel 103 |
104 |
109 | {{end}} 110 | 111 | {{template "footer.html" .}} 112 | -------------------------------------------------------------------------------- /samples/booking/app/views/hotels/list.html: -------------------------------------------------------------------------------- 1 | {{if .hotels}} 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | {{range .hotels}} 14 | 15 | 16 | 17 | 18 | 19 | 22 | 23 | {{end}} 24 | 25 |
NameAddressCity, StateZipAction
{{.Name}}{{.Address}}{{.City}}, {{.State}}, {{.Country}}{{.Zip}} 20 | View Hotel 21 |
26 |

27 | More results 28 |

29 | 30 | {{else}} 31 |

32 | No more results 33 |

34 | {{end}} 35 | -------------------------------------------------------------------------------- /samples/booking/app/views/hotels/settings.html: -------------------------------------------------------------------------------- 1 | {{set . "title" "Settings"}} 2 | {{template "header.html" .}} 3 | 4 |

Change your password

5 | 6 |
7 | {{with $field := field "password" .}} 8 |

9 | Password: 10 | * 11 | {{$field.Error}} 12 |

13 | {{end}} 14 | {{with $field := field "verifyPassword" .}} 15 |

16 | Verify password: 17 | * 18 | {{$field.Error}} 19 |

20 | {{end}} 21 | 22 |

23 | Cancel 24 |

25 |
26 | 27 | {{template "footer.html" .}} 28 | -------------------------------------------------------------------------------- /samples/booking/app/views/hotels/show.html: -------------------------------------------------------------------------------- 1 | {{template "header.html" .}} 2 | 3 |

View hotel

4 | 5 | {{with .hotel}} 6 |
7 | 8 |

9 | Name: {{.Name}} 10 |

11 |

12 | Address: {{.Address}} 13 |

14 |

15 | City: {{.City}} 16 |

17 |

18 | State: {{.State}} 19 |

20 |

21 | Zip: {{.Zip}} 22 |

23 |

24 | Country: {{.Country}} 25 |

26 |

27 | Nightly rate: {{.Price}} 28 |

29 | 30 |

31 | 32 | Back to search 33 |

34 |
35 | {{end}} 36 | 37 | {{template "footer.html" .}} 38 | -------------------------------------------------------------------------------- /samples/booking/conf/app.conf: -------------------------------------------------------------------------------- 1 | # Application 2 | app.name=Booking example 3 | app.secret=secret 4 | 5 | # Server 6 | http.addr= 7 | http.port=9000 8 | http.ssl=false 9 | http.sslcert= 10 | http.sslkey= 11 | 12 | # Logging 13 | log.trace.output = stderr 14 | log.info.output = stderr 15 | log.warn.output = stderr 16 | log.error.output = stderr 17 | 18 | log.trace.prefix = "TRACE " 19 | log.info.prefix = "INFO " 20 | log.warn.prefix = "WARN " 21 | log.error.prefix = "ERROR " 22 | 23 | db.import = github.com/mattn/go-sqlite3 24 | db.driver = sqlite3 25 | db.spec = :memory: 26 | 27 | build.tags=gorp 28 | 29 | module.jobs=github.com/robfig/revel/modules/jobs 30 | module.static=github.com/robfig/revel/modules/static 31 | 32 | [dev] 33 | mode.dev=true 34 | watch=true 35 | module.testrunner=github.com/robfig/revel/modules/testrunner 36 | 37 | [prod] 38 | watch=false 39 | module.testrunner= 40 | 41 | log.trace.output = off 42 | log.info.output = off 43 | log.warn.output = stderr 44 | log.error.output = stderr 45 | -------------------------------------------------------------------------------- /samples/booking/conf/routes: -------------------------------------------------------------------------------- 1 | # Routes 2 | # This file defines all application routes (Higher priority routes first) 3 | # ~~~~ 4 | 5 | module:testrunner 6 | module:jobs 7 | 8 | GET / Application.Index 9 | GET /hotels Hotels.Index 10 | GET /hotels/list Hotels.List 11 | GET /hotels/:id Hotels.Show 12 | GET /hotels/:id/booking Hotels.Book 13 | POST /hotels/:id/booking Hotels.ConfirmBooking 14 | POST /bookings/:id/cancel Hotels.CancelBooking 15 | GET /register Application.Register 16 | POST /register Application.SaveUser 17 | GET /settings Hotels.Settings 18 | POST /settings Hotels.SaveSettings 19 | POST /login Application.Login 20 | GET /logout Application.Logout 21 | 22 | # Map static resources from the /app/public folder to the /public path 23 | GET /public/*filepath Static.Serve("public") 24 | GET /favicon.ico Static.Serve("public/img","favicon.png") 25 | 26 | # Catch all 27 | * /:controller/:action :controller.:action 28 | -------------------------------------------------------------------------------- /samples/booking/public/img/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robfig/revel/e8aac16df9e93b17d309595b4ff57405dc567fdf/samples/booking/public/img/favicon.png -------------------------------------------------------------------------------- /samples/booking/public/img/hotel.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robfig/revel/e8aac16df9e93b17d309595b4ff57405dc567fdf/samples/booking/public/img/hotel.jpg -------------------------------------------------------------------------------- /samples/booking/public/ui-lightness/images/ui-bg_diagonals-thick_18_b81900_40x40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robfig/revel/e8aac16df9e93b17d309595b4ff57405dc567fdf/samples/booking/public/ui-lightness/images/ui-bg_diagonals-thick_18_b81900_40x40.png -------------------------------------------------------------------------------- /samples/booking/public/ui-lightness/images/ui-bg_diagonals-thick_20_666666_40x40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robfig/revel/e8aac16df9e93b17d309595b4ff57405dc567fdf/samples/booking/public/ui-lightness/images/ui-bg_diagonals-thick_20_666666_40x40.png -------------------------------------------------------------------------------- /samples/booking/public/ui-lightness/images/ui-bg_flat_10_000000_40x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robfig/revel/e8aac16df9e93b17d309595b4ff57405dc567fdf/samples/booking/public/ui-lightness/images/ui-bg_flat_10_000000_40x100.png -------------------------------------------------------------------------------- /samples/booking/public/ui-lightness/images/ui-bg_glass_100_f6f6f6_1x400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robfig/revel/e8aac16df9e93b17d309595b4ff57405dc567fdf/samples/booking/public/ui-lightness/images/ui-bg_glass_100_f6f6f6_1x400.png -------------------------------------------------------------------------------- /samples/booking/public/ui-lightness/images/ui-bg_glass_100_fdf5ce_1x400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robfig/revel/e8aac16df9e93b17d309595b4ff57405dc567fdf/samples/booking/public/ui-lightness/images/ui-bg_glass_100_fdf5ce_1x400.png -------------------------------------------------------------------------------- /samples/booking/public/ui-lightness/images/ui-bg_glass_65_ffffff_1x400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robfig/revel/e8aac16df9e93b17d309595b4ff57405dc567fdf/samples/booking/public/ui-lightness/images/ui-bg_glass_65_ffffff_1x400.png -------------------------------------------------------------------------------- /samples/booking/public/ui-lightness/images/ui-bg_gloss-wave_35_f6a828_500x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robfig/revel/e8aac16df9e93b17d309595b4ff57405dc567fdf/samples/booking/public/ui-lightness/images/ui-bg_gloss-wave_35_f6a828_500x100.png -------------------------------------------------------------------------------- /samples/booking/public/ui-lightness/images/ui-bg_highlight-soft_100_eeeeee_1x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robfig/revel/e8aac16df9e93b17d309595b4ff57405dc567fdf/samples/booking/public/ui-lightness/images/ui-bg_highlight-soft_100_eeeeee_1x100.png -------------------------------------------------------------------------------- /samples/booking/public/ui-lightness/images/ui-bg_highlight-soft_75_ffe45c_1x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robfig/revel/e8aac16df9e93b17d309595b4ff57405dc567fdf/samples/booking/public/ui-lightness/images/ui-bg_highlight-soft_75_ffe45c_1x100.png -------------------------------------------------------------------------------- /samples/booking/public/ui-lightness/images/ui-icons_222222_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robfig/revel/e8aac16df9e93b17d309595b4ff57405dc567fdf/samples/booking/public/ui-lightness/images/ui-icons_222222_256x240.png -------------------------------------------------------------------------------- /samples/booking/public/ui-lightness/images/ui-icons_228ef1_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robfig/revel/e8aac16df9e93b17d309595b4ff57405dc567fdf/samples/booking/public/ui-lightness/images/ui-icons_228ef1_256x240.png -------------------------------------------------------------------------------- /samples/booking/public/ui-lightness/images/ui-icons_ef8c08_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robfig/revel/e8aac16df9e93b17d309595b4ff57405dc567fdf/samples/booking/public/ui-lightness/images/ui-icons_ef8c08_256x240.png -------------------------------------------------------------------------------- /samples/booking/public/ui-lightness/images/ui-icons_ffd27a_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robfig/revel/e8aac16df9e93b17d309595b4ff57405dc567fdf/samples/booking/public/ui-lightness/images/ui-icons_ffd27a_256x240.png -------------------------------------------------------------------------------- /samples/booking/public/ui-lightness/images/ui-icons_ffffff_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robfig/revel/e8aac16df9e93b17d309595b4ff57405dc567fdf/samples/booking/public/ui-lightness/images/ui-icons_ffffff_256x240.png -------------------------------------------------------------------------------- /samples/booking/tests/apptest.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import "github.com/robfig/revel" 4 | 5 | type ApplicationTest struct { 6 | revel.TestSuite 7 | } 8 | 9 | func (t *ApplicationTest) Before() { 10 | println("Set up") 11 | } 12 | 13 | func (t *ApplicationTest) TestThatIndexPageWorks() { 14 | t.Get("/") 15 | t.AssertOk() 16 | t.AssertContentType("text/html; charset=utf-8") 17 | } 18 | 19 | func (t *ApplicationTest) After() { 20 | println("Tear down") 21 | } 22 | -------------------------------------------------------------------------------- /samples/chat/app/chatroom/chatroom.go: -------------------------------------------------------------------------------- 1 | package chatroom 2 | 3 | import ( 4 | "container/list" 5 | "time" 6 | ) 7 | 8 | type Event struct { 9 | Type string // "join", "leave", or "message" 10 | User string 11 | Timestamp int // Unix timestamp (secs) 12 | Text string // What the user said (if Type == "message") 13 | } 14 | 15 | type Subscription struct { 16 | Archive []Event // All the events from the archive. 17 | New <-chan Event // New events coming in. 18 | } 19 | 20 | // Owner of a subscription must cancel it when they stop listening to events. 21 | func (s Subscription) Cancel() { 22 | unsubscribe <- s.New // Unsubscribe the channel. 23 | drain(s.New) // Drain it, just in case there was a pending publish. 24 | } 25 | 26 | func newEvent(typ, user, msg string) Event { 27 | return Event{typ, user, int(time.Now().Unix()), msg} 28 | } 29 | 30 | func Subscribe() Subscription { 31 | resp := make(chan Subscription) 32 | subscribe <- resp 33 | return <-resp 34 | } 35 | 36 | func Join(user string) { 37 | publish <- newEvent("join", user, "") 38 | } 39 | 40 | func Say(user, message string) { 41 | publish <- newEvent("message", user, message) 42 | } 43 | 44 | func Leave(user string) { 45 | publish <- newEvent("leave", user, "") 46 | } 47 | 48 | const archiveSize = 10 49 | 50 | var ( 51 | // Send a channel here to get room events back. It will send the entire 52 | // archive initially, and then new messages as they come in. 53 | subscribe = make(chan (chan<- Subscription), 10) 54 | // Send a channel here to unsubscribe. 55 | unsubscribe = make(chan (<-chan Event), 10) 56 | // Send events here to publish them. 57 | publish = make(chan Event, 10) 58 | ) 59 | 60 | // This function loops forever, handling the chat room pubsub 61 | func chatroom() { 62 | archive := list.New() 63 | subscribers := list.New() 64 | 65 | for { 66 | select { 67 | case ch := <-subscribe: 68 | var events []Event 69 | for e := archive.Front(); e != nil; e = e.Next() { 70 | events = append(events, e.Value.(Event)) 71 | } 72 | subscriber := make(chan Event, 10) 73 | subscribers.PushBack(subscriber) 74 | ch <- Subscription{events, subscriber} 75 | 76 | case event := <-publish: 77 | for ch := subscribers.Front(); ch != nil; ch = ch.Next() { 78 | ch.Value.(chan Event) <- event 79 | } 80 | if archive.Len() >= archiveSize { 81 | archive.Remove(archive.Front()) 82 | } 83 | archive.PushBack(event) 84 | 85 | case unsub := <-unsubscribe: 86 | for ch := subscribers.Front(); ch != nil; ch = ch.Next() { 87 | if ch.Value.(chan Event) == unsub { 88 | subscribers.Remove(ch) 89 | break 90 | } 91 | } 92 | } 93 | } 94 | } 95 | 96 | func init() { 97 | go chatroom() 98 | } 99 | 100 | // Helpers 101 | 102 | // Drains a given channel of any messages. 103 | func drain(ch <-chan Event) { 104 | for { 105 | select { 106 | case _, ok := <-ch: 107 | if !ok { 108 | return 109 | } 110 | default: 111 | return 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /samples/chat/app/controllers/app.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "github.com/robfig/revel" 5 | ) 6 | 7 | type Application struct { 8 | *revel.Controller 9 | } 10 | 11 | func (c Application) Index() revel.Result { 12 | return c.Render() 13 | } 14 | 15 | func (c Application) EnterDemo(user, demo string) revel.Result { 16 | c.Validation.Required(user) 17 | c.Validation.Required(demo) 18 | 19 | if c.Validation.HasErrors() { 20 | c.Flash.Error("Please choose a nick name and the demonstration type.") 21 | return c.Redirect(Application.Index) 22 | } 23 | 24 | switch demo { 25 | case "refresh": 26 | return c.Redirect("/refresh?user=%s", user) 27 | case "longpolling": 28 | return c.Redirect("/longpolling/room?user=%s", user) 29 | case "websocket": 30 | return c.Redirect("/websocket/room?user=%s", user) 31 | } 32 | return nil 33 | } 34 | -------------------------------------------------------------------------------- /samples/chat/app/controllers/longpolling.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "github.com/robfig/revel" 5 | "github.com/robfig/revel/samples/chat/app/chatroom" 6 | ) 7 | 8 | type LongPolling struct { 9 | *revel.Controller 10 | } 11 | 12 | func (c LongPolling) Room(user string) revel.Result { 13 | chatroom.Join(user) 14 | return c.Render(user) 15 | } 16 | 17 | func (c LongPolling) Say(user, message string) revel.Result { 18 | chatroom.Say(user, message) 19 | return nil 20 | } 21 | 22 | func (c LongPolling) WaitMessages(lastReceived int) revel.Result { 23 | subscription := chatroom.Subscribe() 24 | defer subscription.Cancel() 25 | 26 | // See if anything is new in the archive. 27 | var events []chatroom.Event 28 | for _, event := range subscription.Archive { 29 | if event.Timestamp > lastReceived { 30 | events = append(events, event) 31 | } 32 | } 33 | 34 | // If we found one, grand. 35 | if len(events) > 0 { 36 | return c.RenderJson(events) 37 | } 38 | 39 | // Else, wait for something new. 40 | event := <-subscription.New 41 | return c.RenderJson([]chatroom.Event{event}) 42 | } 43 | 44 | func (c LongPolling) Leave(user string) revel.Result { 45 | chatroom.Leave(user) 46 | return c.Redirect(Application.Index) 47 | } 48 | -------------------------------------------------------------------------------- /samples/chat/app/controllers/refresh.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "github.com/robfig/revel" 5 | "github.com/robfig/revel/samples/chat/app/chatroom" 6 | "github.com/robfig/revel/samples/chat/app/routes" 7 | ) 8 | 9 | type Refresh struct { 10 | *revel.Controller 11 | } 12 | 13 | func (c Refresh) Index(user string) revel.Result { 14 | chatroom.Join(user) 15 | return c.Redirect(routes.Refresh.Room(user)) 16 | } 17 | 18 | func (c Refresh) Room(user string) revel.Result { 19 | subscription := chatroom.Subscribe() 20 | defer subscription.Cancel() 21 | events := subscription.Archive 22 | for i, _ := range events { 23 | if events[i].User == user { 24 | events[i].User = "you" 25 | } 26 | } 27 | return c.Render(user, events) 28 | } 29 | 30 | func (c Refresh) Say(user, message string) revel.Result { 31 | chatroom.Say(user, message) 32 | return c.Redirect(routes.Refresh.Room(user)) 33 | } 34 | 35 | func (c Refresh) Leave(user string) revel.Result { 36 | chatroom.Leave(user) 37 | return c.Redirect(Application.Index) 38 | } 39 | -------------------------------------------------------------------------------- /samples/chat/app/controllers/websocket.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "code.google.com/p/go.net/websocket" 5 | "github.com/robfig/revel" 6 | "github.com/robfig/revel/samples/chat/app/chatroom" 7 | ) 8 | 9 | type WebSocket struct { 10 | *revel.Controller 11 | } 12 | 13 | func (c WebSocket) Room(user string) revel.Result { 14 | return c.Render(user) 15 | } 16 | 17 | func (c WebSocket) RoomSocket(user string, ws *websocket.Conn) revel.Result { 18 | // Join the room. 19 | subscription := chatroom.Subscribe() 20 | defer subscription.Cancel() 21 | 22 | chatroom.Join(user) 23 | defer chatroom.Leave(user) 24 | 25 | // Send down the archive. 26 | for _, event := range subscription.Archive { 27 | if websocket.JSON.Send(ws, &event) != nil { 28 | // They disconnected 29 | return nil 30 | } 31 | } 32 | 33 | // In order to select between websocket messages and subscription events, we 34 | // need to stuff websocket events into a channel. 35 | newMessages := make(chan string) 36 | go func() { 37 | var msg string 38 | for { 39 | err := websocket.Message.Receive(ws, &msg) 40 | if err != nil { 41 | close(newMessages) 42 | return 43 | } 44 | newMessages <- msg 45 | } 46 | }() 47 | 48 | // Now listen for new events from either the websocket or the chatroom. 49 | for { 50 | select { 51 | case event := <-subscription.New: 52 | if websocket.JSON.Send(ws, &event) != nil { 53 | // They disconnected. 54 | return nil 55 | } 56 | case msg, ok := <-newMessages: 57 | // If the channel is closed, they disconnected. 58 | if !ok { 59 | return nil 60 | } 61 | 62 | // Otherwise, say something. 63 | chatroom.Say(user, msg) 64 | } 65 | } 66 | return nil 67 | } 68 | -------------------------------------------------------------------------------- /samples/chat/app/views/Application/Index.html: -------------------------------------------------------------------------------- 1 | {{set . "title" "Sign in"}} 2 | {{template "header.html" .}} 3 | 4 |
5 | 6 |

The Chat demonstration (from Play!)

7 | 8 |
9 |
10 | {{if .flash.error}} 11 |

12 | {{.flash.error}} 13 |

14 | {{end}} 15 |

16 | 17 | 18 |

19 |

20 | 21 | 27 |

28 |

29 | 30 | 31 |

32 |
33 |
34 | 35 |
36 | {{template "footer.html" .}} 37 | -------------------------------------------------------------------------------- /samples/chat/app/views/LongPolling/Room.html: -------------------------------------------------------------------------------- 1 | {{set . "title" "Chat room"}} 2 | {{template "header.html" .}} 3 | 4 |

Ajax, long polling — You are now chatting as {{.user}} 5 | Leave the chat room

6 | 7 |
8 | 34 |
35 | 36 |
37 | 38 | 39 |
40 | 41 | 83 | {{template "footer.html" .}} 84 | -------------------------------------------------------------------------------- /samples/chat/app/views/Refresh/Room.html: -------------------------------------------------------------------------------- 1 | {{set . "title" "Basic Chat room"}} 2 | {{template "header.html" .}} 3 | 4 |

Ajax, using active refresh — You are now chatting as {{.user}} 5 | Leave the chat room

6 | 7 |
8 | {{range .events}} 9 | {{if eq .Type "message"}} 10 |
11 |

{{.User}}

12 |

13 | {{.Text}} 14 |

15 |
16 | {{end}} 17 | {{if eq .Type "join"}} 18 |
19 |

20 |

21 | {{.User}} joined the room 22 |

23 |
24 | {{end}} 25 | {{if eq .Type "leave"}} 26 |
27 |

28 |

29 | {{.User}} left the room 30 |

31 |
32 | {{end}} 33 | {{end}} 34 |
35 | 36 |
37 |
38 | 39 | 40 |
41 |
42 | 43 | 63 | {{template "footer.html" .}} 64 | -------------------------------------------------------------------------------- /samples/chat/app/views/WebSocket/Room.html: -------------------------------------------------------------------------------- 1 | {{set . "title" "Chat room"}} 2 | {{template "header.html" .}} 3 | 4 |

WebSocket — You are now chatting as {{.user}} 5 | Leave the chat room

6 | 7 |
8 | 42 |
43 | 44 |
45 | 46 | 47 |
48 | 49 | 79 | -------------------------------------------------------------------------------- /samples/chat/app/views/footer.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /samples/chat/app/views/header.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{.title}} 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /samples/chat/conf/app.conf: -------------------------------------------------------------------------------- 1 | app.name=chat 2 | app.secret=pJLzyoiDe17L36mytqC912j81PfTiolHm1veQK6Grn1En3YFdB5lvEHVTwFEaWvj 3 | http.addr= 4 | http.port=9000 5 | 6 | module.static=github.com/robfig/revel/modules/static 7 | 8 | [dev] 9 | mode.dev=true 10 | results.pretty=true 11 | watch=true 12 | 13 | log.trace.output = off 14 | log.info.output = stderr 15 | log.warn.output = stderr 16 | log.error.output = stderr 17 | 18 | module.testrunner=github.com/robfig/revel/modules/testrunner 19 | 20 | [prod] 21 | mode.dev=false 22 | results.pretty=false 23 | watch=false 24 | 25 | log.trace.output = off 26 | log.info.output = off 27 | log.warn.output = %(app.name)s.log 28 | log.error.output = %(app.name)s.log 29 | -------------------------------------------------------------------------------- /samples/chat/conf/routes: -------------------------------------------------------------------------------- 1 | # Routes 2 | # This file defines all application routes (Higher priority routes first) 3 | # ~~~~ 4 | 5 | module:testrunner 6 | 7 | # Login 8 | GET / Application.Index 9 | GET /demo Application.EnterDemo 10 | 11 | # Refresh demo 12 | GET /refresh Refresh.Index 13 | GET /refresh/room Refresh.Room 14 | POST /refresh/room Refresh.Say 15 | GET /refresh/room/leave Refresh.Leave 16 | 17 | # Long polling demo 18 | GET /longpolling/room LongPolling.Room 19 | GET /longpolling/room/messages LongPolling.WaitMessages 20 | POST /longpolling/room/messages LongPolling.Say 21 | GET /longpolling/room/leave LongPolling.Leave 22 | 23 | # WebSocket demo 24 | GET /websocket/room WebSocket.Room 25 | WS /websocket/room/socket WebSocket.RoomSocket 26 | 27 | # Map static resources from the /app/public folder to the /public path 28 | GET /public/*filepath Static.Serve("public") 29 | 30 | -------------------------------------------------------------------------------- /samples/chat/public/images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robfig/revel/e8aac16df9e93b17d309595b4ff57405dc567fdf/samples/chat/public/images/favicon.png -------------------------------------------------------------------------------- /samples/chat/public/javascripts/jquery.scrollTo-min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * jQuery.ScrollTo - Easy element scrolling using jQuery. 3 | * Copyright (c) 2007-2009 Ariel Flesler - aflesler(at)gmail(dot)com | http://flesler.blogspot.com 4 | * Dual licensed under MIT and GPL. 5 | * Date: 5/25/2009 6 | * @author Ariel Flesler 7 | * @version 1.4.2 8 | * 9 | * http://flesler.blogspot.com/2007/10/jqueryscrollto.html 10 | */ 11 | ;(function(d){var k=d.scrollTo=function(a,i,e){d(window).scrollTo(a,i,e)};k.defaults={axis:'xy',duration:parseFloat(d.fn.jquery)>=1.3?0:1};k.window=function(a){return d(window)._scrollable()};d.fn._scrollable=function(){return this.map(function(){var a=this,i=!a.nodeName||d.inArray(a.nodeName.toLowerCase(),['iframe','#document','html','body'])!=-1;if(!i)return a;var e=(a.contentWindow||a).document||a.ownerDocument||a;return d.browser.safari||e.compatMode=='BackCompat'?e.body:e.documentElement})};d.fn.scrollTo=function(n,j,b){if(typeof j=='object'){b=j;j=0}if(typeof b=='function')b={onAfter:b};if(n=='max')n=9e9;b=d.extend({},k.defaults,b);j=j||b.speed||b.duration;b.queue=b.queue&&b.axis.length>1;if(b.queue)j/=2;b.offset=p(b.offset);b.over=p(b.over);return this._scrollable().each(function(){var q=this,r=d(q),f=n,s,g={},u=r.is('html,body');switch(typeof f){case'number':case'string':if(/^([+-]=)?\d+(\.\d+)?(px|%)?$/.test(f)){f=p(f);break}f=d(f,this);case'object':if(f.is||f.style)s=(f=d(f)).offset()}d.each(b.axis.split(''),function(a,i){var e=i=='x'?'Left':'Top',h=e.toLowerCase(),c='scroll'+e,l=q[c],m=k.max(q,i);if(s){g[c]=s[h]+(u?0:l-r.offset()[h]);if(b.margin){g[c]-=parseInt(f.css('margin'+e))||0;g[c]-=parseInt(f.css('border'+e+'Width'))||0}g[c]+=b.offset[h]||0;if(b.over[h])g[c]+=f[i=='x'?'width':'height']()*b.over[h]}else{var o=f[h];g[c]=o.slice&&o.slice(-1)=='%'?parseFloat(o)/100*m:o}if(/^\d+$/.test(g[c]))g[c]=g[c]<=0?0:Math.min(g[c],m);if(!a&&b.queue){if(l!=g[c])t(b.onAfterFirst);delete g[c]}});t(b.onAfter);function t(a){r.animate(g,j,b.easing,a&&function(){a.call(this,n,b)})}}).end()};k.max=function(a,i){var e=i=='x'?'Width':'Height',h='scroll'+e;if(!d(a).is('html,body'))return a[h]-d(a)[e.toLowerCase()]();var c='client'+e,l=a.ownerDocument.documentElement,m=a.ownerDocument.body;return Math.max(l[h],m[h])-Math.min(l[c],m[c])};function p(a){return typeof a=='object'?a:{top:a,left:a}}})(jQuery); -------------------------------------------------------------------------------- /samples/chat/public/javascripts/templating.js: -------------------------------------------------------------------------------- 1 | 2 | // Simple JavaScript Templating 3 | // John Resig - http://ejohn.org/ - MIT Licensed 4 | (function(){ 5 | var cache = {}; 6 | 7 | this.tmpl = function tmpl(str, data){ 8 | // Figure out if we're getting a template, or if we need to 9 | // load the template - and be sure to cache the result. 10 | var fn = !/\W/.test(str) ? 11 | cache[str] = cache[str] || 12 | tmpl(document.getElementById(str).innerHTML) : 13 | 14 | // Generate a reusable function that will serve as a template 15 | // generator (and which will be cached). 16 | new Function("obj", 17 | "var p=[],print=function(){p.push.apply(p,arguments);};" + 18 | 19 | // Introduce the data as local variables using with(){} 20 | "with(obj){p.push('" + 21 | 22 | // Convert the template into pure JavaScript 23 | str 24 | .replace(/[\r\t\n]/g, " ") 25 | .split("<%").join("\t") 26 | .replace(/((^|%>)[^\t]*)'/g, "$1\r") 27 | .replace(/\t=(.*?)%>/g, "',$1,'") 28 | .split("\t").join("');") 29 | .split("%>").join("p.push('") 30 | .split("\r").join("\\'") 31 | + "');}return p.join('');"); 32 | 33 | // Provide some basic currying to the user 34 | return data ? fn( data ) : fn; 35 | }; 36 | })(); 37 | -------------------------------------------------------------------------------- /samples/chat/public/stylesheets/main.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | background: #ddd; 3 | padding: 0; 4 | margin: 0; 5 | font-family: Helvetica, Arial, Sans; 6 | } 7 | 8 | #home { 9 | margin: 100px auto; 10 | text-align: center; 11 | } 12 | 13 | #signin { 14 | margin: 10px auto; 15 | width: 400px; 16 | border: 8px solid #333; 17 | padding: 20px; 18 | background: #fff; 19 | text-align: left; 20 | } 21 | 22 | #signin p { 23 | margin: 5px 0; 24 | } 25 | 26 | #signin label { 27 | font-size: 14px; 28 | display: block; 29 | width: 150px; 30 | text-align: right; 31 | float: left; 32 | margin-right: 5px; 33 | padding-top: 2px; 34 | } 35 | 36 | #signin #nick { 37 | margin-left: 10px; 38 | width: 200px; 39 | } 40 | 41 | #signin #enter { 42 | display: block; 43 | margin: 10px 0 0 160px; 44 | } 45 | 46 | #signin .error { 47 | background: #c00; 48 | margin: 0 0 10px 0; 49 | padding: 5px; 50 | text-align: center; 51 | color: #fff; 52 | font-size: 14px; 53 | } 54 | 55 | h1 { 56 | background: #111; 57 | margin: 0; 58 | color: gold; 59 | padding: 7px 14px; 60 | font-size: 14px; 61 | font-weight: normal; 62 | } 63 | 64 | h1 a { 65 | position: absolute; 66 | right: 1em; 67 | color: lightblue; 68 | } 69 | 70 | #thread { 71 | position: fixed; 72 | top: 50px; 73 | left: 14px; 74 | right: 14px; 75 | bottom: 100px; 76 | background: white; 77 | border-bottom: 3px solid #666; 78 | padding: 10px; 79 | overflow-y: scroll; 80 | } 81 | 82 | .message { 83 | border-bottom: 1px solid #efefef; 84 | position: relative; 85 | } 86 | 87 | .message h2 { 88 | background: #efefef; 89 | color: #000; 90 | font-size: 12px; 91 | font-weight: bold; 92 | margin: 0; 93 | padding: 5px; 94 | position: absolute; 95 | top: 0; 96 | left: 0; 97 | bottom: 0; 98 | width: 100px; 99 | text-align: right; 100 | border-right: 1px solid #ccc; 101 | } 102 | 103 | .message p { 104 | font-size: 12px; 105 | padding: 5px; 106 | margin: 0 0 0 110px; 107 | } 108 | 109 | .message.you * { 110 | background: #FFFFCC; 111 | } 112 | 113 | .message.notice * { 114 | background: #D9E7FB; 115 | } 116 | 117 | .message.important * { 118 | background: #c00; 119 | color: #fff; 120 | } 121 | 122 | .message.notice h2 { 123 | visibility: hidden; 124 | } 125 | 126 | .message.notice p { 127 | margin: 0; 128 | padding-left: 115px; 129 | } 130 | 131 | #newMessage { 132 | position: fixed; 133 | left: 14px; 134 | right: 14px; 135 | bottom: 30px; 136 | height: 60px; 137 | } 138 | 139 | #newMessage #message { 140 | width: 70%; 141 | padding: 5px; 142 | } 143 | -------------------------------------------------------------------------------- /samples/chat/tests/apptest.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import "github.com/robfig/revel" 4 | 5 | type ApplicationTest struct { 6 | revel.TestSuite 7 | } 8 | 9 | func (t *ApplicationTest) Before() { 10 | println("Set up") 11 | } 12 | 13 | func (t *ApplicationTest) TestThatIndexPageWorks() { 14 | t.Get("/") 15 | t.AssertOk() 16 | t.AssertContentType("text/html; charset=utf-8") 17 | } 18 | 19 | func (t *ApplicationTest) After() { 20 | println("Tear down") 21 | } 22 | -------------------------------------------------------------------------------- /samples/facebook-oauth2/app/controllers/app.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "code.google.com/p/goauth2/oauth" 7 | "github.com/robfig/revel" 8 | "github.com/robfig/revel/samples/facebook-oauth2/app/models" 9 | "net/http" 10 | "net/url" 11 | "strconv" 12 | ) 13 | 14 | type Application struct { 15 | *revel.Controller 16 | } 17 | 18 | // The following keys correspond to a test application 19 | // registered on Facebook, and associated with the loisant.org domain. 20 | // You need to bind loisant.org to your machine with /etc/hosts to 21 | // test the application locally. 22 | 23 | var FACEBOOK = &oauth.Config{ 24 | ClientId: "95341411595", 25 | ClientSecret: "8eff1b488da7fe3426f9ecaf8de1ba54", 26 | AuthURL: "https://graph.facebook.com/oauth/authorize", 27 | TokenURL: "https://graph.facebook.com/oauth/access_token", 28 | RedirectURL: "http://loisant.org:9000/Application/Auth", 29 | } 30 | 31 | func (c Application) Index() revel.Result { 32 | u := c.connected() 33 | me := map[string]interface{}{} 34 | if u != nil && u.AccessToken != "" { 35 | resp, _ := http.Get("https://graph.facebook.com/me?access_token=" + 36 | url.QueryEscape(u.AccessToken)) 37 | defer resp.Body.Close() 38 | if err := json.NewDecoder(resp.Body).Decode(&me); err != nil { 39 | revel.ERROR.Println(err) 40 | } 41 | revel.INFO.Println(me) 42 | } 43 | 44 | authUrl := FACEBOOK.AuthCodeURL("foo") 45 | return c.Render(me, authUrl) 46 | } 47 | 48 | func (c Application) Auth(code string) revel.Result { 49 | t := &oauth.Transport{Config: FACEBOOK} 50 | tok, err := t.Exchange(code) 51 | if err != nil { 52 | revel.ERROR.Println(err) 53 | return c.Redirect(Application.Index) 54 | } 55 | 56 | user := c.connected() 57 | user.AccessToken = tok.AccessToken 58 | return c.Redirect(Application.Index) 59 | } 60 | 61 | func setuser(c *revel.Controller) revel.Result { 62 | var user *models.User 63 | if _, ok := c.Session["uid"]; ok { 64 | uid, _ := strconv.ParseInt(c.Session["uid"], 10, 0) 65 | user = models.GetUser(int(uid)) 66 | } 67 | if user == nil { 68 | user = models.NewUser() 69 | c.Session["uid"] = fmt.Sprintf("%d", user.Uid) 70 | } 71 | c.RenderArgs["user"] = user 72 | return nil 73 | } 74 | 75 | func init() { 76 | revel.InterceptFunc(setuser, revel.BEFORE, &Application{}) 77 | } 78 | 79 | func (c Application) connected() *models.User { 80 | return c.RenderArgs["user"].(*models.User) 81 | } 82 | -------------------------------------------------------------------------------- /samples/facebook-oauth2/app/models/user.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import "math/rand" 4 | 5 | type User struct { 6 | Uid int 7 | AccessToken string 8 | } 9 | 10 | var db = make(map[int]*User) 11 | 12 | func GetUser(id int) *User { 13 | return db[id] 14 | } 15 | 16 | func NewUser() *User { 17 | user := &User{Uid: rand.Intn(10000)} 18 | db[user.Uid] = user 19 | return user 20 | } 21 | -------------------------------------------------------------------------------- /samples/facebook-oauth2/app/views/Application/Index.html: -------------------------------------------------------------------------------- 1 | {{set . "title" "Home"}} 2 | {{template "header.html" .}} 3 | 4 |

Facebook!

5 | 6 |

The example is configured with a key corresponding to the loisant.org domain. 7 | Facebook will no accept redirection to other domains (such as localhost) 8 | To test it, you need to either map 127.0.0.1 to loisant.org in your /etc/hosts, 9 | either replace the configuration to an application that you created on Facebook.

10 | 11 | {{if .me}} 12 |

You're {{.me.name}} on Facebook

13 | 14 | 15 | {{else}} 16 | login 17 | {{end}} 18 | 19 | {{template "footer.html" .}} 20 | -------------------------------------------------------------------------------- /samples/facebook-oauth2/app/views/footer.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /samples/facebook-oauth2/app/views/header.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{.title}} 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /samples/facebook-oauth2/conf/app.conf: -------------------------------------------------------------------------------- 1 | app.name=Facebook OAuth2 2 | app.secret=ly2bgKNgQ7BSoWW8BZ33Tos9Z9hy2Ck6bFXotLxwSaXk60iXPqlZUKnJpGVoIHhs 3 | module.static=github.com/robfig/revel/modules/static 4 | module.testrunner=github.com/robfig/revel/modules/testrunner 5 | mode.dev=true 6 | [dev] 7 | [prod] 8 | -------------------------------------------------------------------------------- /samples/facebook-oauth2/conf/routes: -------------------------------------------------------------------------------- 1 | # Routes 2 | # This file defines all application routes (Higher priority routes first) 3 | # ~~~~ 4 | 5 | module:testrunner 6 | 7 | # Home page 8 | GET / Application.Index 9 | 10 | # Map static resources from the /app/public folder to the /public path 11 | GET /public/*filepath Static.Serve("public") 12 | 13 | # Catch all 14 | * /:controller/:action :controller.:action 15 | -------------------------------------------------------------------------------- /samples/facebook-oauth2/public/css/main.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robfig/revel/e8aac16df9e93b17d309595b4ff57405dc567fdf/samples/facebook-oauth2/public/css/main.css -------------------------------------------------------------------------------- /samples/facebook-oauth2/public/images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robfig/revel/e8aac16df9e93b17d309595b4ff57405dc567fdf/samples/facebook-oauth2/public/images/favicon.png -------------------------------------------------------------------------------- /samples/facebook-oauth2/tests/apptest.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import "github.com/robfig/revel" 4 | 5 | type ApplicationTest struct { 6 | revel.TestSuite 7 | } 8 | 9 | func (t *ApplicationTest) Before() { 10 | println("Set up") 11 | } 12 | 13 | func (t *ApplicationTest) TestThatIndexPageWorks() { 14 | t.Get("/") 15 | t.AssertOk() 16 | t.AssertContentType("text/html; charset=utf-8") 17 | } 18 | 19 | func (t *ApplicationTest) After() { 20 | println("Tear down") 21 | } 22 | -------------------------------------------------------------------------------- /samples/i18n/app/controllers/app.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import "github.com/robfig/revel" 4 | 5 | type Application struct { 6 | *revel.Controller 7 | } 8 | 9 | func (c Application) Index() revel.Result { 10 | // Localization information 11 | c.RenderArgs["acceptLanguageHeader"] = c.Request.Header.Get("Accept-Language") 12 | c.RenderArgs["acceptLanguageHeaderParsed"] = c.Request.AcceptLanguages.String() 13 | c.RenderArgs["acceptLanguageHeaderMostQualified"] = c.Request.AcceptLanguages[0] 14 | c.RenderArgs["controllerCurrentLocale"] = c.Request.Locale 15 | 16 | // Controller-resolves messages 17 | c.RenderArgs["controllerGreeting"] = c.Message("greeting") 18 | c.RenderArgs["controllerGreetingName"] = c.Message("greeting.name") 19 | c.RenderArgs["controllerGreetingSuffix"] = c.Message("greeting.suffix") 20 | c.RenderArgs["controllerGreetingFull"] = c.Message("greeting.full") 21 | c.RenderArgs["controllerGreetingWithArgument"] = c.Message("greeting.full.name", "Steve Buscemi") 22 | 23 | return c.Render() 24 | } 25 | -------------------------------------------------------------------------------- /samples/i18n/app/views/Application/Index.html: -------------------------------------------------------------------------------- 1 | {{set . "title" "Home"}} 2 | {{template "header.html" .}} 3 | 4 |

Internationalization overview/test page

5 | 6 |

Client localization information

7 |

8 |

    9 |
  • Accept-Language HTTP header: {{.acceptLanguageHeader}}
  • 10 |
  • Parsed Accept-Language HTTP header: {{.acceptLanguageHeaderParsed}}
  • 11 |
  • Current preferred locale as determined by the framework: {{.currentLocale}}
  • 12 |
13 |

14 | 15 |

Messages from the sample message file as resolved using Controller.Message(...)

16 |

17 |

    18 |
  • Greeting: {{.controllerGreeting}}
  • 19 |
  • Greeting name: {{.controllerGreetingName}}
  • 20 |
  • Greeting suffix: {{.controllerGreetingSuffix}}
  • 21 |
  • Full greeting: {{.controllerGreetingFull}}
  • 22 |
  • Full greeting with argument: {{.controllerGreetingWithArgument}}
  • 23 |
24 |

25 | 26 |

Messages from the sample message file as resolved using the {{msg . "message" "arg" "arg"}} template function

27 |

28 |

    29 |
  • Greeting: {{msg . "greeting"}}
  • 30 |
  • Greeting name: {{msg . "greeting.name"}}
  • 31 |
  • Greeting suffix: {{msg . "greeting.suffix"}}
  • 32 |
  • Full greeting: {{msg . "greeting.full"}}
  • 33 |
  • Full greeting with argument: {{msg . "greeting.full.name" "Tommy Lee Jones"}}
  • 34 |
  • Cross-file referencing: {{msg . "alpha"}}
  • 35 |
  • Message not found: {{msg . "doesnt exist"}}
  • 36 |
37 |

38 | 39 | {{template "footer.html" .}} 40 | -------------------------------------------------------------------------------- /samples/i18n/app/views/errors/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Not found 5 | 6 | 7 | {{if eq .RunMode "dev"}} 8 | {{template "errors/404-dev.html" .}} 9 | {{else}} 10 | {{with .Error}} 11 |

12 | {{.Title}} 13 |

14 |

15 | {{.Description}} 16 |

17 | {{end}} 18 | {{end}} 19 | 20 | 21 | -------------------------------------------------------------------------------- /samples/i18n/app/views/errors/500.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Application error 5 | 6 | 7 | {{if eq .RunMode "dev"}} 8 | {{template "errors/500-dev.html" .}} 9 | {{else}} 10 |

Oops, an error occured

11 |

12 | This exception has been logged. 13 |

14 | {{end}} 15 | 16 | 17 | -------------------------------------------------------------------------------- /samples/i18n/app/views/footer.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /samples/i18n/app/views/header.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{.title}} 6 | 7 | 8 | 9 | 10 | {{range .moreStyles}} 11 | 12 | {{end}} 13 | {{range .moreScripts}} 14 | 15 | {{end}} 16 | 17 | 18 | -------------------------------------------------------------------------------- /samples/i18n/conf/app.conf: -------------------------------------------------------------------------------- 1 | app.name=i18n 2 | app.secret=bPlNFGdSC2wd8f2QnFhk5A84JJjKWZdKH9H2FHFuvUs9Jz8UvBHv3Vc5awx39ivu 3 | http.addr= 4 | http.port=9000 5 | cookie.prefix=REVEL 6 | 7 | # The language cookie name used to store the current language. 8 | i18n.cookie=%(cookie.prefix)s_LANG 9 | 10 | # The default language of this application. 11 | i18n.default_language=en 12 | 13 | [dev] 14 | mode.dev=true 15 | results.pretty=true 16 | results.staging=true 17 | watch=true 18 | 19 | #module.testrunner = github.com/robfig/revel/modules/testrunner 20 | module.static=github.com/robfig/revel/modules/static 21 | 22 | log.trace.output = stdout 23 | log.info.output = stderr 24 | log.warn.output = stderr 25 | log.error.output = stderr 26 | 27 | [prod] 28 | mode.dev=false 29 | results.pretty=false 30 | results.staging=false 31 | watch=false 32 | 33 | module.testrunner = 34 | 35 | log.trace.output = off 36 | log.info.output = off 37 | log.warn.output = %(app.name)s.log 38 | log.error.output = %(app.name)s.log 39 | -------------------------------------------------------------------------------- /samples/i18n/conf/routes: -------------------------------------------------------------------------------- 1 | # Routes 2 | # This file defines all application routes (Higher priority routes first) 3 | # ~~~~ 4 | 5 | GET / Application.Index 6 | 7 | # Ignore favicon requests 8 | GET /favicon.ico 404 9 | 10 | # Map static resources from the /app/public folder to the /public path 11 | GET /public/*filepath Static.Serve("public") 12 | 13 | # Catch all 14 | * /:controller/:action :controller.:action 15 | -------------------------------------------------------------------------------- /samples/i18n/messages/sample.en: -------------------------------------------------------------------------------- 1 | # Sample messages file for the English language (en) 2 | # Message file extensions should be ISO 639-1 codes (http://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) 3 | # Sections within each message file can optionally override the defaults using ISO 3166-1 alpha-2 codes (http://en.wikipedia.org/wiki/ISO_3166-1_alpha-2) 4 | # See also: 5 | # - http://www.rfc-editor.org/rfc/bcp/bcp47.txt 6 | # - http://www.w3.org/International/questions/qa-accept-lang-locales 7 | 8 | # Default values for English 9 | greeting=Hello 10 | greeting.name=Rob 11 | greeting.suffix=, welcome to Revel! 12 | 13 | greeting.full=%(greeting)s %(greeting.name)s%(greeting.suffix)s 14 | 15 | greeting.full.name=%(greeting)s %s%(greeting.suffix)s 16 | 17 | # beta is defined in sample2.en 18 | alpha=alpha %(beta)s 19 | 20 | # Specify region-specific overrides 21 | [AU] 22 | greeting=G'day 23 | 24 | [US] 25 | greeting=Howdy 26 | 27 | [GB] 28 | greeting=All right 29 | -------------------------------------------------------------------------------- /samples/i18n/messages/sample2.en: -------------------------------------------------------------------------------- 1 | beta=beta 2 | -------------------------------------------------------------------------------- /samples/i18n/public/images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robfig/revel/e8aac16df9e93b17d309595b4ff57405dc567fdf/samples/i18n/public/images/favicon.png -------------------------------------------------------------------------------- /samples/i18n/public/js/jquery-1.5.2.min.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robfig/revel/e8aac16df9e93b17d309595b4ff57405dc567fdf/samples/i18n/public/js/jquery-1.5.2.min.js -------------------------------------------------------------------------------- /samples/i18n/tests/apptest.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import "github.com/robfig/revel" 4 | 5 | type ApplicationTest struct { 6 | revel.TestSuite 7 | } 8 | 9 | func (t ApplicationTest) Before() { 10 | println("Set up") 11 | } 12 | 13 | func (t ApplicationTest) TestThatIndexPageWorks() { 14 | t.Get("/") 15 | t.AssertOk() 16 | t.AssertContentType("text/html; charset=utf-8") 17 | } 18 | 19 | func (t ApplicationTest) After() { 20 | println("Tear down") 21 | } 22 | -------------------------------------------------------------------------------- /samples/persona/.gitignore: -------------------------------------------------------------------------------- 1 | test-results/ 2 | tmp/ 3 | routes/ 4 | -------------------------------------------------------------------------------- /samples/persona/app/controllers/app.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io/ioutil" 7 | "net/http" 8 | "net/url" 9 | "strings" 10 | 11 | "github.com/robfig/revel" 12 | ) 13 | 14 | const host = "" // set this to your host 15 | 16 | type App struct { 17 | *revel.Controller 18 | } 19 | 20 | type PersonaResponse struct { 21 | Status string `json:"status"` 22 | Email string `json:"email"` 23 | Audience string `json:"audience"` 24 | Expires int64 `json:"expires"` 25 | Issuer string `json:"issuer"` 26 | } 27 | 28 | type LoginResult struct { 29 | StatusCode int 30 | Message string 31 | } 32 | 33 | func (r LoginResult) Apply(req *revel.Request, resp *revel.Response) { 34 | resp.WriteHeader(r.StatusCode, "text/html") 35 | resp.Out.Write([]byte(r.Message)) 36 | } 37 | 38 | func (c App) Index() revel.Result { 39 | email := c.Session["email"] 40 | return c.Render(email) 41 | } 42 | 43 | func (c App) Login(assertion string) revel.Result { 44 | assertion = strings.TrimSpace(assertion) 45 | if assertion == "" { 46 | return &LoginResult{ 47 | StatusCode: http.StatusBadRequest, 48 | Message: "Assertion required.", 49 | } 50 | } 51 | 52 | values := url.Values{"assertion": {assertion}, "audience": {host}} 53 | resp, err := http.PostForm("https://verifier.login.persona.org/verify", values) 54 | if err != nil { 55 | return &LoginResult{ 56 | StatusCode: http.StatusBadRequest, 57 | Message: "Authentication failed.", 58 | } 59 | } 60 | 61 | body, err := ioutil.ReadAll(resp.Body) 62 | if err != nil { 63 | return &LoginResult{ 64 | StatusCode: http.StatusBadRequest, 65 | Message: "Authentication failed.", 66 | } 67 | } 68 | 69 | p := &PersonaResponse{} 70 | err = json.Unmarshal(body, p) 71 | if err != nil { 72 | return &LoginResult{ 73 | StatusCode: http.StatusBadRequest, 74 | Message: "Authentication failed.", 75 | } 76 | } 77 | 78 | c.Session["email"] = p.Email 79 | fmt.Println("Login successful: ", p.Email) 80 | 81 | return &LoginResult{ 82 | StatusCode: http.StatusOK, 83 | Message: "Login successful.", 84 | } 85 | } 86 | 87 | func (c App) Logout() revel.Result { 88 | delete(c.Session, "email") 89 | return c.Redirect("/") 90 | } 91 | -------------------------------------------------------------------------------- /samples/persona/app/views/App/Index.html: -------------------------------------------------------------------------------- 1 | {{append . "moreStyles" "css/common.css"}} 2 | 3 | {{set . "title" "Home"}} 4 | {{template "header.html" .}} 5 | 6 |
7 |
8 |
9 |
10 | {{if .email}} 11 | Hello {{.email}} Signout 12 | {{else}} 13 | Signin with your email 14 | {{end}} 15 |
16 |
17 |
18 |
19 | 20 |
21 |
22 |
23 | {{template "flash.html" .}} 24 |
25 |
26 |
27 | 28 | {{template "footer.html" .}} 29 | -------------------------------------------------------------------------------- /samples/persona/app/views/debug.html: -------------------------------------------------------------------------------- 1 | 20 | 44 | 45 | 46 | 63 | -------------------------------------------------------------------------------- /samples/persona/app/views/flash.html: -------------------------------------------------------------------------------- 1 | {{if .flash.success}} 2 |
3 | {{.flash.success}} 4 |
5 | {{end}} 6 | 7 | {{if or .errors .flash.error}} 8 |
9 | {{if .flash.error}} 10 | {{.flash.error}} 11 | {{end}} 12 |
    13 | {{range .errors}} 14 |
  • {{.}}
  • 15 | {{end}} 16 |
17 |
18 | {{end}} 19 | -------------------------------------------------------------------------------- /samples/persona/app/views/footer.html: -------------------------------------------------------------------------------- 1 | {{if eq .RunMode "dev"}} 2 | {{template "debug.html" .}} 3 | {{end}} 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /samples/persona/app/views/header.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{.title}} 6 | 7 | 8 | 9 | 10 | 11 | 42 | {{range .moreStyles}} 43 | 44 | {{end}} 45 | {{range .moreScripts}} 46 | 47 | {{end}} 48 | 49 | 50 | -------------------------------------------------------------------------------- /samples/persona/conf/app.conf: -------------------------------------------------------------------------------- 1 | app.name=persona 2 | app.secret=bPlNFGdSC2wd8f2QnFhk5A84JJjKWZdKH9H2FHFuvUs9Jz8UvBHv3Vc5awx39ivu 3 | http.addr= 4 | http.port=9000 5 | http.ssl=false 6 | http.sslcert= 7 | http.sslkey= 8 | cookie.prefix=REVEL 9 | format.date=01/02/2006 10 | format.datetime=01/02/2006 15:04 11 | results.chunked=false 12 | 13 | log.trace.prefix = "TRACE " 14 | log.info.prefix = "INFO " 15 | log.warn.prefix = "WARN " 16 | log.error.prefix = "ERROR " 17 | 18 | # The default language of this application. 19 | i18n.default_language=en 20 | 21 | module.static=github.com/robfig/revel/modules/static 22 | 23 | [dev] 24 | mode.dev=true 25 | results.pretty=true 26 | watch=true 27 | 28 | module.testrunner = github.com/robfig/revel/modules/testrunner 29 | 30 | log.trace.output = off 31 | log.info.output = stderr 32 | log.warn.output = stderr 33 | log.error.output = stderr 34 | 35 | [prod] 36 | mode.dev=false 37 | results.pretty=false 38 | watch=false 39 | 40 | module.testrunner = 41 | 42 | log.trace.output = off 43 | log.info.output = off 44 | log.warn.output = %(app.name)s.log 45 | log.error.output = %(app.name)s.log 46 | -------------------------------------------------------------------------------- /samples/persona/conf/routes: -------------------------------------------------------------------------------- 1 | # Routes 2 | # This file defines all application routes (Higher priority routes first) 3 | # ~~~~ 4 | 5 | module:testrunner 6 | 7 | GET / App.Index 8 | GET /login App.Login 9 | POST /login App.Login 10 | GET /logout App.Logout 11 | 12 | # Ignore favicon requests 13 | GET /favicon.ico 404 14 | 15 | # Map static resources from the /app/public folder to the /public path 16 | GET /public/*filepath Static.Serve("public") 17 | 18 | # Catch all 19 | * /:controller/:action :controller.:action 20 | -------------------------------------------------------------------------------- /samples/persona/messages/sample.en: -------------------------------------------------------------------------------- 1 | # Sample messages file for the English language (en) 2 | # Message file extensions should be ISO 639-1 codes (http://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) 3 | # Sections within each message file can optionally override the defaults using ISO 3166-1 alpha-2 codes (http://en.wikipedia.org/wiki/ISO_3166-1_alpha-2) 4 | # See also: 5 | # - http://www.rfc-editor.org/rfc/bcp/bcp47.txt 6 | # - http://www.w3.org/International/questions/qa-accept-lang-locales 7 | 8 | -------------------------------------------------------------------------------- /samples/persona/public/css/common.css: -------------------------------------------------------------------------------- 1 | #login { 2 | background: url(../img/persona-signin.png) no-repeat top left; 3 | text-indent: -99999px; 4 | width: 202px; 5 | height: 25px; 6 | display: inline-block; 7 | } -------------------------------------------------------------------------------- /samples/persona/public/img/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robfig/revel/e8aac16df9e93b17d309595b4ff57405dc567fdf/samples/persona/public/img/favicon.png -------------------------------------------------------------------------------- /samples/persona/public/img/glyphicons-halflings-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robfig/revel/e8aac16df9e93b17d309595b4ff57405dc567fdf/samples/persona/public/img/glyphicons-halflings-white.png -------------------------------------------------------------------------------- /samples/persona/public/img/glyphicons-halflings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robfig/revel/e8aac16df9e93b17d309595b4ff57405dc567fdf/samples/persona/public/img/glyphicons-halflings.png -------------------------------------------------------------------------------- /samples/persona/public/img/persona-signin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robfig/revel/e8aac16df9e93b17d309595b4ff57405dc567fdf/samples/persona/public/img/persona-signin.png -------------------------------------------------------------------------------- /samples/persona/tests/apptest.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import "github.com/robfig/revel" 4 | 5 | type AppTest struct { 6 | revel.TestSuite 7 | } 8 | 9 | func (t *AppTest) Before() { 10 | println("Set up") 11 | } 12 | 13 | func (t AppTest) TestThatIndexPageWorks() { 14 | t.Get("/") 15 | t.AssertOk() 16 | t.AssertContentType("text/html; charset=utf-8") 17 | } 18 | 19 | func (t *AppTest) After() { 20 | println("Tear down") 21 | } 22 | -------------------------------------------------------------------------------- /samples/twitter-oauth/app/controllers/app.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/mrjones/oauth" 6 | "github.com/robfig/revel" 7 | "github.com/robfig/revel/samples/twitter-oauth/app/models" 8 | "io/ioutil" 9 | ) 10 | 11 | var TWITTER = oauth.NewConsumer( 12 | "VgRjky4dTA1U2Ck16MmZw", 13 | "l8lOLyIF3peCFEvrEoTc8h4oFwieAFgPM6eeTRo30I", 14 | oauth.ServiceProvider{ 15 | AuthorizeTokenUrl: "https://api.twitter.com/oauth/authorize", 16 | RequestTokenUrl: "https://api.twitter.com/oauth/request_token", 17 | AccessTokenUrl: "https://api.twitter.com/oauth/access_token", 18 | }, 19 | ) 20 | 21 | type Application struct { 22 | *revel.Controller 23 | } 24 | 25 | func (c Application) Index() revel.Result { 26 | user := getUser() 27 | if user.AccessToken == nil { 28 | return c.Render() 29 | } 30 | 31 | // We have a token, so look for mentions. 32 | resp, err := TWITTER.Get( 33 | "https://api.twitter.com/1.1/statuses/mentions_timeline.json", 34 | map[string]string{"count": "10"}, 35 | user.AccessToken) 36 | if err != nil { 37 | revel.ERROR.Println(err) 38 | return c.Render() 39 | } 40 | defer resp.Body.Close() 41 | 42 | // Extract the mention text. 43 | mentions := []struct { 44 | Text string `json:text` 45 | }{} 46 | err = json.NewDecoder(resp.Body).Decode(&mentions) 47 | if err != nil { 48 | revel.ERROR.Println(err) 49 | } 50 | revel.INFO.Println(mentions) 51 | return c.Render(mentions) 52 | } 53 | 54 | func (c Application) SetStatus(status string) revel.Result { 55 | resp, err := TWITTER.PostForm( 56 | "http://api.twitter.com/1.1/statuses/update.json", 57 | map[string]string{"status": status}, 58 | getUser().AccessToken, 59 | ) 60 | if err != nil { 61 | revel.ERROR.Println(err) 62 | return c.RenderError(err) 63 | } 64 | defer resp.Body.Close() 65 | bodyBytes, _ := ioutil.ReadAll(resp.Body) 66 | revel.INFO.Println(string(bodyBytes)) 67 | c.Response.ContentType = "application/json" 68 | return c.RenderText(string(bodyBytes)) 69 | } 70 | 71 | // Twitter authentication 72 | 73 | func (c Application) Authenticate(oauth_verifier string) revel.Result { 74 | user := getUser() 75 | if oauth_verifier != "" { 76 | // We got the verifier; now get the access token, store it and back to index 77 | accessToken, err := TWITTER.AuthorizeToken(user.RequestToken, oauth_verifier) 78 | if err == nil { 79 | user.AccessToken = accessToken 80 | } else { 81 | revel.ERROR.Println("Error connecting to twitter:", err) 82 | } 83 | return c.Redirect(Application.Index) 84 | } 85 | 86 | requestToken, url, err := TWITTER.GetRequestTokenAndUrl("http://127.0.0.1:9000/Application/Authenticate") 87 | if err == nil { 88 | // We received the unauthorized tokens in the OAuth object - store it before we proceed 89 | user.RequestToken = requestToken 90 | return c.Redirect(url) 91 | } else { 92 | revel.ERROR.Println("Error connecting to twitter:", err) 93 | } 94 | return c.Redirect(Application.Index) 95 | } 96 | 97 | func getUser() *models.User { 98 | return models.FindOrCreate("guest") 99 | } 100 | 101 | func init() { 102 | TWITTER.Debug(true) 103 | } 104 | -------------------------------------------------------------------------------- /samples/twitter-oauth/app/models/user.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import "github.com/mrjones/oauth" 4 | 5 | type User struct { 6 | Username string 7 | RequestToken *oauth.RequestToken 8 | AccessToken *oauth.AccessToken 9 | } 10 | 11 | func FindOrCreate(username string) *User { 12 | if user, ok := db[username]; ok { 13 | return user 14 | } 15 | user := &User{Username: username} 16 | db[username] = user 17 | return user 18 | } 19 | 20 | var db = make(map[string]*User) 21 | -------------------------------------------------------------------------------- /samples/twitter-oauth/app/views/Application/Index.html: -------------------------------------------------------------------------------- 1 | {{set . "title" "Home"}} 2 | {{template "header.html" .}} 3 | 4 |

Useless Twitter Mashup

5 | 6 | 7 | 8 | Status: 9 | 10 |

Mentions

11 | 12 |
    13 | {{range .mentions}} 14 |
  • 15 | {{.Text}} 16 |
  • 17 | {{end}} 18 |
19 | 20 | 40 | 41 | {{template "footer.html" .}} 42 | -------------------------------------------------------------------------------- /samples/twitter-oauth/app/views/footer.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /samples/twitter-oauth/app/views/header.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{.title}} 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /samples/twitter-oauth/conf/app.conf: -------------------------------------------------------------------------------- 1 | app.secret=e227tafmfs0xrexah43hm34kkrcetav48nwk9x037wp87jkrp06m7n8wc8m7gbag 2 | module.static=github.com/robfig/revel/modules/static 3 | module.testrunner=github.com/robfig/revel/modules/testrunner 4 | mode.dev=true 5 | [dev] 6 | [prod] 7 | -------------------------------------------------------------------------------- /samples/twitter-oauth/conf/routes: -------------------------------------------------------------------------------- 1 | # Routes 2 | # This file defines all application routes (Higher priority routes first) 3 | # ~~~~ 4 | 5 | module:testrunner 6 | 7 | # Home page 8 | GET / Application.index 9 | GET /auth Application.authenticate 10 | 11 | POST /ws/status Application.setStatus 12 | 13 | # Map static resources from the /app/public folder to the /public path 14 | GET /public/*filepath Static.Serve("public") 15 | 16 | # Catch all 17 | * /:controller/:action :controller.:action 18 | -------------------------------------------------------------------------------- /samples/twitter-oauth/public/css/main.css: -------------------------------------------------------------------------------- 1 | html { 2 | background-color: #1196FC; 3 | font-family: sans-serif; 4 | } 5 | 6 | body { 7 | background-color: #77D9F9; 8 | -moz-border-radius: 20px; 9 | -webkit-border-radius: 20px; 10 | padding: 20px 30px 20px 30px; 11 | } 12 | 13 | .toolbar { 14 | height: 50px; 15 | } 16 | 17 | .toolbar a { 18 | float: right; 19 | text-decoration: none; 20 | background-color: #1196FC; 21 | color: white; 22 | font-weight: bold; 23 | cursor: pointer; 24 | -moz-border-radius: 10px; 25 | -webkit-border-radius: 10px; 26 | border-radius: 10px; 27 | padding: 3px 15px 3px 15px; 28 | } 29 | 30 | h1, h2, h3 { 31 | color: #154E84; 32 | } 33 | -------------------------------------------------------------------------------- /samples/twitter-oauth/public/images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robfig/revel/e8aac16df9e93b17d309595b4ff57405dc567fdf/samples/twitter-oauth/public/images/favicon.png -------------------------------------------------------------------------------- /samples/twitter-oauth/tests/apptest.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import "github.com/robfig/revel" 4 | 5 | type ApplicationTest struct { 6 | revel.TestSuite 7 | } 8 | 9 | func (t *ApplicationTest) Before() { 10 | println("Set up") 11 | } 12 | 13 | func (t *ApplicationTest) TestThatIndexPageWorks() { 14 | t.Get("/") 15 | t.AssertOk() 16 | t.AssertContentType("text/html; charset=utf-8") 17 | } 18 | 19 | func (t *ApplicationTest) After() { 20 | println("Tear down") 21 | } 22 | -------------------------------------------------------------------------------- /samples/validation/app/controllers/app.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import "github.com/robfig/revel" 4 | 5 | type Application struct { 6 | *revel.Controller 7 | } 8 | 9 | func (c Application) Index() revel.Result { 10 | return c.Render() 11 | } 12 | -------------------------------------------------------------------------------- /samples/validation/app/controllers/sample1.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "github.com/robfig/revel" 5 | ) 6 | 7 | type Sample1 struct { 8 | *revel.Controller 9 | } 10 | 11 | func (c Sample1) Index() revel.Result { 12 | return c.Render() 13 | } 14 | 15 | func (c Sample1) HandleSubmit( 16 | username, firstname, lastname string, 17 | age int, 18 | password, passwordConfirm, email, emailConfirm string, 19 | termsOfUse bool) revel.Result { 20 | 21 | // Validation rules 22 | c.Validation.Required(username).Message("Username is required.") 23 | c.Validation.MinSize(username, 6).Message("Username must be at least 6 characters.") 24 | c.Validation.Required(firstname).Message("First name is required.") 25 | c.Validation.Required(lastname).Message("Last name is required.") 26 | c.Validation.Required(age).Message("Age is required.") 27 | c.Validation.Range(age, 16, 120).Message("Age must be between 16 and 120.") 28 | c.Validation.Required(password).Message("Password is required.") 29 | c.Validation.MinSize(password, 6).Message("Password must be greater than 6 characters.") 30 | c.Validation.Required(passwordConfirm).Message("Please confirm your password.") 31 | c.Validation.Required(passwordConfirm == password).Message("Your passwords do not match.") 32 | c.Validation.Required(email).Message("Email is required.") 33 | c.Validation.Email(email).Message("A valid email is required.") 34 | c.Validation.Required(emailConfirm).Message("Please confirm your email address.") 35 | c.Validation.Required(emailConfirm == email).Message("Your email addresses do not match.") 36 | c.Validation.Required(termsOfUse == true).Message("Please agree to the terms.") 37 | 38 | // Handle errors 39 | if c.Validation.HasErrors() { 40 | c.Validation.Keep() 41 | c.FlashParams() 42 | return c.Redirect(Sample1.Index) 43 | } 44 | 45 | // Ok, display the created user 46 | return c.Render(username, firstname, lastname, age, password, email) 47 | } 48 | -------------------------------------------------------------------------------- /samples/validation/app/controllers/sample2.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "github.com/robfig/revel" 5 | ) 6 | 7 | type Sample2 struct { 8 | *revel.Controller 9 | } 10 | 11 | func (c Sample2) Index() revel.Result { 12 | return c.Render() 13 | } 14 | 15 | func (c Sample2) HandleSubmit( 16 | username, firstname, lastname string, 17 | age int, 18 | password, passwordConfirm, email, emailConfirm string, 19 | termsOfUse bool) revel.Result { 20 | 21 | // Validation rules 22 | c.Validation.Required(username) 23 | c.Validation.MinSize(username, 6) 24 | c.Validation.Required(firstname) 25 | c.Validation.Required(lastname) 26 | c.Validation.Required(age) 27 | c.Validation.Range(age, 16, 120) 28 | c.Validation.Required(password) 29 | c.Validation.MinSize(password, 6) 30 | c.Validation.Required(passwordConfirm) 31 | c.Validation.Required(passwordConfirm == password).Message("Your passwords do not match.") 32 | c.Validation.Required(email) 33 | c.Validation.Email(email) 34 | c.Validation.Required(emailConfirm) 35 | c.Validation.Required(emailConfirm == email).Message("Your email addresses do not match.") 36 | c.Validation.Required(termsOfUse == true).Message("Please agree to the terms.") 37 | 38 | // Handle errors 39 | if c.Validation.HasErrors() { 40 | c.Validation.Keep() 41 | c.FlashParams() 42 | return c.Redirect(Sample2.Index) 43 | } 44 | 45 | // Ok, display the created user 46 | return c.Render(username, firstname, lastname, age, password, email) 47 | } 48 | -------------------------------------------------------------------------------- /samples/validation/app/controllers/sample3.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "github.com/robfig/revel" 5 | "github.com/robfig/revel/samples/validation/app/models" 6 | ) 7 | 8 | type Sample3 struct { 9 | *revel.Controller 10 | } 11 | 12 | func (c Sample3) Index() revel.Result { 13 | return c.Render() 14 | } 15 | 16 | func (c Sample3) HandleSubmit(user *models.User) revel.Result { 17 | user.Validate(c.Validation) 18 | 19 | // Handle errors 20 | if c.Validation.HasErrors() { 21 | c.Validation.Keep() 22 | c.FlashParams() 23 | return c.Redirect(Sample3.Index) 24 | } 25 | 26 | // Ok, display the created user 27 | return c.Render(user) 28 | } 29 | -------------------------------------------------------------------------------- /samples/validation/app/controllers/sample4.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "github.com/robfig/revel" 5 | "github.com/robfig/revel/samples/validation/app/models" 6 | ) 7 | 8 | type Sample4 struct { 9 | *revel.Controller 10 | } 11 | 12 | func (c Sample4) Index() revel.Result { 13 | return c.Render() 14 | } 15 | 16 | func (c Sample4) HandleSubmit(user *models.User) revel.Result { 17 | user.Validate(c.Validation) 18 | 19 | // Handle errors 20 | if c.Validation.HasErrors() { 21 | c.Validation.Keep() 22 | c.FlashParams() 23 | return c.Redirect(Sample4.Index) 24 | } 25 | 26 | // Ok, display the created user 27 | return c.Render(user) 28 | } 29 | -------------------------------------------------------------------------------- /samples/validation/app/models/user.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import "github.com/robfig/revel" 4 | 5 | type User struct { 6 | Username string 7 | FirstName string 8 | LastName string 9 | Age int 10 | Password string 11 | PasswordConfirm string 12 | Email string 13 | EmailConfirm string 14 | TermsOfUse bool 15 | } 16 | 17 | func (user *User) Validate(v *revel.Validation) { 18 | v.Required(user.Username) 19 | v.MinSize(user.Username, 6) 20 | v.Required(user.FirstName) 21 | v.Required(user.LastName) 22 | v.Required(user.Age) 23 | v.Range(user.Age, 16, 120) 24 | v.Required(user.Password) 25 | v.MinSize(user.Password, 6) 26 | v.Required(user.PasswordConfirm) 27 | v.Required(user.PasswordConfirm == user.Password). 28 | Message("The passwords do not match.") 29 | v.Required(user.Email) 30 | v.Email(user.Email) 31 | v.Required(user.EmailConfirm) 32 | v.Required(user.EmailConfirm == user.Email). 33 | Message("The email addresses do not match") 34 | v.Required(user.TermsOfUse) 35 | } 36 | -------------------------------------------------------------------------------- /samples/validation/app/views/Application/Index.html: -------------------------------------------------------------------------------- 1 | {{set . "title" "Home"}} 2 | {{template "header.html" .}} 3 | 4 |

Validation samples

5 | 6 |

7 | Learn how to use the Revel validation framework. Each sample has it own controller and it own templates. 8 | {{/* Don't forget to take a look at the the conf/messages file. */}} 9 |

10 | 11 |
    12 |
  • 13 |

    Sample 1

    14 |

    15 | This sample shows very basic validation. All errors are displayed at the top 16 | of the form. 17 |

    18 |

    19 | See the sample 20 |

    21 |
  • 22 |
  • 23 |

    Sample 2

    24 |

    25 | Same controller than for the 1st sample, but errors are displayed inline, next to each 26 | field. 27 |

    28 |

    29 | See the sample 30 |

    31 |
  • 32 |
  • 33 |

    Sample 3

    34 |

    35 | This demonstrates best practice for validating a struct (bean), rather 36 | than many individual fields. 37 |

    38 |

    39 | See the sample 40 |

    41 |
  • 42 |
  • 43 |

    Sample 4

    44 |

    45 | Same validation as Sample 3, but the template use the {{"{{field}}"}} tag 46 | to scope all field data (error, flash, name, ...) to a single part of the 47 | template. 48 |

    49 |

    50 | See the sample 51 |

    52 |
  • 53 |
54 | -------------------------------------------------------------------------------- /samples/validation/app/views/Sample1/HandleSubmit.html: -------------------------------------------------------------------------------- 1 | {{template "header.html" .}} 2 | 3 |

User created !

4 | 5 |

6 | Desired username is {{.username}} 7 |

8 |

9 | First name is {{.firstname}} 10 |

11 |

12 | Last name is {{.lastname}} 13 |

14 |

15 | Age is {{.age}} 16 |

17 |

18 | Password is {{.password}} 19 |

20 |

21 | Email is {{.email}} 22 |

23 | 24 | Back 25 | 26 | {{template "footer.html" .}} 27 | -------------------------------------------------------------------------------- /samples/validation/app/views/Sample1/Index.html: -------------------------------------------------------------------------------- 1 | {{template "header.html" .}} 2 | 3 |

Validation sample 1

4 | 5 | {{if .errors}} 6 |
7 |

Oops, please correct these errors

8 |
    9 | {{range .errors}} 10 |
  • {{.}}
  • 11 | {{end}} 12 |
13 |
14 | {{end}} 15 | 16 |
17 |
18 | User informations 19 | 20 |
21 | 22 | 23 |
24 | 25 |
26 | 27 | 28 |
29 | 30 |
31 | 32 | 33 |
34 | 35 |
36 | 37 | 38 |
39 | 40 |
41 |
42 | Password 43 | 44 |
45 | 46 | 47 |
48 | 49 |
50 | 51 | 52 |
53 | 54 |
55 |
56 | Email 57 | 58 |
59 | 60 | 61 |
62 | 63 |
64 | 65 | 66 |
67 | 68 |
69 |
70 | Conditions 71 | 72 |
73 | Checking this box indicates that you accept terms of use. 74 | If you do not accept these terms, do not use this website : 75 |
76 | 77 |
78 | 79 | 80 |
81 | 82 |
83 | 84 | 85 |
86 | 87 | {{template "footer.html" .}} 88 | -------------------------------------------------------------------------------- /samples/validation/app/views/Sample2/HandleSubmit.html: -------------------------------------------------------------------------------- 1 | {{template "header.html" .}} 2 | 3 |

User created !

4 | 5 |

6 | Desired username is {{.username}} 7 |

8 |

9 | First name is {{.firstname}} 10 |

11 |

12 | Last name is {{.lastname}} 13 |

14 |

15 | Age is {{.age}} 16 |

17 |

18 | Password is {{.password}} 19 |

20 |

21 | Email is {{.email}} 22 |

23 | 24 | Back 25 | 26 | {{template "footer.html" .}} 27 | -------------------------------------------------------------------------------- /samples/validation/app/views/Sample2/Index.html: -------------------------------------------------------------------------------- 1 | {{template "header.html" .}} 2 | 3 |

Validation sample 2

4 | 5 | {{if .errors}} 6 |
7 |

Oops, please correct these errors

8 |
9 | {{end}} 10 | 11 |
12 |
13 | User informations 14 | 15 |
16 | 17 | 18 | {{.errors.username}} 19 |
20 | 21 |
22 | 23 | 24 | {{.errors.firstname}} 25 |
26 | 27 |
28 | 29 | 30 | {{.errors.lastname}} 31 |
32 | 33 |
34 | 35 | 36 | {{.errors.age}} 37 |
38 | 39 |
40 |
41 | Password 42 | 43 |
44 | 45 | 46 | {{.errors.password}} 47 |
48 | 49 |
50 | 51 | 52 | {{.errors.passwordConfirm}} 53 |
54 | 55 |
56 |
57 | Email 58 | 59 |
60 | 61 | 62 | {{.errors.email}} 63 |
64 | 65 |
66 | 67 | 68 | {{.errors.emailConfirm}} 69 |
70 | 71 |
72 |
73 | Conditions 74 | 75 |
76 | Checking this box indicates that you accept terms of use. 77 | If you do not accept these terms, do not use this website : 78 |
79 | 80 |
81 | 82 | 83 | {{.errors.termsOfUse}} 84 |
85 | 86 |
87 | 88 | 89 |
90 | 91 | {{template "footer.html" .}} 92 | -------------------------------------------------------------------------------- /samples/validation/app/views/Sample3/HandleSubmit.html: -------------------------------------------------------------------------------- 1 | {{template "header.html" .}} 2 | 3 |

User created !

4 | 5 |

6 | Desired username is {{.user.Username}} 7 |

8 |

9 | First name is {{.user.FirstName}} 10 |

11 |

12 | Last name is {{.user.LastName}} 13 |

14 |

15 | Age is {{.user.Age}} 16 |

17 |

18 | Password is {{.user.Password}} 19 |

20 |

21 | Email is {{.user.Email}} 22 |

23 | 24 | Back 25 | 26 | {{template "footer.html" .}} 27 | -------------------------------------------------------------------------------- /samples/validation/app/views/Sample4/HandleSubmit.html: -------------------------------------------------------------------------------- 1 | {{template "header.html" .}} 2 | 3 |

User created !

4 | 5 |

6 | Desired username is {{.user.Username}} 7 |

8 |

9 | First name is {{.user.FirstName}} 10 |

11 |

12 | Last name is {{.user.LastName}} 13 |

14 |

15 | Age is {{.user.Age}} 16 |

17 |

18 | Password is {{.user.Password}} 19 |

20 |

21 | Email is {{.user.Email}} 22 |

23 | 24 | Back 25 | 26 | {{template "footer.html" .}} 27 | -------------------------------------------------------------------------------- /samples/validation/app/views/footer.html: -------------------------------------------------------------------------------- 1 |
2 | Home 3 |
4 | This sample is based on the Validation sample app provided 5 | with Play! Framework. 6 |
7 | 8 | 9 | -------------------------------------------------------------------------------- /samples/validation/app/views/header.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{.title}} 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /samples/validation/conf/app.conf: -------------------------------------------------------------------------------- 1 | app.name=Sample validation 2 | app.secret=774a66ffb3d8113a2f730bbb33192251dc521ab0bee1bb0d88290299fe05c618 3 | module.static=github.com/robfig/revel/modules/static 4 | module.testrunner=github.com/robfig/revel/modules/testrunner 5 | mode.dev=true 6 | [dev] 7 | [prod] 8 | -------------------------------------------------------------------------------- /samples/validation/conf/routes: -------------------------------------------------------------------------------- 1 | # Routes 2 | # This file defines all application routes (Higher priority routes first) 3 | # ~~~~ 4 | 5 | module:testrunner 6 | 7 | GET / Application.Index 8 | 9 | # Map static resources from the app/public folder to /public 10 | GET /public/*filepath Static.Serve("public") 11 | 12 | # Catch all 13 | * /:controller/:action :controller.:action 14 | -------------------------------------------------------------------------------- /samples/validation/public/css/main.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: Arial, Sans; 3 | font-size: 90%; 4 | width: 800px; 5 | margin: 1em auto; 6 | } 7 | 8 | fieldset { 9 | border: 1px solid #888; 10 | margin-bottom: 1em; 11 | } 12 | 13 | legend { 14 | font-weight: bold; 15 | } 16 | 17 | label { 18 | display: block; 19 | width: 200px; 20 | float: left; 21 | cursor: pointer; 22 | } 23 | 24 | input[type=text], input[type=password] { 25 | border: 1px solid #aaa; 26 | float: left; 27 | margin-right: 5px; 28 | } 29 | 30 | .field { 31 | clear: both; 32 | margin-bottom: 2px; 33 | } 34 | 35 | .infos { 36 | color: #666; 37 | margin-bottom: 5px; 38 | } 39 | 40 | .error { 41 | color: #c00; 42 | } 43 | 44 | .hasError { 45 | background: pink; 46 | } 47 | 48 | .has-error { 49 | background: #ffb6c1; 50 | } 51 | -------------------------------------------------------------------------------- /samples/validation/public/images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robfig/revel/e8aac16df9e93b17d309595b4ff57405dc567fdf/samples/validation/public/images/favicon.png -------------------------------------------------------------------------------- /samples/validation/tests/apptest.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import "github.com/robfig/revel" 4 | 5 | type ApplicationTest struct { 6 | revel.TestSuite 7 | } 8 | 9 | func (t *ApplicationTest) Before() { 10 | println("Set up") 11 | } 12 | 13 | func (t *ApplicationTest) TestThatIndexPageWorks() { 14 | t.Get("/") 15 | t.AssertOk() 16 | t.AssertContentType("text/html; charset=utf-8") 17 | } 18 | 19 | func (t *ApplicationTest) After() { 20 | println("Tear down") 21 | } 22 | -------------------------------------------------------------------------------- /server_test.go: -------------------------------------------------------------------------------- 1 | package revel 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "os" 7 | "path" 8 | "strings" 9 | "testing" 10 | ) 11 | 12 | // This tries to benchmark the usual request-serving pipeline to get an overall 13 | // performance metric. 14 | // 15 | // Each iteration runs one mock request to display a hotel's detail page by id. 16 | // 17 | // Contributing parts: 18 | // - Routing 19 | // - Controller lookup / invocation 20 | // - Parameter binding 21 | // - Session, flash, i18n cookies 22 | // - Render() call magic 23 | // - Template rendering 24 | func BenchmarkServeAction(b *testing.B) { 25 | benchmarkRequest(b, showRequest) 26 | } 27 | 28 | func BenchmarkServeJson(b *testing.B) { 29 | benchmarkRequest(b, jsonRequest) 30 | } 31 | 32 | func BenchmarkServePlaintext(b *testing.B) { 33 | benchmarkRequest(b, plaintextRequest) 34 | } 35 | 36 | // This tries to benchmark the static serving overhead when serving an "average 37 | // size" 7k file. 38 | func BenchmarkServeStatic(b *testing.B) { 39 | benchmarkRequest(b, staticRequest) 40 | } 41 | 42 | func benchmarkRequest(b *testing.B, req *http.Request) { 43 | startFakeBookingApp() 44 | b.ResetTimer() 45 | resp := httptest.NewRecorder() 46 | for i := 0; i < b.N; i++ { 47 | handle(resp, req) 48 | } 49 | } 50 | 51 | // Test that the booking app can be successfully run for a test. 52 | func TestFakeServer(t *testing.T) { 53 | startFakeBookingApp() 54 | 55 | resp := httptest.NewRecorder() 56 | 57 | // First, test that the expected responses are actually generated 58 | handle(resp, showRequest) 59 | if !strings.Contains(resp.Body.String(), "300 Main St.") { 60 | t.Errorf("Failed to find hotel address in action response:\n%s", resp.Body) 61 | t.FailNow() 62 | } 63 | resp.Body.Reset() 64 | 65 | handle(resp, staticRequest) 66 | sessvarsSize := getFileSize(t, path.Join(BasePath, "public", "js", "sessvars.js")) 67 | if int64(resp.Body.Len()) != sessvarsSize { 68 | t.Errorf("Expected sessvars.js to have %d bytes, got %d:\n%s", sessvarsSize, resp.Body.Len(), resp.Body) 69 | t.FailNow() 70 | } 71 | resp.Body.Reset() 72 | 73 | handle(resp, jsonRequest) 74 | if !strings.Contains(resp.Body.String(), `"Address":"300 Main St."`) { 75 | t.Errorf("Failed to find hotel address in JSON response:\n%s", resp.Body) 76 | t.FailNow() 77 | } 78 | resp.Body.Reset() 79 | 80 | handle(resp, plaintextRequest) 81 | if resp.Body.String() != "Hello, World!" { 82 | t.Errorf("Failed to find greeting in plaintext response:\n%s", resp.Body) 83 | t.FailNow() 84 | } 85 | 86 | resp.Body = nil 87 | } 88 | 89 | func getFileSize(t *testing.T, name string) int64 { 90 | fi, err := os.Stat(name) 91 | if err != nil { 92 | t.Errorf("Unable to stat file:\n%s", name) 93 | t.FailNow() 94 | } 95 | return fi.Size() 96 | } 97 | 98 | var ( 99 | showRequest, _ = http.NewRequest("GET", "/hotels/3", nil) 100 | staticRequest, _ = http.NewRequest("GET", "/public/js/sessvars.js", nil) 101 | jsonRequest, _ = http.NewRequest("GET", "/hotels/3/booking", nil) 102 | plaintextRequest, _ = http.NewRequest("GET", "/hotels", nil) 103 | ) 104 | -------------------------------------------------------------------------------- /session_test.go: -------------------------------------------------------------------------------- 1 | package revel 2 | 3 | import ( 4 | "net/http" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | func TestSessionRestore(t *testing.T) { 10 | expireAfterDuration = 0 11 | originSession := make(Session) 12 | originSession["foo"] = "foo" 13 | originSession["bar"] = "bar" 14 | cookie := originSession.cookie() 15 | if !cookie.Expires.IsZero() { 16 | t.Error("incorrect cookie expire", cookie.Expires) 17 | } 18 | 19 | restoredSession := getSessionFromCookie(cookie) 20 | for k, v := range originSession { 21 | if restoredSession[k] != v { 22 | t.Errorf("session restore failed session[%s] != %s", k, v) 23 | } 24 | } 25 | } 26 | 27 | func TestSessionExpire(t *testing.T) { 28 | expireAfterDuration = time.Hour 29 | session := make(Session) 30 | session["user"] = "Tom" 31 | var cookie *http.Cookie 32 | for i := 0; i < 3; i++ { 33 | cookie = session.cookie() 34 | time.Sleep(time.Second) 35 | session = getSessionFromCookie(cookie) 36 | } 37 | expectExpire := time.Now().Add(expireAfterDuration) 38 | if cookie.Expires.Unix() < expectExpire.Add(-time.Second).Unix() { 39 | t.Error("expect expires", cookie.Expires, "after", expectExpire.Add(-time.Second)) 40 | } 41 | if cookie.Expires.Unix() > expectExpire.Unix() { 42 | t.Error("expect expires", cookie.Expires, "before", expectExpire) 43 | } 44 | 45 | session.SetNoExpiration() 46 | for i := 0; i < 3; i++ { 47 | cookie = session.cookie() 48 | session = getSessionFromCookie(cookie) 49 | } 50 | cookie = session.cookie() 51 | if !cookie.Expires.IsZero() { 52 | t.Error("expect cookie expires is zero") 53 | } 54 | 55 | session.SetDefaultExpiration() 56 | cookie = session.cookie() 57 | expectExpire = time.Now().Add(expireAfterDuration) 58 | if cookie.Expires.Unix() < expectExpire.Add(-time.Second).Unix() { 59 | t.Error("expect expires", cookie.Expires, "after", expectExpire.Add(-time.Second)) 60 | } 61 | if cookie.Expires.Unix() > expectExpire.Unix() { 62 | t.Error("expect expires", cookie.Expires, "before", expectExpire) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /skeleton/.gitignore: -------------------------------------------------------------------------------- 1 | test-results/ 2 | tmp/ 3 | routes/ 4 | -------------------------------------------------------------------------------- /skeleton/app/controllers/app.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import "github.com/robfig/revel" 4 | 5 | type App struct { 6 | *revel.Controller 7 | } 8 | 9 | func (c App) Index() revel.Result { 10 | return c.Render() 11 | } 12 | -------------------------------------------------------------------------------- /skeleton/app/init.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import "github.com/robfig/revel" 4 | 5 | func init() { 6 | // Filters is the default set of global filters. 7 | revel.Filters = []revel.Filter{ 8 | revel.PanicFilter, // Recover from panics and display an error page instead. 9 | revel.RouterFilter, // Use the routing table to select the right Action 10 | revel.FilterConfiguringFilter, // A hook for adding or removing per-Action filters. 11 | revel.ParamsFilter, // Parse parameters into Controller.Params. 12 | revel.SessionFilter, // Restore and write the session cookie. 13 | revel.FlashFilter, // Restore and write the flash cookie. 14 | revel.ValidationFilter, // Restore kept validation errors and save new ones from cookie. 15 | revel.I18nFilter, // Resolve the requested language 16 | HeaderFilter, // Add some security based headers 17 | revel.InterceptorFilter, // Run interceptors around the action. 18 | revel.CompressFilter, // Compress the result. 19 | revel.ActionInvoker, // Invoke the action. 20 | } 21 | 22 | // register startup functions with OnAppStart 23 | // ( order dependent ) 24 | // revel.OnAppStart(InitDB()) 25 | // revel.OnAppStart(FillCache()) 26 | } 27 | 28 | // TODO turn this into revel.HeaderFilter 29 | // should probably also have a filter for CSRF 30 | // not sure if it can go in the same filter or not 31 | var HeaderFilter = func(c *revel.Controller, fc []revel.Filter) { 32 | // Add some common security headers 33 | c.Response.Out.Header().Add("X-Frame-Options", "SAMEORIGIN") 34 | c.Response.Out.Header().Add("X-XSS-Protection", "1; mode=block") 35 | c.Response.Out.Header().Add("X-Content-Type-Options", "nosniff") 36 | 37 | fc[0](c, fc[1:]) // Execute the next filter stage. 38 | } 39 | -------------------------------------------------------------------------------- /skeleton/app/views/App/Index.html: -------------------------------------------------------------------------------- 1 | {{set . "title" "Home"}} 2 | {{template "header.html" .}} 3 | 4 |
5 |
6 |
7 |
8 |

It works!

9 |

10 |
11 |
12 |
13 |
14 | 15 |
16 |
17 |
18 | {{template "flash.html" .}} 19 |
20 |
21 |
22 | 23 | {{template "footer.html" .}} 24 | -------------------------------------------------------------------------------- /skeleton/app/views/debug.html: -------------------------------------------------------------------------------- 1 | 20 | 44 | 45 | 46 | 65 | -------------------------------------------------------------------------------- /skeleton/app/views/errors/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Not found 5 | 6 | 7 | {{if eq .RunMode "dev"}} 8 | {{template "errors/404-dev.html" .}} 9 | {{else}} 10 | {{with .Error}} 11 |

12 | {{.Title}} 13 |

14 |

15 | {{.Description}} 16 |

17 | {{end}} 18 | {{end}} 19 | 20 | 21 | -------------------------------------------------------------------------------- /skeleton/app/views/errors/500.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Application error 5 | 6 | 7 | {{if eq .RunMode "dev"}} 8 | {{template "errors/500-dev.html" .}} 9 | {{else}} 10 |

Oops, an error occured

11 |

12 | This exception has been logged. 13 |

14 | {{end}} 15 | 16 | 17 | -------------------------------------------------------------------------------- /skeleton/app/views/flash.html: -------------------------------------------------------------------------------- 1 | {{if .flash.success}} 2 |
3 | {{.flash.success}} 4 |
5 | {{end}} 6 | 7 | {{if or .errors .flash.error}} 8 |
9 | {{if .flash.error}} 10 | {{.flash.error}} 11 | {{end}} 12 |
    13 | {{range .errors}} 14 |
  • {{.}}
  • 15 | {{end}} 16 |
17 |
18 | {{end}} 19 | -------------------------------------------------------------------------------- /skeleton/app/views/footer.html: -------------------------------------------------------------------------------- 1 | {{if eq .RunMode "dev"}} 2 | {{template "debug.html" .}} 3 | {{end}} 4 | 5 | 6 | -------------------------------------------------------------------------------- /skeleton/app/views/header.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{.title}} 6 | 7 | 8 | 9 | 10 | {{range .moreStyles}} 11 | 12 | {{end}} 13 | {{range .moreScripts}} 14 | 15 | {{end}} 16 | 17 | 18 | -------------------------------------------------------------------------------- /skeleton/conf/app.conf.template: -------------------------------------------------------------------------------- 1 | app.name={{ .AppName }} 2 | app.secret={{ .Secret }} 3 | http.addr= 4 | http.port=9000 5 | http.ssl=false 6 | http.sslcert= 7 | http.sslkey= 8 | cookie.httponly=false 9 | cookie.prefix=REVEL 10 | cookie.secure=false 11 | format.date=01/02/2006 12 | format.datetime=01/02/2006 15:04 13 | results.chunked=false 14 | 15 | log.trace.prefix = "TRACE " 16 | log.info.prefix = "INFO " 17 | log.warn.prefix = "WARN " 18 | log.error.prefix = "ERROR " 19 | 20 | # The default language of this application. 21 | i18n.default_language=en 22 | 23 | module.static=github.com/robfig/revel/modules/static 24 | 25 | [dev] 26 | mode.dev=true 27 | results.pretty=true 28 | watch=true 29 | 30 | module.testrunner = github.com/robfig/revel/modules/testrunner 31 | 32 | log.trace.output = off 33 | log.info.output = stderr 34 | log.warn.output = stderr 35 | log.error.output = stderr 36 | 37 | [prod] 38 | mode.dev=false 39 | results.pretty=false 40 | watch=false 41 | 42 | module.testrunner = 43 | 44 | log.trace.output = off 45 | log.info.output = off 46 | log.warn.output = %(app.name)s.log 47 | log.error.output = %(app.name)s.log 48 | -------------------------------------------------------------------------------- /skeleton/conf/routes: -------------------------------------------------------------------------------- 1 | # Routes 2 | # This file defines all application routes (Higher priority routes first) 3 | # ~~~~ 4 | 5 | module:testrunner 6 | 7 | GET / App.Index 8 | 9 | # Ignore favicon requests 10 | GET /favicon.ico 404 11 | 12 | # Map static resources from the /app/public folder to the /public path 13 | GET /public/*filepath Static.Serve("public") 14 | 15 | # Catch all 16 | * /:controller/:action :controller.:action 17 | -------------------------------------------------------------------------------- /skeleton/messages/sample.en: -------------------------------------------------------------------------------- 1 | # Sample messages file for the English language (en) 2 | # Message file extensions should be ISO 639-1 codes (http://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) 3 | # Sections within each message file can optionally override the defaults using ISO 3166-1 alpha-2 codes (http://en.wikipedia.org/wiki/ISO_3166-1_alpha-2) 4 | # See also: 5 | # - http://www.rfc-editor.org/rfc/bcp/bcp47.txt 6 | # - http://www.w3.org/International/questions/qa-accept-lang-locales 7 | 8 | -------------------------------------------------------------------------------- /skeleton/public/img/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robfig/revel/e8aac16df9e93b17d309595b4ff57405dc567fdf/skeleton/public/img/favicon.png -------------------------------------------------------------------------------- /skeleton/public/img/glyphicons-halflings-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robfig/revel/e8aac16df9e93b17d309595b4ff57405dc567fdf/skeleton/public/img/glyphicons-halflings-white.png -------------------------------------------------------------------------------- /skeleton/public/img/glyphicons-halflings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robfig/revel/e8aac16df9e93b17d309595b4ff57405dc567fdf/skeleton/public/img/glyphicons-halflings.png -------------------------------------------------------------------------------- /skeleton/tests/apptest.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import "github.com/robfig/revel" 4 | 5 | type AppTest struct { 6 | revel.TestSuite 7 | } 8 | 9 | func (t *AppTest) Before() { 10 | println("Set up") 11 | } 12 | 13 | func (t AppTest) TestThatIndexPageWorks() { 14 | t.Get("/") 15 | t.AssertOk() 16 | t.AssertContentType("text/html; charset=utf-8") 17 | } 18 | 19 | func (t *AppTest) After() { 20 | println("Tear down") 21 | } 22 | -------------------------------------------------------------------------------- /templates/errors/403.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Forbidden 5 | 6 | 7 | {{with .Error}} 8 |

9 | {{.Title}} 10 |

11 |

12 | {{.Description}} 13 |

14 | {{end}} 15 | 16 | 17 | -------------------------------------------------------------------------------- /templates/errors/403.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "{{js .Error.Title}}", 3 | "description": "{{js .Error.Description}}" 4 | } 5 | -------------------------------------------------------------------------------- /templates/errors/403.txt: -------------------------------------------------------------------------------- 1 | {{.Error.Title}} 2 | 3 | {{.Error.Description}} 4 | -------------------------------------------------------------------------------- /templates/errors/403.xml: -------------------------------------------------------------------------------- 1 | {{.Error.Description}} 2 | -------------------------------------------------------------------------------- /templates/errors/404-dev.html: -------------------------------------------------------------------------------- 1 | 45 | 46 | 56 |
57 |

These routes have been tried, in this order :

58 |
    59 | {{range .Router.Routes}} 60 |
  1. {{pad .Method 10}}{{pad .Path 50}}{{.Action}}
  2. 61 | {{end}} 62 |
63 |
64 | -------------------------------------------------------------------------------- /templates/errors/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Not found 5 | 6 | 7 | 8 | {{if .DevMode}} 9 | 10 | {{template "errors/404-dev.html" .}} 11 | 12 | {{else}} 13 | 14 | {{with .Error}} 15 |

16 | {{.Title}} 17 |

18 |

19 | {{.Description}} 20 |

21 | {{end}} 22 | 23 | {{end}} 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /templates/errors/404.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "{{js .Error.Title}}", 3 | "description": "{{js .Error.Description}}" 4 | } 5 | -------------------------------------------------------------------------------- /templates/errors/404.txt: -------------------------------------------------------------------------------- 1 | {{.Error.Title}} 2 | 3 | {{.Error.Description}} 4 | -------------------------------------------------------------------------------- /templates/errors/404.xml: -------------------------------------------------------------------------------- 1 | {{.Error.Description}} 2 | -------------------------------------------------------------------------------- /templates/errors/500-dev.html: -------------------------------------------------------------------------------- 1 | 82 | {{with .Error}} 83 | 95 | {{if .Path}} 96 |
97 |

In {{.Path}} 98 | {{if .Line}} 99 | (around {{if .Line}}line {{.Line}}{{end}}{{if .Column}} column {{.Column}}{{end}}) 100 | {{end}} 101 |

102 | {{range .ContextSource}} 103 |
104 | {{.Line}}: 105 |
{{.Source}}
106 |
107 | {{end}} 108 |
109 | {{end}} 110 | {{if .MetaError}} 111 |
112 |

Additionally, an error occurred while handling this error.

113 |
114 | {{.MetaError}} 115 |
116 |
117 | {{end}} 118 | {{end}} 119 | -------------------------------------------------------------------------------- /templates/errors/500.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Application error 5 | 6 | 7 | {{if .DevMode}} 8 | {{template "errors/500-dev.html" .}} 9 | {{else}} 10 |

Oops, an error occured

11 |

12 | This exception has been logged. 13 |

14 | {{end}} 15 | 16 | 17 | -------------------------------------------------------------------------------- /templates/errors/500.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "{{js .Error.Title}}", 3 | "description": "{{js .Error.Description}}" 4 | } 5 | -------------------------------------------------------------------------------- /templates/errors/500.txt: -------------------------------------------------------------------------------- 1 | {{.Error.Title}} 2 | {{.Error.Description}} 3 | 4 | {{if eq .RunMode "dev"}} 5 | {{with .Error}} 6 | {{if .Path}} 7 | ---------- 8 | In {{.Path}} {{if .Line}}(around line {{.Line}}){{end}} 9 | 10 | {{range .ContextSource}} 11 | {{if .IsError}}>{{else}} {{end}} {{.Line}}: {{.Source}}{{end}} 12 | 13 | {{end}} 14 | {{end}} 15 | {{end}} 16 | -------------------------------------------------------------------------------- /templates/errors/500.xml: -------------------------------------------------------------------------------- 1 | 2 | {{.Error.Title}} 3 | {{.Error.Description}} 4 | 5 | -------------------------------------------------------------------------------- /testdata/i18n/config/test_app.conf: -------------------------------------------------------------------------------- 1 | app.name={{ .AppName }} 2 | app.secret={{ .Secret }} 3 | http.addr= 4 | http.port=9000 5 | cookie.prefix=REVEL 6 | 7 | i18n.default_language=en 8 | i18n.cookie=APP_LANG 9 | 10 | [dev] 11 | results.pretty=true 12 | results.staging=true 13 | watch=true 14 | 15 | module.testrunner = github.com/robfig/revel/modules/testrunner 16 | module.static=github.com/robfig/revel/modules/static 17 | 18 | log.trace.output = off 19 | log.info.output = stderr 20 | log.warn.output = stderr 21 | log.error.output = stderr 22 | 23 | [prod] 24 | results.pretty=false 25 | results.staging=false 26 | watch=false 27 | 28 | module.testrunner = 29 | 30 | log.trace.output = off 31 | log.info.output = off 32 | log.warn.output = %(app.name)s.log 33 | log.error.output = %(app.name)s.log 34 | -------------------------------------------------------------------------------- /testdata/i18n/messages/dutch_messages.nl: -------------------------------------------------------------------------------- 1 | greeting=Hallo 2 | greeting.name=Rob 3 | greeting.suffix=, welkom bij Revel! 4 | 5 | [NL] 6 | greeting=Goeiedag 7 | 8 | [BE] 9 | greeting=Hallokes 10 | -------------------------------------------------------------------------------- /testdata/i18n/messages/english_messages.en: -------------------------------------------------------------------------------- 1 | greeting=Hello 2 | greeting.name=Rob 3 | greeting.suffix=, welcome to Revel! 4 | 5 | folded=Greeting is '%(greeting)s' 6 | folded.arguments=%(greeting.name)s is %d years old 7 | 8 | arguments.string=My name is %s 9 | arguments.hex=The number %d in hexadecimal notation would be %x 10 | arguments.none=No arguments here son 11 | 12 | only_exists_in_default=Default 13 | 14 | [AU] 15 | greeting=G'day 16 | 17 | [US] 18 | greeting=Howdy 19 | 20 | [GB] 21 | greeting=All right -------------------------------------------------------------------------------- /testdata/i18n/messages/english_messages2.en: -------------------------------------------------------------------------------- 1 | greeting2=Yo! 2 | -------------------------------------------------------------------------------- /testdata/i18n/messages/invalid_message_file_name.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robfig/revel/e8aac16df9e93b17d309595b4ff57405dc567fdf/testdata/i18n/messages/invalid_message_file_name.txt -------------------------------------------------------------------------------- /util_test.go: -------------------------------------------------------------------------------- 1 | package revel 2 | 3 | import ( 4 | "path" 5 | "path/filepath" 6 | "reflect" 7 | "testing" 8 | ) 9 | 10 | func TestContentTypeByFilename(t *testing.T) { 11 | testCases := map[string]string{ 12 | "xyz.jpg": "image/jpeg", 13 | "helloworld.c": "text/x-c; charset=utf-8", 14 | "helloworld.": "application/octet-stream", 15 | "helloworld": "application/octet-stream", 16 | "hello.world.c": "text/x-c; charset=utf-8", 17 | } 18 | srcPath, _ := findSrcPaths(REVEL_IMPORT_PATH) 19 | ConfPaths = []string{path.Join( 20 | srcPath, 21 | filepath.FromSlash(REVEL_IMPORT_PATH), 22 | "conf"), 23 | } 24 | LoadMimeConfig() 25 | for filename, expected := range testCases { 26 | actual := ContentTypeByFilename(filename) 27 | if actual != expected { 28 | t.Errorf("%s: %s, Expected %s", filename, actual, expected) 29 | } 30 | } 31 | } 32 | 33 | func TestEqual(t *testing.T) { 34 | type testStruct struct{} 35 | type testStruct2 struct{} 36 | i, i2 := 8, 9 37 | s, s2 := "@朕µ\n\tüöäß", "@朕µ\n\tüöäss" 38 | slice, slice2 := []int{1, 2, 3, 4, 5}, []int{1, 2, 3, 4, 5} 39 | slice3, slice4 := []int{5, 4, 3, 2, 1}, []int{5, 4, 3, 2, 1} 40 | 41 | tm := map[string][]interface{}{ 42 | "slices": {slice, slice2}, 43 | "slices2": {slice3, slice4}, 44 | "types": {new(testStruct), new(testStruct)}, 45 | "types2": {new(testStruct2), new(testStruct2)}, 46 | "ints": {int(i), int8(i), int16(i), int32(i), int64(i)}, 47 | "ints2": {int(i2), int8(i2), int16(i2), int32(i2), int64(i2)}, 48 | "uints": {uint(i), uint8(i), uint16(i), uint32(i), uint64(i)}, 49 | "uints2": {uint(i2), uint8(i2), uint16(i2), uint32(i2), uint64(i2)}, 50 | "floats": {float32(i), float64(i)}, 51 | "floats2": {float32(i2), float64(i2)}, 52 | "strings": {[]byte(s), s}, 53 | "strings2": {[]byte(s2), s2}, 54 | } 55 | 56 | testRow := func(row, row2 string, expected bool) { 57 | for _, a := range tm[row] { 58 | for _, b := range tm[row2] { 59 | ok := Equal(a, b) 60 | if ok != expected { 61 | ak := reflect.TypeOf(a).Kind() 62 | bk := reflect.TypeOf(b).Kind() 63 | t.Errorf("eq(%s=%v,%s=%v) want %t got %t", ak, a, bk, b, expected, ok) 64 | } 65 | } 66 | } 67 | } 68 | 69 | testRow("slices", "slices", true) 70 | testRow("slices", "slices2", false) 71 | testRow("slices2", "slices", false) 72 | 73 | testRow("types", "types", true) 74 | testRow("types2", "types", false) 75 | testRow("types", "types2", false) 76 | 77 | testRow("ints", "ints", true) 78 | testRow("ints", "ints2", false) 79 | testRow("ints2", "ints", false) 80 | 81 | testRow("uints", "uints", true) 82 | testRow("uints2", "uints", false) 83 | testRow("uints", "uints2", false) 84 | 85 | testRow("floats", "floats", true) 86 | testRow("floats2", "floats", false) 87 | testRow("floats", "floats2", false) 88 | 89 | testRow("strings", "strings", true) 90 | testRow("strings2", "strings", false) 91 | testRow("strings", "strings2", false) 92 | } 93 | -------------------------------------------------------------------------------- /validation_test.go: -------------------------------------------------------------------------------- 1 | package revel 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "testing" 7 | ) 8 | 9 | // getRecordedCookie returns the recorded cookie from a ResponseRecorder with 10 | // the given name. It utilizes the cookie reader found in the standard library. 11 | func getRecordedCookie(recorder *httptest.ResponseRecorder, name string) (*http.Cookie, error) { 12 | r := &http.Response{Header: recorder.HeaderMap} 13 | for _, cookie := range r.Cookies() { 14 | if cookie.Name == name { 15 | return cookie, nil 16 | } 17 | } 18 | return nil, http.ErrNoCookie 19 | } 20 | 21 | func validationTester(req *Request, fn func(c *Controller)) *httptest.ResponseRecorder { 22 | recorder := httptest.NewRecorder() 23 | c := NewController(req, NewResponse(recorder)) 24 | ValidationFilter(c, []Filter{func(c *Controller, _ []Filter) { 25 | fn(c) 26 | }}) 27 | return recorder 28 | } 29 | 30 | // Test that errors are encoded into the _ERRORS cookie. 31 | func TestValidationWithError(t *testing.T) { 32 | recorder := validationTester(buildEmptyRequest(), func(c *Controller) { 33 | c.Validation.Required("") 34 | if !c.Validation.HasErrors() { 35 | t.Fatal("errors should be present") 36 | } 37 | c.Validation.Keep() 38 | }) 39 | 40 | if cookie, err := getRecordedCookie(recorder, "REVEL_ERRORS"); err != nil { 41 | t.Fatal(err) 42 | } else if cookie.MaxAge < 0 { 43 | t.Fatalf("cookie should not expire") 44 | } 45 | } 46 | 47 | // Test that no cookie is sent if errors are found, but Keep() is not called. 48 | func TestValidationNoKeep(t *testing.T) { 49 | recorder := validationTester(buildEmptyRequest(), func(c *Controller) { 50 | c.Validation.Required("") 51 | if !c.Validation.HasErrors() { 52 | t.Fatal("errors should not be present") 53 | } 54 | }) 55 | 56 | if _, err := getRecordedCookie(recorder, "REVEL_ERRORS"); err != http.ErrNoCookie { 57 | t.Fatal(err) 58 | } 59 | } 60 | 61 | // Test that a previously set _ERRORS cookie is deleted if no errors are found. 62 | func TestValidationNoKeepCookiePreviouslySet(t *testing.T) { 63 | req := buildRequestWithCookie("REVEL_ERRORS", "invalid") 64 | recorder := validationTester(req, func(c *Controller) { 65 | c.Validation.Required("success") 66 | if c.Validation.HasErrors() { 67 | t.Fatal("errors should not be present") 68 | } 69 | }) 70 | 71 | if cookie, err := getRecordedCookie(recorder, "REVEL_ERRORS"); err != nil { 72 | t.Fatal(err) 73 | } else if cookie.MaxAge >= 0 { 74 | t.Fatalf("cookie should be deleted") 75 | } 76 | } 77 | --------------------------------------------------------------------------------