diff --git a/cmd/web.go b/cmd/web.go index af92b713..90560295 100644 --- a/cmd/web.go +++ b/cmd/web.go @@ -195,9 +195,10 @@ func runWeb(*cli.Context) { r.Get("/:org/dashboard", org.Dashboard) r.Get("/:org/members", org.Members) - r.Get("/:org/teams/:team/edit", org.EditTeam) - r.Get("/:org/teams/new", org.NewTeam) r.Get("/:org/teams", org.Teams) + r.Get("/:org/teams/new", org.NewTeam) + r.Post("/:org/teams/new", bindIgnErr(auth.CreateTeamForm{}), org.NewTeamPost) + r.Get("/:org/teams/:team/edit", org.EditTeam) r.Get("/:org/settings", org.Settings) r.Post("/:org/settings", bindIgnErr(auth.OrgSettingForm{}), org.SettingsPost) diff --git a/gogs.go b/gogs.go index bef01859..df736647 100644 --- a/gogs.go +++ b/gogs.go @@ -17,7 +17,7 @@ import ( "github.com/gogits/gogs/modules/setting" ) -const APP_VER = "0.4.5.0629 Alpha" +const APP_VER = "0.4.5.0702 Alpha" func init() { runtime.GOMAXPROCS(runtime.NumCPU()) diff --git a/models/org.go b/models/org.go index 71b1efe9..2625ed42 100644 --- a/models/org.go +++ b/models/org.go @@ -5,11 +5,17 @@ package models import ( + "errors" "strings" "github.com/gogits/gogs/modules/base" ) +var ( + ErrOrgNotExist = errors.New("Organization does not exist") + ErrTeamAlreadyExist = errors.New("Team already exist") +) + // IsOrgOwner returns true if given user is in the owner team. func (org *User) IsOrgOwner(uid int64) bool { return IsOrganizationOwner(org.Id, uid) @@ -156,6 +162,13 @@ func DeleteOrganization(org *User) (err error) { return sess.Commit() } +// ___________ +// \__ ___/___ _____ _____ +// | |_/ __ \\__ \ / \ +// | |\ ___/ / __ \| Y Y \ +// |____| \___ >____ /__|_| / +// \/ \/ \/ + type AuthorizeType int const ( @@ -192,11 +205,41 @@ func (t *Team) GetMembers() (err error) { } // NewTeam creates a record of new team. +// It's caller's responsibility to assign organization ID. func NewTeam(t *Team) error { - // TODO: check if same name team of organization exists. + has, err := x.Id(t.OrgId).Get(new(User)) + if err != nil { + return err + } else if !has { + return ErrOrgNotExist + } + t.LowerName = strings.ToLower(t.Name) - _, err := x.Insert(t) - return err + has, err = x.Where("org_id=?", t.OrgId).And("lower_name=?", t.LowerName).Get(new(Team)) + if err != nil { + return err + } else if has { + return ErrTeamAlreadyExist + } + + sess := x.NewSession() + defer sess.Close() + if err = sess.Begin(); err != nil { + return err + } + + if _, err = sess.Insert(t); err != nil { + sess.Rollback() + return err + } + + // Update organization number of teams. + rawSql := "UPDATE `user` SET num_teams = num_teams + 1 WHERE id = ?" + if _, err = sess.Exec(rawSql, t.OrgId); err != nil { + sess.Rollback() + return err + } + return sess.Commit() } // UpdateTeam updates information of team. diff --git a/models/repo.go b/models/repo.go index 8eec131f..d95f8b1a 100644 --- a/models/repo.go +++ b/models/repo.go @@ -158,7 +158,7 @@ func IsRepositoryExist(u *User, repoName string) (bool, error) { } var ( - illegalEquals = []string{"raw", "install", "api", "avatar", "user", "org", "help", "stars", "issues", "pulls", "commits", "repo", "template", "admin"} + illegalEquals = []string{"raw", "install", "api", "avatar", "user", "org", "help", "stars", "issues", "pulls", "commits", "repo", "template", "admin", "new"} illegalSuffixs = []string{".git"} ) diff --git a/modules/auth/auth.go b/modules/auth/auth.go index eefb5ed6..e9b21510 100644 --- a/modules/auth/auth.go +++ b/modules/auth/auth.go @@ -7,183 +7,49 @@ package auth import ( "net/http" "reflect" - "strings" "github.com/go-martini/martini" "github.com/gogits/gogs/modules/base" - "github.com/gogits/gogs/modules/log" "github.com/gogits/gogs/modules/middleware/binding" ) -// Web form interface. -type Form interface { - Name(field string) string +type AuthenticationForm struct { + Id int64 `form:"id"` + Type int `form:"type"` + AuthName string `form:"name" binding:"Required;MaxSize(50)"` + Domain string `form:"domain"` + Host string `form:"host"` + Port int `form:"port"` + UseSSL bool `form:"usessl"` + BaseDN string `form:"base_dn"` + Attributes string `form:"attributes"` + Filter string `form:"filter"` + MsAdSA string `form:"ms_ad_sa"` + IsActived bool `form:"is_actived"` + SmtpAuth string `form:"smtpauth"` + SmtpHost string `form:"smtphost"` + SmtpPort int `form:"smtpport"` + Tls bool `form:"tls"` + AllowAutoRegister bool `form:"allowautoregister"` } -type RegisterForm struct { - UserName string `form:"username" binding:"Required;AlphaDashDot;MaxSize(30)"` - Email string `form:"email" binding:"Required;Email;MaxSize(50)"` - Password string `form:"passwd" binding:"Required;MinSize(6);MaxSize(30)"` - RetypePasswd string `form:"retypepasswd"` - LoginType string `form:"logintype"` - LoginName string `form:"loginname"` -} - -func (f *RegisterForm) Name(field string) string { +func (f *AuthenticationForm) Name(field string) string { names := map[string]string{ - "UserName": "Username", - "Email": "E-mail address", - "Password": "Password", - "RetypePasswd": "Re-type password", + "AuthName": "Authentication's name", + "Domain": "Domain name", + "Host": "Host address", + "Port": "Port Number", + "UseSSL": "Use SSL", + "BaseDN": "Base DN", + "Attributes": "Search attributes", + "Filter": "Search filter", + "MsAdSA": "Ms Ad SA", } return names[field] } -func (f *RegisterForm) Validate(errs *binding.Errors, req *http.Request, ctx martini.Context) { - data := ctx.Get(reflect.TypeOf(base.TmplData{})).Interface().(base.TmplData) - validate(errs, data, f) -} - -type LogInForm struct { - UserName string `form:"username" binding:"Required;MaxSize(35)"` - Password string `form:"passwd" binding:"Required;MinSize(6);MaxSize(30)"` - Remember bool `form:"remember"` -} - -func (f *LogInForm) Name(field string) string { - names := map[string]string{ - "UserName": "Username", - "Password": "Password", - } - return names[field] -} - -func (f *LogInForm) Validate(errs *binding.Errors, req *http.Request, ctx martini.Context) { - data := ctx.Get(reflect.TypeOf(base.TmplData{})).Interface().(base.TmplData) - validate(errs, data, f) -} - -func GetMinMaxSize(field reflect.StructField) string { - for _, rule := range strings.Split(field.Tag.Get("binding"), ";") { - if strings.HasPrefix(rule, "MinSize(") || strings.HasPrefix(rule, "MaxSize(") { - return rule[8 : len(rule)-1] - } - } - return "" -} - -func validate(errs *binding.Errors, data base.TmplData, f Form) { - if errs.Count() == 0 { - return - } else if len(errs.Overall) > 0 { - for _, err := range errs.Overall { - log.Error("%s: %v", reflect.TypeOf(f), err) - } - return - } - - data["HasError"] = true - AssignForm(f, data) - - typ := reflect.TypeOf(f) - val := reflect.ValueOf(f) - - if typ.Kind() == reflect.Ptr { - typ = typ.Elem() - val = val.Elem() - } - - for i := 0; i < typ.NumField(); i++ { - field := typ.Field(i) - - fieldName := field.Tag.Get("form") - // Allow ignored fields in the struct - if fieldName == "-" { - continue - } - - if err, ok := errs.Fields[field.Name]; ok { - data["Err_"+field.Name] = true - switch err { - case binding.BindingRequireError: - data["ErrorMsg"] = f.Name(field.Name) + " cannot be empty" - case binding.BindingAlphaDashError: - data["ErrorMsg"] = f.Name(field.Name) + " must be valid alpha or numeric or dash(-_) characters" - case binding.BindingAlphaDashDotError: - data["ErrorMsg"] = f.Name(field.Name) + " must be valid alpha or numeric or dash(-_) or dot characters" - case binding.BindingMinSizeError: - data["ErrorMsg"] = f.Name(field.Name) + " must contain at least " + GetMinMaxSize(field) + " characters" - case binding.BindingMaxSizeError: - data["ErrorMsg"] = f.Name(field.Name) + " must contain at most " + GetMinMaxSize(field) + " characters" - case binding.BindingEmailError: - data["ErrorMsg"] = f.Name(field.Name) + " is not a valid e-mail address" - case binding.BindingUrlError: - data["ErrorMsg"] = f.Name(field.Name) + " is not a valid URL" - default: - data["ErrorMsg"] = "Unknown error: " + err - } - return - } - } -} - -// AssignForm assign form values back to the template data. -func AssignForm(form interface{}, data base.TmplData) { - typ := reflect.TypeOf(form) - val := reflect.ValueOf(form) - - if typ.Kind() == reflect.Ptr { - typ = typ.Elem() - val = val.Elem() - } - - for i := 0; i < typ.NumField(); i++ { - field := typ.Field(i) - - fieldName := field.Tag.Get("form") - // Allow ignored fields in the struct - if fieldName == "-" { - continue - } - - data[fieldName] = val.Field(i).Interface() - } -} - -type InstallForm struct { - Database string `form:"database" binding:"Required"` - Host string `form:"host"` - User string `form:"user"` - Passwd string `form:"passwd"` - DatabaseName string `form:"database_name"` - SslMode string `form:"ssl_mode"` - DatabasePath string `form:"database_path"` - RepoRootPath string `form:"repo_path"` - RunUser string `form:"run_user"` - Domain string `form:"domain"` - AppUrl string `form:"app_url"` - AdminName string `form:"admin_name" binding:"Required;AlphaDashDot;MaxSize(30)"` - AdminPasswd string `form:"admin_pwd" binding:"Required;MinSize(6);MaxSize(30)"` - AdminEmail string `form:"admin_email" binding:"Required;Email;MaxSize(50)"` - SmtpHost string `form:"smtp_host"` - SmtpEmail string `form:"mailer_user"` - SmtpPasswd string `form:"mailer_pwd"` - RegisterConfirm string `form:"register_confirm"` - MailNotify string `form:"mail_notify"` -} - -func (f *InstallForm) Name(field string) string { - names := map[string]string{ - "Database": "Database name", - "AdminName": "Admin user name", - "AdminPasswd": "Admin password", - "AdminEmail": "Admin e-maill address", - } - return names[field] -} - -func (f *InstallForm) Validate(errors *binding.Errors, req *http.Request, context martini.Context) { +func (f *AuthenticationForm) Validate(errors *binding.Errors, req *http.Request, context martini.Context) { data := context.Get(reflect.TypeOf(base.TmplData{})).Interface().(base.TmplData) validate(errors, data, f) } diff --git a/modules/auth/authentication.go b/modules/auth/authentication.go deleted file mode 100644 index e9b21510..00000000 --- a/modules/auth/authentication.go +++ /dev/null @@ -1,55 +0,0 @@ -// Copyright 2014 The Gogs Authors. All rights reserved. -// Use of this source code is governed by a MIT-style -// license that can be found in the LICENSE file. - -package auth - -import ( - "net/http" - "reflect" - - "github.com/go-martini/martini" - - "github.com/gogits/gogs/modules/base" - "github.com/gogits/gogs/modules/middleware/binding" -) - -type AuthenticationForm struct { - Id int64 `form:"id"` - Type int `form:"type"` - AuthName string `form:"name" binding:"Required;MaxSize(50)"` - Domain string `form:"domain"` - Host string `form:"host"` - Port int `form:"port"` - UseSSL bool `form:"usessl"` - BaseDN string `form:"base_dn"` - Attributes string `form:"attributes"` - Filter string `form:"filter"` - MsAdSA string `form:"ms_ad_sa"` - IsActived bool `form:"is_actived"` - SmtpAuth string `form:"smtpauth"` - SmtpHost string `form:"smtphost"` - SmtpPort int `form:"smtpport"` - Tls bool `form:"tls"` - AllowAutoRegister bool `form:"allowautoregister"` -} - -func (f *AuthenticationForm) Name(field string) string { - names := map[string]string{ - "AuthName": "Authentication's name", - "Domain": "Domain name", - "Host": "Host address", - "Port": "Port Number", - "UseSSL": "Use SSL", - "BaseDN": "Base DN", - "Attributes": "Search attributes", - "Filter": "Search filter", - "MsAdSA": "Ms Ad SA", - } - return names[field] -} - -func (f *AuthenticationForm) Validate(errors *binding.Errors, req *http.Request, context martini.Context) { - data := context.Get(reflect.TypeOf(base.TmplData{})).Interface().(base.TmplData) - validate(errors, data, f) -} diff --git a/modules/auth/org.go b/modules/auth/org.go index f87d10a7..e243627e 100644 --- a/modules/auth/org.go +++ b/modules/auth/org.go @@ -14,6 +14,13 @@ import ( "github.com/gogits/gogs/modules/middleware/binding" ) +// ________ .__ __ .__ +// \_____ \_______ _________ ____ |__|____________ _/ |_|__| ____ ____ +// / | \_ __ \/ ___\__ \ / \| \___ /\__ \\ __\ |/ _ \ / \ +// / | \ | \/ /_/ > __ \| | \ |/ / / __ \| | | ( <_> ) | \ +// \_______ /__| \___ (____ /___| /__/_____ \(____ /__| |__|\____/|___| / +// \/ /_____/ \/ \/ \/ \/ \/ + type CreateOrgForm struct { OrgName string `form:"orgname" binding:"Required;AlphaDashDot;MaxSize(30)"` Email string `form:"email" binding:"Required;Email;MaxSize(50)"` @@ -55,3 +62,29 @@ func (f *OrgSettingForm) Validate(errors *binding.Errors, req *http.Request, con data := context.Get(reflect.TypeOf(base.TmplData{})).Interface().(base.TmplData) validate(errors, data, f) } + +// ___________ +// \__ ___/___ _____ _____ +// | |_/ __ \\__ \ / \ +// | |\ ___/ / __ \| Y Y \ +// |____| \___ >____ /__|_| / +// \/ \/ \/ + +type CreateTeamForm struct { + TeamName string `form:"name" binding:"Required;AlphaDashDot;MaxSize(30)"` + Description string `form:"desc" binding:"MaxSize(255)"` + Permission string `form:"permission"` +} + +func (f *CreateTeamForm) Name(field string) string { + names := map[string]string{ + "TeamName": "Team name", + "Description": "Team description", + } + return names[field] +} + +func (f *CreateTeamForm) Validate(errs *binding.Errors, req *http.Request, ctx martini.Context) { + data := ctx.Get(reflect.TypeOf(base.TmplData{})).Interface().(base.TmplData) + validate(errs, data, f) +} diff --git a/modules/auth/user.go b/modules/auth/user.go index 4a781acf..dfb969e8 100644 --- a/modules/auth/user.go +++ b/modules/auth/user.go @@ -7,6 +7,7 @@ package auth import ( "net/http" "reflect" + "strings" "github.com/go-martini/martini" @@ -19,6 +20,178 @@ import ( "github.com/gogits/gogs/modules/setting" ) +// Web form interface. +type Form interface { + Name(field string) string +} + +type RegisterForm struct { + UserName string `form:"username" binding:"Required;AlphaDashDot;MaxSize(30)"` + Email string `form:"email" binding:"Required;Email;MaxSize(50)"` + Password string `form:"passwd" binding:"Required;MinSize(6);MaxSize(30)"` + RetypePasswd string `form:"retypepasswd"` + LoginType string `form:"logintype"` + LoginName string `form:"loginname"` +} + +func (f *RegisterForm) Name(field string) string { + names := map[string]string{ + "UserName": "Username", + "Email": "E-mail address", + "Password": "Password", + "RetypePasswd": "Re-type password", + } + return names[field] +} + +func (f *RegisterForm) Validate(errs *binding.Errors, req *http.Request, ctx martini.Context) { + data := ctx.Get(reflect.TypeOf(base.TmplData{})).Interface().(base.TmplData) + validate(errs, data, f) +} + +type LogInForm struct { + UserName string `form:"username" binding:"Required;MaxSize(35)"` + Password string `form:"passwd" binding:"Required;MinSize(6);MaxSize(30)"` + Remember bool `form:"remember"` +} + +func (f *LogInForm) Name(field string) string { + names := map[string]string{ + "UserName": "Username", + "Password": "Password", + } + return names[field] +} + +func (f *LogInForm) Validate(errs *binding.Errors, req *http.Request, ctx martini.Context) { + data := ctx.Get(reflect.TypeOf(base.TmplData{})).Interface().(base.TmplData) + validate(errs, data, f) +} + +func GetMinMaxSize(field reflect.StructField) string { + for _, rule := range strings.Split(field.Tag.Get("binding"), ";") { + if strings.HasPrefix(rule, "MinSize(") || strings.HasPrefix(rule, "MaxSize(") { + return rule[8 : len(rule)-1] + } + } + return "" +} + +func validate(errs *binding.Errors, data base.TmplData, f Form) { + if errs.Count() == 0 { + return + } else if len(errs.Overall) > 0 { + for _, err := range errs.Overall { + log.Error("%s: %v", reflect.TypeOf(f), err) + } + return + } + + data["HasError"] = true + AssignForm(f, data) + + typ := reflect.TypeOf(f) + val := reflect.ValueOf(f) + + if typ.Kind() == reflect.Ptr { + typ = typ.Elem() + val = val.Elem() + } + + for i := 0; i < typ.NumField(); i++ { + field := typ.Field(i) + + fieldName := field.Tag.Get("form") + // Allow ignored fields in the struct + if fieldName == "-" { + continue + } + + if err, ok := errs.Fields[field.Name]; ok { + data["Err_"+field.Name] = true + switch err { + case binding.BindingRequireError: + data["ErrorMsg"] = f.Name(field.Name) + " cannot be empty" + case binding.BindingAlphaDashError: + data["ErrorMsg"] = f.Name(field.Name) + " must be valid alpha or numeric or dash(-_) characters" + case binding.BindingAlphaDashDotError: + data["ErrorMsg"] = f.Name(field.Name) + " must be valid alpha or numeric or dash(-_) or dot characters" + case binding.BindingMinSizeError: + data["ErrorMsg"] = f.Name(field.Name) + " must contain at least " + GetMinMaxSize(field) + " characters" + case binding.BindingMaxSizeError: + data["ErrorMsg"] = f.Name(field.Name) + " must contain at most " + GetMinMaxSize(field) + " characters" + case binding.BindingEmailError: + data["ErrorMsg"] = f.Name(field.Name) + " is not a valid e-mail address" + case binding.BindingUrlError: + data["ErrorMsg"] = f.Name(field.Name) + " is not a valid URL" + default: + data["ErrorMsg"] = "Unknown error: " + err + } + return + } + } +} + +// AssignForm assign form values back to the template data. +func AssignForm(form interface{}, data base.TmplData) { + typ := reflect.TypeOf(form) + val := reflect.ValueOf(form) + + if typ.Kind() == reflect.Ptr { + typ = typ.Elem() + val = val.Elem() + } + + for i := 0; i < typ.NumField(); i++ { + field := typ.Field(i) + + fieldName := field.Tag.Get("form") + // Allow ignored fields in the struct + if fieldName == "-" { + continue + } + + data[fieldName] = val.Field(i).Interface() + } +} + +type InstallForm struct { + Database string `form:"database" binding:"Required"` + Host string `form:"host"` + User string `form:"user"` + Passwd string `form:"passwd"` + DatabaseName string `form:"database_name"` + SslMode string `form:"ssl_mode"` + DatabasePath string `form:"database_path"` + RepoRootPath string `form:"repo_path"` + RunUser string `form:"run_user"` + Domain string `form:"domain"` + AppUrl string `form:"app_url"` + AdminName string `form:"admin_name" binding:"Required;AlphaDashDot;MaxSize(30)"` + AdminPasswd string `form:"admin_pwd" binding:"Required;MinSize(6);MaxSize(30)"` + AdminEmail string `form:"admin_email" binding:"Required;Email;MaxSize(50)"` + SmtpHost string `form:"smtp_host"` + SmtpEmail string `form:"mailer_user"` + SmtpPasswd string `form:"mailer_pwd"` + RegisterConfirm string `form:"register_confirm"` + MailNotify string `form:"mail_notify"` +} + +func (f *InstallForm) Name(field string) string { + names := map[string]string{ + "Database": "Database name", + "AdminName": "Admin user name", + "AdminPasswd": "Admin password", + "AdminEmail": "Admin e-maill address", + } + return names[field] +} + +func (f *InstallForm) Validate(errors *binding.Errors, req *http.Request, context martini.Context) { + data := context.Get(reflect.TypeOf(base.TmplData{})).Interface().(base.TmplData) + validate(errors, data, f) +} + // SignedInId returns the id of signed in user. func SignedInId(header http.Header, sess session.SessionStore) int64 { if !models.HasEngine { diff --git a/routers/org/teams.go b/routers/org/teams.go index eef6a634..49273925 100644 --- a/routers/org/teams.go +++ b/routers/org/teams.go @@ -8,12 +8,15 @@ import ( "github.com/go-martini/martini" "github.com/gogits/gogs/models" + "github.com/gogits/gogs/modules/auth" "github.com/gogits/gogs/modules/base" + "github.com/gogits/gogs/modules/log" "github.com/gogits/gogs/modules/middleware" ) const ( - TEAMS base.TplName = "org/teams" + TEAMS base.TplName = "org/teams" + TEAM_NEW base.TplName = "org/team_new" ) func Teams(ctx *middleware.Context, params martini.Params) { @@ -46,8 +49,80 @@ func Teams(ctx *middleware.Context, params martini.Params) { } func NewTeam(ctx *middleware.Context, params martini.Params) { - ctx.Data["Title"] = "Organization " + params["org"] + " New Team" - ctx.HTML(200, "org/new_team") + org, err := models.GetUserByName(params["org"]) + if err != nil { + if err == models.ErrUserNotExist { + ctx.Handle(404, "org.NewTeam(GetUserByName)", err) + } else { + ctx.Handle(500, "org.NewTeam(GetUserByName)", err) + } + return + } + ctx.Data["Org"] = org + + // Check ownership of organization. + if !org.IsOrgOwner(ctx.User.Id) { + ctx.Error(403) + return + } + + ctx.HTML(200, TEAM_NEW) +} + +func NewTeamPost(ctx *middleware.Context, params martini.Params, form auth.CreateTeamForm) { + org, err := models.GetUserByName(params["org"]) + if err != nil { + if err == models.ErrUserNotExist { + ctx.Handle(404, "org.NewTeamPost(GetUserByName)", err) + } else { + ctx.Handle(500, "org.NewTeamPost(GetUserByName)", err) + } + return + } + ctx.Data["Org"] = org + + // Check ownership of organization. + if !org.IsOrgOwner(ctx.User.Id) { + ctx.Error(403) + return + } + + if ctx.HasError() { + ctx.HTML(200, TEAM_NEW) + return + } + + // Validate permission level. + var auth models.AuthorizeType + switch form.Permission { + case "read": + auth = models.ORG_READABLE + case "write": + auth = models.ORG_WRITABLE + case "admin": + auth = models.ORG_ADMIN + default: + ctx.Error(401) + return + } + + t := &models.Team{ + OrgId: org.Id, + Name: form.TeamName, + Description: form.Description, + Authorize: auth, + } + if err = models.NewTeam(t); err != nil { + if err == models.ErrTeamAlreadyExist { + ctx.Data["Err_TeamName"] = true + ctx.RenderWithErr("Team name has already been used", TEAM_NEW, &form) + } else { + ctx.Handle(500, "org.NewTeamPost(NewTeam)", err) + } + return + } + log.Trace("%s Team created: %s/%s", ctx.Req.RequestURI, org.Name, t.Name) + ctx.Redirect("/org/" + org.LowerName + "/teams/" + t.LowerName) } func EditTeam(ctx *middleware.Context, params martini.Params) { diff --git a/templates/VERSION b/templates/VERSION index eccb7a39..8777e130 100644 --- a/templates/VERSION +++ b/templates/VERSION @@ -1 +1 @@ -0.4.5.0629 Alpha \ No newline at end of file +0.4.5.0702 Alpha \ No newline at end of file diff --git a/templates/org/new.tmpl b/templates/org/new.tmpl index bb46db4a..870f3982 100644 --- a/templates/org/new.tmpl +++ b/templates/org/new.tmpl @@ -13,7 +13,7 @@ -