* Add units concept for modulable functions of a repository * remove unused comment codes & fix lints and tests * remove unused comment codes * use struct config instead of map * fix lint * rm wrong files * fix teststags/v1.2.0-rc1
@@ -441,7 +441,7 @@ func runWeb(ctx *cli.Context) error { | |||
}, func(ctx *context.Context) { | |||
ctx.Data["PageIsSettings"] = true | |||
}) | |||
}, context.UnitTypes()) | |||
}, reqSignIn, context.RepoAssignment(), reqRepoAdmin, context.RepoRef()) | |||
m.Get("/:username/:reponame/action/:action", reqSignIn, context.RepoAssignment(), repo.Action) | |||
@@ -535,7 +535,7 @@ func runWeb(ctx *cli.Context) error { | |||
return | |||
} | |||
}) | |||
}, reqSignIn, context.RepoAssignment(), repo.MustBeNotBare) | |||
}, reqSignIn, context.RepoAssignment(), repo.MustBeNotBare, context.UnitTypes()) | |||
m.Group("/:username/:reponame", func() { | |||
m.Group("", func() { | |||
@@ -581,7 +581,7 @@ func runWeb(ctx *cli.Context) error { | |||
m.Get("/commit/:sha([a-f0-9]{7,40})\\.:ext(patch|diff)", repo.RawDiff) | |||
m.Get("/compare/:before([a-z0-9]{40})\\.\\.\\.:after([a-z0-9]{40})", repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.CompareDiff) | |||
}, ignSignIn, context.RepoAssignment(), repo.MustBeNotBare) | |||
}, ignSignIn, context.RepoAssignment(), repo.MustBeNotBare, context.UnitTypes()) | |||
m.Group("/:username/:reponame", func() { | |||
m.Get("/stars", repo.Stars) | |||
m.Get("/watchers", repo.Watchers) | |||
@@ -591,7 +591,7 @@ func runWeb(ctx *cli.Context) error { | |||
m.Group("/:reponame", func() { | |||
m.Get("", repo.SetEditorconfigIfExists, repo.Home) | |||
m.Get("\\.git$", repo.SetEditorconfigIfExists, repo.Home) | |||
}, ignSignIn, context.RepoAssignment(true), context.RepoRef()) | |||
}, ignSignIn, context.RepoAssignment(true), context.RepoRef(), context.UnitTypes()) | |||
m.Group("/:reponame", func() { | |||
m.Group("/info/lfs", func() { | |||
@@ -76,10 +76,12 @@ var migrations = []Migration{ | |||
// v13 -> v14:v0.9.87 | |||
NewMigration("set comment updated with created", setCommentUpdatedWithCreated), | |||
// v14 | |||
// v14 -> v15 | |||
NewMigration("create user column diff view style", createUserColumnDiffViewStyle), | |||
// v15 | |||
// v15 -> v16 | |||
NewMigration("create user column allow create organization", createAllowCreateOrganizationColumn), | |||
// V16 -> v17 | |||
NewMigration("create repo unit table and add units for all repos", addUnitsToTables), | |||
} | |||
// Migrate database to current version | |||
@@ -0,0 +1,117 @@ | |||
package migrations | |||
import ( | |||
"fmt" | |||
"time" | |||
"code.gitea.io/gitea/modules/markdown" | |||
"github.com/go-xorm/xorm" | |||
) | |||
// RepoUnit describes all units of a repository | |||
type RepoUnit struct { | |||
ID int64 | |||
RepoID int64 `xorm:"INDEX(s)"` | |||
Type int `xorm:"INDEX(s)"` | |||
Index int | |||
Config map[string]string `xorm:"JSON"` | |||
CreatedUnix int64 `xorm:"INDEX CREATED"` | |||
Created time.Time `xorm:"-"` | |||
} | |||
// Enumerate all the unit types | |||
const ( | |||
UnitTypeCode = iota + 1 // 1 code | |||
UnitTypeIssues // 2 issues | |||
UnitTypePRs // 3 PRs | |||
UnitTypeCommits // 4 Commits | |||
UnitTypeReleases // 5 Releases | |||
UnitTypeWiki // 6 Wiki | |||
UnitTypeSettings // 7 Settings | |||
UnitTypeExternalWiki // 8 ExternalWiki | |||
UnitTypeExternalTracker // 9 ExternalTracker | |||
) | |||
// Repo describes a repository | |||
type Repo struct { | |||
ID int64 | |||
EnableWiki, EnableExternalWiki, EnableIssues, EnableExternalTracker, EnablePulls bool | |||
ExternalWikiURL, ExternalTrackerURL, ExternalTrackerFormat, ExternalTrackerStyle string | |||
} | |||
func addUnitsToTables(x *xorm.Engine) error { | |||
var repos []Repo | |||
err := x.Table("repository").Find(&repos) | |||
if err != nil { | |||
return fmt.Errorf("Query repositories: %v", err) | |||
} | |||
sess := x.NewSession() | |||
defer sess.Close() | |||
if err := sess.Begin(); err != nil { | |||
return err | |||
} | |||
var repoUnit RepoUnit | |||
if err := sess.CreateTable(&repoUnit); err != nil { | |||
return fmt.Errorf("CreateTable RepoUnit: %v", err) | |||
} | |||
if err := sess.CreateUniques(&repoUnit); err != nil { | |||
return fmt.Errorf("CreateUniques RepoUnit: %v", err) | |||
} | |||
if err := sess.CreateIndexes(&repoUnit); err != nil { | |||
return fmt.Errorf("CreateIndexes RepoUnit: %v", err) | |||
} | |||
for _, repo := range repos { | |||
for i := 1; i <= 9; i++ { | |||
if (i == UnitTypeWiki || i == UnitTypeExternalWiki) && !repo.EnableWiki { | |||
continue | |||
} | |||
if i == UnitTypeExternalWiki && !repo.EnableExternalWiki { | |||
continue | |||
} | |||
if i == UnitTypePRs && !repo.EnablePulls { | |||
continue | |||
} | |||
if (i == UnitTypeIssues || i == UnitTypeExternalTracker) && !repo.EnableIssues { | |||
continue | |||
} | |||
if i == UnitTypeExternalTracker && !repo.EnableExternalTracker { | |||
continue | |||
} | |||
var config = make(map[string]string) | |||
switch i { | |||
case UnitTypeExternalTracker: | |||
config["ExternalTrackerURL"] = repo.ExternalTrackerURL | |||
config["ExternalTrackerFormat"] = repo.ExternalTrackerFormat | |||
if len(repo.ExternalTrackerStyle) == 0 { | |||
repo.ExternalTrackerStyle = markdown.IssueNameStyleNumeric | |||
} | |||
config["ExternalTrackerStyle"] = repo.ExternalTrackerStyle | |||
case UnitTypeExternalWiki: | |||
config["ExternalWikiURL"] = repo.ExternalWikiURL | |||
} | |||
if _, err = sess.Insert(&RepoUnit{ | |||
RepoID: repo.ID, | |||
Type: i, | |||
Index: i, | |||
Config: config, | |||
}); err != nil { | |||
return fmt.Errorf("Insert repo unit: %v", err) | |||
} | |||
} | |||
} | |||
if err := sess.Commit(); err != nil { | |||
return err | |||
} | |||
return nil | |||
} |
@@ -106,6 +106,7 @@ func init() { | |||
new(IssueUser), | |||
new(LFSMetaObject), | |||
new(TwoFactor), | |||
new(RepoUnit), | |||
) | |||
gonicNames := []string{"SSL", "UID"} | |||
@@ -200,17 +200,8 @@ type Repository struct { | |||
IsMirror bool `xorm:"INDEX"` | |||
*Mirror `xorm:"-"` | |||
// Advanced settings | |||
EnableWiki bool `xorm:"NOT NULL DEFAULT true"` | |||
EnableExternalWiki bool | |||
ExternalWikiURL string | |||
EnableIssues bool `xorm:"NOT NULL DEFAULT true"` | |||
EnableExternalTracker bool | |||
ExternalTrackerURL string | |||
ExternalTrackerFormat string | |||
ExternalTrackerStyle string | |||
ExternalMetas map[string]string `xorm:"-"` | |||
EnablePulls bool `xorm:"NOT NULL DEFAULT true"` | |||
ExternalMetas map[string]string `xorm:"-"` | |||
Units []*RepoUnit `xorm:"-"` | |||
IsFork bool `xorm:"INDEX NOT NULL DEFAULT false"` | |||
ForkID int64 `xorm:"INDEX"` | |||
@@ -247,10 +238,6 @@ func (repo *Repository) AfterSet(colName string, _ xorm.Cell) { | |||
repo.NumOpenPulls = repo.NumPulls - repo.NumClosedPulls | |||
case "num_closed_milestones": | |||
repo.NumOpenMilestones = repo.NumMilestones - repo.NumClosedMilestones | |||
case "external_tracker_style": | |||
if len(repo.ExternalTrackerStyle) == 0 { | |||
repo.ExternalTrackerStyle = markdown.IssueNameStyleNumeric | |||
} | |||
case "created_unix": | |||
repo.Created = time.Unix(repo.CreatedUnix, 0).Local() | |||
case "updated_unix": | |||
@@ -307,6 +294,72 @@ func (repo *Repository) APIFormat(mode AccessMode) *api.Repository { | |||
} | |||
} | |||
func (repo *Repository) getUnits(e Engine) (err error) { | |||
if repo.Units != nil { | |||
return nil | |||
} | |||
repo.Units, err = getUnitsByRepoID(e, repo.ID) | |||
return err | |||
} | |||
func getUnitsByRepoID(e Engine, repoID int64) (units []*RepoUnit, err error) { | |||
return units, e.Where("repo_id = ?", repoID).Find(&units) | |||
} | |||
// EnableUnit if this repository enabled some unit | |||
func (repo *Repository) EnableUnit(tp UnitType) bool { | |||
repo.getUnits(x) | |||
for _, unit := range repo.Units { | |||
if unit.Type == tp { | |||
return true | |||
} | |||
} | |||
return false | |||
} | |||
var ( | |||
// ErrUnitNotExist organization does not exist | |||
ErrUnitNotExist = errors.New("Unit does not exist") | |||
) | |||
// MustGetUnit always returns a RepoUnit object | |||
func (repo *Repository) MustGetUnit(tp UnitType) *RepoUnit { | |||
ru, err := repo.GetUnit(tp) | |||
if err == nil { | |||
return ru | |||
} | |||
if tp == UnitTypeExternalWiki { | |||
return &RepoUnit{ | |||
Type: tp, | |||
Config: new(ExternalWikiConfig), | |||
} | |||
} else if tp == UnitTypeExternalTracker { | |||
return &RepoUnit{ | |||
Type: tp, | |||
Config: new(ExternalTrackerConfig), | |||
} | |||
} | |||
return &RepoUnit{ | |||
Type: tp, | |||
Config: new(UnitConfig), | |||
} | |||
} | |||
// GetUnit returns a RepoUnit object | |||
func (repo *Repository) GetUnit(tp UnitType) (*RepoUnit, error) { | |||
if err := repo.getUnits(x); err != nil { | |||
return nil, err | |||
} | |||
for _, unit := range repo.Units { | |||
if unit.Type == tp { | |||
return unit, nil | |||
} | |||
} | |||
return nil, ErrUnitNotExist | |||
} | |||
func (repo *Repository) getOwner(e Engine) (err error) { | |||
if repo.Owner != nil { | |||
return nil | |||
@@ -334,15 +387,18 @@ func (repo *Repository) mustOwner(e Engine) *User { | |||
// ComposeMetas composes a map of metas for rendering external issue tracker URL. | |||
func (repo *Repository) ComposeMetas() map[string]string { | |||
if !repo.EnableExternalTracker { | |||
unit, err := repo.GetUnit(UnitTypeExternalTracker) | |||
if err != nil { | |||
return nil | |||
} else if repo.ExternalMetas == nil { | |||
} | |||
if repo.ExternalMetas == nil { | |||
repo.ExternalMetas = map[string]string{ | |||
"format": repo.ExternalTrackerFormat, | |||
"format": unit.ExternalTrackerConfig().ExternalTrackerFormat, | |||
"user": repo.MustOwner().Name, | |||
"repo": repo.Name, | |||
} | |||
switch repo.ExternalTrackerStyle { | |||
switch unit.ExternalTrackerConfig().ExternalTrackerStyle { | |||
case markdown.IssueNameStyleAlphanumeric: | |||
repo.ExternalMetas["style"] = markdown.IssueNameStyleAlphanumeric | |||
default: | |||
@@ -359,6 +415,8 @@ func (repo *Repository) DeleteWiki() { | |||
for _, wikiPath := range wikiPaths { | |||
RemoveAllWithNotice("Delete repository wiki", wikiPath) | |||
} | |||
x.Where("repo_id = ?", repo.ID).And("type = ?", UnitTypeWiki).Delete(new(RepoUnit)) | |||
} | |||
func (repo *Repository) getAssignees(e Engine) (_ []*User, err error) { | |||
@@ -482,7 +540,7 @@ func (repo *Repository) CanEnablePulls() bool { | |||
// AllowsPulls returns true if repository meets the requirements of accepting pulls and has them enabled. | |||
func (repo *Repository) AllowsPulls() bool { | |||
return repo.CanEnablePulls() && repo.EnablePulls | |||
return repo.CanEnablePulls() && repo.EnableUnit(UnitTypePullRequests) | |||
} | |||
// CanEnableEditor returns true if repository meets the requirements of web editor. | |||
@@ -997,6 +1055,20 @@ func createRepository(e *xorm.Session, u *User, repo *Repository) (err error) { | |||
return err | |||
} | |||
// insert units for repo | |||
var units = make([]RepoUnit, 0, len(defaultRepoUnits)) | |||
for i, tp := range defaultRepoUnits { | |||
units = append(units, RepoUnit{ | |||
RepoID: repo.ID, | |||
Type: tp, | |||
Index: i, | |||
}) | |||
} | |||
if _, err = e.Insert(&units); err != nil { | |||
return err | |||
} | |||
u.NumRepos++ | |||
// Remember visibility preference. | |||
u.LastRepoVisibility = repo.IsPrivate | |||
@@ -1035,15 +1107,12 @@ func CreateRepository(u *User, opts CreateRepoOptions) (_ *Repository, err error | |||
} | |||
repo := &Repository{ | |||
OwnerID: u.ID, | |||
Owner: u, | |||
Name: opts.Name, | |||
LowerName: strings.ToLower(opts.Name), | |||
Description: opts.Description, | |||
IsPrivate: opts.IsPrivate, | |||
EnableWiki: true, | |||
EnableIssues: true, | |||
EnablePulls: true, | |||
OwnerID: u.ID, | |||
Owner: u, | |||
Name: opts.Name, | |||
LowerName: strings.ToLower(opts.Name), | |||
Description: opts.Description, | |||
IsPrivate: opts.IsPrivate, | |||
} | |||
sess := x.NewSession() | |||
@@ -1380,6 +1449,25 @@ func UpdateRepository(repo *Repository, visibilityChanged bool) (err error) { | |||
return sess.Commit() | |||
} | |||
// UpdateRepositoryUnits updates a repository's units | |||
func UpdateRepositoryUnits(repo *Repository, units []RepoUnit) (err error) { | |||
sess := x.NewSession() | |||
defer sess.Close() | |||
if err = sess.Begin(); err != nil { | |||
return err | |||
} | |||
if _, err = sess.Where("repo_id = ?", repo.ID).Delete(new(RepoUnit)); err != nil { | |||
return err | |||
} | |||
if _, err = sess.Insert(units); err != nil { | |||
return err | |||
} | |||
return sess.Commit() | |||
} | |||
// DeleteRepository deletes a repository for a user or organization. | |||
func DeleteRepository(uid, repoID int64) error { | |||
repo := &Repository{ID: repoID, OwnerID: uid} | |||
@@ -1467,6 +1555,10 @@ func DeleteRepository(uid, repoID int64) error { | |||
return err | |||
} | |||
if _, err = sess.Where("repo_id = ?", repoID).Delete(new(RepoUnit)); err != nil { | |||
return err | |||
} | |||
if repo.IsFork { | |||
if _, err = sess.Exec("UPDATE `repository` SET num_forks=num_forks-1 WHERE id=?", repo.ForkID); err != nil { | |||
return fmt.Errorf("decrease fork count: %v", err) | |||
@@ -14,34 +14,42 @@ func TestRepo(t *testing.T) { | |||
repo.Name = "testrepo" | |||
repo.Owner = new(User) | |||
repo.Owner.Name = "testuser" | |||
repo.ExternalTrackerFormat = "https://someurl.com/{user}/{repo}/{issue}" | |||
externalTracker := RepoUnit{ | |||
Type: UnitTypeExternalTracker, | |||
Config: &ExternalTrackerConfig{ | |||
ExternalTrackerFormat: "https://someurl.com/{user}/{repo}/{issue}", | |||
}, | |||
} | |||
repo.Units = []*RepoUnit{ | |||
&externalTracker, | |||
} | |||
Convey("When no external tracker is configured", func() { | |||
Convey("It should be nil", func() { | |||
repo.EnableExternalTracker = false | |||
repo.Units = nil | |||
So(repo.ComposeMetas(), ShouldEqual, map[string]string(nil)) | |||
}) | |||
Convey("It should be nil even if other settings are present", func() { | |||
repo.EnableExternalTracker = false | |||
repo.ExternalTrackerFormat = "http://someurl.com/{user}/{repo}/{issue}" | |||
repo.ExternalTrackerStyle = markdown.IssueNameStyleNumeric | |||
repo.Units = nil | |||
So(repo.ComposeMetas(), ShouldEqual, map[string]string(nil)) | |||
}) | |||
}) | |||
Convey("When an external issue tracker is configured", func() { | |||
repo.EnableExternalTracker = true | |||
repo.Units = []*RepoUnit{ | |||
&externalTracker, | |||
} | |||
Convey("It should default to numeric issue style", func() { | |||
metas := repo.ComposeMetas() | |||
So(metas["style"], ShouldEqual, markdown.IssueNameStyleNumeric) | |||
}) | |||
Convey("It should pass through numeric issue style setting", func() { | |||
repo.ExternalTrackerStyle = markdown.IssueNameStyleNumeric | |||
externalTracker.ExternalTrackerConfig().ExternalTrackerStyle = markdown.IssueNameStyleNumeric | |||
metas := repo.ComposeMetas() | |||
So(metas["style"], ShouldEqual, markdown.IssueNameStyleNumeric) | |||
}) | |||
Convey("It should pass through alphanumeric issue style setting", func() { | |||
repo.ExternalTrackerStyle = markdown.IssueNameStyleAlphanumeric | |||
externalTracker.ExternalTrackerConfig().ExternalTrackerStyle = markdown.IssueNameStyleAlphanumeric | |||
metas := repo.ComposeMetas() | |||
So(metas["style"], ShouldEqual, markdown.IssueNameStyleAlphanumeric) | |||
}) | |||
@@ -0,0 +1,137 @@ | |||
// Copyright 2017 The Gitea 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 models | |||
import ( | |||
"encoding/json" | |||
"time" | |||
"github.com/Unknwon/com" | |||
"github.com/go-xorm/core" | |||
"github.com/go-xorm/xorm" | |||
) | |||
// RepoUnit describes all units of a repository | |||
type RepoUnit struct { | |||
ID int64 | |||
RepoID int64 `xorm:"INDEX(s)"` | |||
Type UnitType `xorm:"INDEX(s)"` | |||
Index int | |||
Config core.Conversion `xorm:"TEXT"` | |||
CreatedUnix int64 `xorm:"INDEX CREATED"` | |||
Created time.Time `xorm:"-"` | |||
} | |||
// UnitConfig describes common unit config | |||
type UnitConfig struct { | |||
} | |||
// FromDB fills up a UnitConfig from serialized format. | |||
func (cfg *UnitConfig) FromDB(bs []byte) error { | |||
return json.Unmarshal(bs, &cfg) | |||
} | |||
// ToDB exports a UnitConfig to a serialized format. | |||
func (cfg *UnitConfig) ToDB() ([]byte, error) { | |||
return json.Marshal(cfg) | |||
} | |||
// ExternalWikiConfig describes external wiki config | |||
type ExternalWikiConfig struct { | |||
ExternalWikiURL string | |||
} | |||
// FromDB fills up a ExternalWikiConfig from serialized format. | |||
func (cfg *ExternalWikiConfig) FromDB(bs []byte) error { | |||
return json.Unmarshal(bs, &cfg) | |||
} | |||
// ToDB exports a ExternalWikiConfig to a serialized format. | |||
func (cfg *ExternalWikiConfig) ToDB() ([]byte, error) { | |||
return json.Marshal(cfg) | |||
} | |||
// ExternalTrackerConfig describes external tracker config | |||
type ExternalTrackerConfig struct { | |||
ExternalTrackerURL string | |||
ExternalTrackerFormat string | |||
ExternalTrackerStyle string | |||
} | |||
// FromDB fills up a ExternalTrackerConfig from serialized format. | |||
func (cfg *ExternalTrackerConfig) FromDB(bs []byte) error { | |||
return json.Unmarshal(bs, &cfg) | |||
} | |||
// ToDB exports a ExternalTrackerConfig to a serialized format. | |||
func (cfg *ExternalTrackerConfig) ToDB() ([]byte, error) { | |||
return json.Marshal(cfg) | |||
} | |||
// BeforeSet is invoked from XORM before setting the value of a field of this object. | |||
func (r *RepoUnit) BeforeSet(colName string, val xorm.Cell) { | |||
switch colName { | |||
case "type": | |||
switch UnitType(Cell2Int64(val)) { | |||
case UnitTypeCode, UnitTypeIssues, UnitTypePullRequests, UnitTypeCommits, UnitTypeReleases, | |||
UnitTypeWiki, UnitTypeSettings: | |||
r.Config = new(UnitConfig) | |||
case UnitTypeExternalWiki: | |||
r.Config = new(ExternalWikiConfig) | |||
case UnitTypeExternalTracker: | |||
r.Config = new(ExternalTrackerConfig) | |||
default: | |||
panic("unrecognized repo unit type: " + com.ToStr(*val)) | |||
} | |||
} | |||
} | |||
// AfterSet is invoked from XORM after setting the value of a field of this object. | |||
func (r *RepoUnit) AfterSet(colName string, _ xorm.Cell) { | |||
switch colName { | |||
case "created_unix": | |||
r.Created = time.Unix(r.CreatedUnix, 0).Local() | |||
} | |||
} | |||
// Unit returns Unit | |||
func (r *RepoUnit) Unit() Unit { | |||
return Units[r.Type] | |||
} | |||
// CodeConfig returns config for UnitTypeCode | |||
func (r *RepoUnit) CodeConfig() *UnitConfig { | |||
return r.Config.(*UnitConfig) | |||
} | |||
// IssuesConfig returns config for UnitTypeIssues | |||
func (r *RepoUnit) IssuesConfig() *UnitConfig { | |||
return r.Config.(*UnitConfig) | |||
} | |||
// PullRequestsConfig returns config for UnitTypePullRequests | |||
func (r *RepoUnit) PullRequestsConfig() *UnitConfig { | |||
return r.Config.(*UnitConfig) | |||
} | |||
// CommitsConfig returns config for UnitTypeCommits | |||
func (r *RepoUnit) CommitsConfig() *UnitConfig { | |||
return r.Config.(*UnitConfig) | |||
} | |||
// ReleasesConfig returns config for UnitTypeReleases | |||
func (r *RepoUnit) ReleasesConfig() *UnitConfig { | |||
return r.Config.(*UnitConfig) | |||
} | |||
// ExternalWikiConfig returns config for UnitTypeExternalWiki | |||
func (r *RepoUnit) ExternalWikiConfig() *ExternalWikiConfig { | |||
return r.Config.(*ExternalWikiConfig) | |||
} | |||
// ExternalTrackerConfig returns config for UnitTypeExternalTracker | |||
func (r *RepoUnit) ExternalTrackerConfig() *ExternalTrackerConfig { | |||
return r.Config.(*ExternalTrackerConfig) | |||
} |
@@ -0,0 +1,137 @@ | |||
// Copyright 2017 The Gitea 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 models | |||
// UnitType is Unit's Type | |||
type UnitType int | |||
// Enumerate all the unit types | |||
const ( | |||
UnitTypeCode UnitType = iota + 1 // 1 code | |||
UnitTypeIssues // 2 issues | |||
UnitTypePullRequests // 3 PRs | |||
UnitTypeCommits // 4 Commits | |||
UnitTypeReleases // 5 Releases | |||
UnitTypeWiki // 6 Wiki | |||
UnitTypeSettings // 7 Settings | |||
UnitTypeExternalWiki // 8 ExternalWiki | |||
UnitTypeExternalTracker // 9 ExternalTracker | |||
) | |||
// Unit is a tab page of one repository | |||
type Unit struct { | |||
Type UnitType | |||
NameKey string | |||
URI string | |||
DescKey string | |||
Idx int | |||
} | |||
// Enumerate all the units | |||
var ( | |||
UnitCode = Unit{ | |||
UnitTypeCode, | |||
"repo.code", | |||
"/", | |||
"repo.code_desc", | |||
0, | |||
} | |||
UnitIssues = Unit{ | |||
UnitTypeIssues, | |||
"repo.issues", | |||
"/issues", | |||
"repo.issues_desc", | |||
1, | |||
} | |||
UnitExternalTracker = Unit{ | |||
UnitTypeExternalTracker, | |||
"repo.issues", | |||
"/issues", | |||
"repo.issues_desc", | |||
1, | |||
} | |||
UnitPullRequests = Unit{ | |||
UnitTypePullRequests, | |||
"repo.pulls", | |||
"/pulls", | |||
"repo.pulls_desc", | |||
2, | |||
} | |||
UnitCommits = Unit{ | |||
UnitTypeCommits, | |||
"repo.commits", | |||
"/commits/master", | |||
"repo.commits_desc", | |||
3, | |||
} | |||
UnitReleases = Unit{ | |||
UnitTypeReleases, | |||
"repo.releases", | |||
"/releases", | |||
"repo.releases_desc", | |||
4, | |||
} | |||
UnitWiki = Unit{ | |||
UnitTypeWiki, | |||
"repo.wiki", | |||
"/wiki", | |||
"repo.wiki_desc", | |||
5, | |||
} | |||
UnitExternalWiki = Unit{ | |||
UnitTypeExternalWiki, | |||
"repo.wiki", | |||
"/wiki", | |||
"repo.wiki_desc", | |||
5, | |||
} | |||
UnitSettings = Unit{ | |||
UnitTypeSettings, | |||
"repo.settings", | |||
"/settings", | |||
"repo.settings_desc", | |||
6, | |||
} | |||
// defaultRepoUnits contains all the default unit types | |||
defaultRepoUnits = []UnitType{ | |||
UnitTypeCode, | |||
UnitTypeIssues, | |||
UnitTypePullRequests, | |||
UnitTypeCommits, | |||
UnitTypeReleases, | |||
UnitTypeWiki, | |||
UnitTypeSettings, | |||
} | |||
// MustRepoUnits contains the units could be disabled currently | |||
MustRepoUnits = []UnitType{ | |||
UnitTypeCode, | |||
UnitTypeCommits, | |||
UnitTypeReleases, | |||
UnitTypeSettings, | |||
} | |||
// Units contains all the units | |||
Units = map[UnitType]Unit{ | |||
UnitTypeCode: UnitCode, | |||
UnitTypeIssues: UnitIssues, | |||
UnitTypeExternalTracker: UnitExternalTracker, | |||
UnitTypePullRequests: UnitPullRequests, | |||
UnitTypeCommits: UnitCommits, | |||
UnitTypeReleases: UnitReleases, | |||
UnitTypeWiki: UnitWiki, | |||
UnitTypeExternalWiki: UnitExternalWiki, | |||
UnitTypeSettings: UnitSettings, | |||
} | |||
) |
@@ -477,3 +477,18 @@ func GitHookService() macaron.Handler { | |||
} | |||
} | |||
} | |||
// UnitTypes returns a macaron middleware to set unit types to context variables. | |||
func UnitTypes() macaron.Handler { | |||
return func(ctx *Context) { | |||
ctx.Data["UnitTypeCode"] = models.UnitTypeCode | |||
ctx.Data["UnitTypeIssues"] = models.UnitTypeIssues | |||
ctx.Data["UnitTypePullRequests"] = models.UnitTypePullRequests | |||
ctx.Data["UnitTypeCommits"] = models.UnitTypeCommits | |||
ctx.Data["UnitTypeReleases"] = models.UnitTypeReleases | |||
ctx.Data["UnitTypeWiki"] = models.UnitTypeWiki | |||
ctx.Data["UnitTypeSettings"] = models.UnitTypeSettings | |||
ctx.Data["UnitTypeExternalWiki"] = models.UnitTypeExternalWiki | |||
ctx.Data["UnitTypeExternalTracker"] = models.UnitTypeExternalTracker | |||
} | |||
} |
@@ -205,7 +205,7 @@ func orgAssignment(args ...bool) macaron.Handler { | |||
} | |||
func mustEnableIssues(ctx *context.APIContext) { | |||
if !ctx.Repo.Repository.EnableIssues || ctx.Repo.Repository.EnableExternalTracker { | |||
if !ctx.Repo.Repository.EnableUnit(models.UnitTypeIssues) { | |||
ctx.Status(404) | |||
return | |||
} | |||
@@ -59,13 +59,15 @@ var ( | |||
// MustEnableIssues check if repository enable internal issues | |||
func MustEnableIssues(ctx *context.Context) { | |||
if !ctx.Repo.Repository.EnableIssues { | |||
if !ctx.Repo.Repository.EnableUnit(models.UnitTypeIssues) && | |||
!ctx.Repo.Repository.EnableUnit(models.UnitTypeExternalTracker) { | |||
ctx.Handle(404, "MustEnableIssues", nil) | |||
return | |||
} | |||
if ctx.Repo.Repository.EnableExternalTracker { | |||
ctx.Redirect(ctx.Repo.Repository.ExternalTrackerURL) | |||
unit, err := ctx.Repo.Repository.GetUnit(models.UnitTypeExternalTracker) | |||
if err == nil { | |||
ctx.Redirect(unit.ExternalTrackerConfig().ExternalTrackerURL) | |||
return | |||
} | |||
} | |||
@@ -143,18 +143,70 @@ func SettingsPost(ctx *context.Context, form auth.RepoSettingForm) { | |||
ctx.Redirect(repo.Link() + "/settings") | |||
case "advanced": | |||
repo.EnableWiki = form.EnableWiki | |||
repo.EnableExternalWiki = form.EnableExternalWiki | |||
repo.ExternalWikiURL = form.ExternalWikiURL | |||
repo.EnableIssues = form.EnableIssues | |||
repo.EnableExternalTracker = form.EnableExternalTracker | |||
repo.ExternalTrackerURL = form.ExternalTrackerURL | |||
repo.ExternalTrackerFormat = form.TrackerURLFormat | |||
repo.ExternalTrackerStyle = form.TrackerIssueStyle | |||
repo.EnablePulls = form.EnablePulls | |||
if err := models.UpdateRepository(repo, false); err != nil { | |||
ctx.Handle(500, "UpdateRepository", err) | |||
var units []models.RepoUnit | |||
for _, tp := range models.MustRepoUnits { | |||
units = append(units, models.RepoUnit{ | |||
RepoID: repo.ID, | |||
Type: tp, | |||
Index: int(tp), | |||
Config: new(models.UnitConfig), | |||
}) | |||
} | |||
if form.EnableWiki { | |||
if form.EnableExternalWiki { | |||
units = append(units, models.RepoUnit{ | |||
RepoID: repo.ID, | |||
Type: models.UnitTypeExternalWiki, | |||
Index: int(models.UnitTypeExternalWiki), | |||
Config: &models.ExternalWikiConfig{ | |||
ExternalWikiURL: form.ExternalWikiURL, | |||
}, | |||
}) | |||
} else { | |||
units = append(units, models.RepoUnit{ | |||
RepoID: repo.ID, | |||
Type: models.UnitTypeWiki, | |||
Index: int(models.UnitTypeWiki), | |||
Config: new(models.UnitConfig), | |||
}) | |||
} | |||
} | |||
if form.EnableIssues { | |||
if form.EnableExternalTracker { | |||
units = append(units, models.RepoUnit{ | |||
RepoID: repo.ID, | |||
Type: models.UnitTypeExternalWiki, | |||
Index: int(models.UnitTypeExternalWiki), | |||
Config: &models.ExternalTrackerConfig{ | |||
ExternalTrackerURL: form.ExternalTrackerURL, | |||
ExternalTrackerFormat: form.TrackerURLFormat, | |||
ExternalTrackerStyle: form.TrackerIssueStyle, | |||
}, | |||
}) | |||
} else { | |||
units = append(units, models.RepoUnit{ | |||
RepoID: repo.ID, | |||
Type: models.UnitTypeIssues, | |||
Index: int(models.UnitTypeIssues), | |||
Config: new(models.UnitConfig), | |||
}) | |||
} | |||
} | |||
if form.EnablePulls { | |||
units = append(units, models.RepoUnit{ | |||
RepoID: repo.ID, | |||
Type: models.UnitTypePullRequests, | |||
Index: int(models.UnitTypePullRequests), | |||
Config: new(models.UnitConfig), | |||
}) | |||
} | |||
if err := models.UpdateRepositoryUnits(repo, units); err != nil { | |||
ctx.Handle(500, "UpdateRepositoryUnits", err) | |||
return | |||
} | |||
log.Trace("Repository advanced settings updated: %s/%s", ctx.Repo.Owner.Name, repo.Name) | |||
@@ -281,12 +333,6 @@ func SettingsPost(ctx *context.Context, form auth.RepoSettingForm) { | |||
repo.DeleteWiki() | |||
log.Trace("Repository wiki deleted: %s/%s", ctx.Repo.Owner.Name, repo.Name) | |||
repo.EnableWiki = false | |||
if err := models.UpdateRepository(repo, false); err != nil { | |||
ctx.Handle(500, "UpdateRepository", err) | |||
return | |||
} | |||
ctx.Flash.Success(ctx.Tr("repo.settings.wiki_deletion_success")) | |||
ctx.Redirect(ctx.Repo.RepoLink + "/settings") | |||
@@ -27,13 +27,15 @@ const ( | |||
// MustEnableWiki check if wiki is enabled, if external then redirect | |||
func MustEnableWiki(ctx *context.Context) { | |||
if !ctx.Repo.Repository.EnableWiki { | |||
if !ctx.Repo.Repository.EnableUnit(models.UnitTypeWiki) && | |||
!ctx.Repo.Repository.EnableUnit(models.UnitTypeExternalWiki) { | |||
ctx.Handle(404, "MustEnableWiki", nil) | |||
return | |||
} | |||
if ctx.Repo.Repository.EnableExternalWiki { | |||
ctx.Redirect(ctx.Repo.Repository.ExternalWikiURL) | |||
unit, err := ctx.Repo.Repository.GetUnit(models.UnitTypeExternalWiki) | |||
if err == nil { | |||
ctx.Redirect(unit.ExternalWikiConfig().ExternalWikiURL) | |||
return | |||
} | |||
} | |||
@@ -236,7 +236,7 @@ func Issues(ctx *context.Context) { | |||
for _, repo := range repos { | |||
if (isPullList && repo.NumPulls == 0) || | |||
(!isPullList && | |||
(!repo.EnableIssues || repo.EnableExternalTracker || repo.NumIssues == 0)) { | |||
(!repo.EnableUnit(models.UnitTypeIssues) || repo.NumIssues == 0)) { | |||
continue | |||
} | |||
@@ -49,30 +49,48 @@ | |||
{{if not (or .IsBareRepo .IsDiffCompare)}} | |||
<div class="ui tabs container"> | |||
<div class="ui tabular menu navbar"> | |||
{{if .Repository.EnableUnit $.UnitTypeCode}} | |||
<a class="{{if .PageIsViewCode}}active{{end}} item" href="{{.RepoLink}}"> | |||
<i class="octicon octicon-code"></i> {{.i18n.Tr "repo.code"}} | |||
</a> | |||
{{if .Repository.EnableIssues}} | |||
{{end}} | |||
{{if .Repository.EnableUnit $.UnitTypeIssues}} | |||
<a class="{{if .PageIsIssueList}}active{{end}} item" href="{{.RepoLink}}/issues"> | |||
<i class="octicon octicon-issue-opened"></i> {{.i18n.Tr "repo.issues"}} <span class="ui {{if not .Repository.NumOpenIssues}}gray{{else}}blue{{end}} small label">{{.Repository.NumOpenIssues}}</span> | |||
</a> | |||
{{end}} | |||
{{if .Repository.EnableUnit $.UnitTypeExternalTracker}} | |||
<a class="{{if .PageIsIssueList}}active{{end}} item" href="{{.RepoLink}}/issues"> | |||
<i class="octicon octicon-issue-opened"></i> {{.i18n.Tr "repo.issues"}} {{if not .Repository.EnableExternalTracker}}<span class="ui {{if not .Repository.NumOpenIssues}}gray{{else}}blue{{end}} small label">{{.Repository.NumOpenIssues}}{{end}}</span> | |||
<i class="octicon octicon-issue-opened"></i> {{.i18n.Tr "repo.issues"}} </span> | |||
</a> | |||
{{end}} | |||
{{if .Repository.AllowsPulls}} | |||
<a class="{{if .PageIsPullList}}active{{end}} item" href="{{.RepoLink}}/pulls"> | |||
<i class="octicon octicon-git-pull-request"></i> {{.i18n.Tr "repo.pulls"}} <span class="ui {{if not .Repository.NumOpenPulls}}gray{{else}}blue{{end}} small label">{{.Repository.NumOpenPulls}}</span> | |||
</a> | |||
{{end}} | |||
{{if .Repository.EnableUnit $.UnitTypeCommits}} | |||
<a class="{{if (or (.PageIsCommits) (.PageIsDiff))}}active{{end}} item" href="{{.RepoLink}}/commits/{{EscapePound .BranchName}}"> | |||
<i class="octicon octicon-history"></i> {{.i18n.Tr "repo.commits"}} <span class="ui {{if not .CommitsCount}}gray{{else}}blue{{end}} small label">{{.CommitsCount}}</span> | |||
</a> | |||
{{end}} | |||
{{if .Repository.EnableUnit $.UnitTypeReleases}} | |||
<a class="{{if .PageIsReleaseList}}active{{end}} item" href="{{.RepoLink}}/releases"> | |||
<i class="octicon octicon-tag"></i> {{.i18n.Tr "repo.releases"}} <span class="ui {{if not .Repository.NumTags}}gray{{else}}blue{{end}} small label">{{.Repository.NumTags}}</span> | |||
</a> | |||
{{if .Repository.EnableWiki}} | |||
{{end}} | |||
{{if or (.Repository.EnableUnit $.UnitTypeWiki) (.Repository.EnableUnit $.UnitTypeExternalWiki)}} | |||
<a class="{{if .PageIsWiki}}active{{end}} item" href="{{.RepoLink}}/wiki"> | |||
<i class="octicon octicon-book"></i> {{.i18n.Tr "repo.wiki"}} | |||
</a> | |||
{{end}} | |||
{{if .IsRepositoryAdmin}} | |||
<div class="right menu"> | |||
<a class="{{if .PageIsSettings}}active{{end}} item" href="{{.RepoLink}}/settings"> | |||
@@ -114,26 +114,26 @@ | |||
<div class="inline field"> | |||
<label>{{.i18n.Tr "repo.wiki"}}</label> | |||
<div class="ui checkbox"> | |||
<input class="enable-system" name="enable_wiki" type="checkbox" data-target="#wiki_box" {{if .Repository.EnableWiki}}checked{{end}}> | |||
<input class="enable-system" name="enable_wiki" type="checkbox" data-target="#wiki_box" {{if or (.Repository.EnableUnit $.UnitTypeWiki) (.Repository.EnableUnit $.UnitTypeExternalWiki)}}checked{{end}}> | |||
<label>{{.i18n.Tr "repo.settings.wiki_desc"}}</label> | |||
</div> | |||
</div> | |||
<div class="field {{if not .Repository.EnableWiki}}disabled{{end}}" id="wiki_box"> | |||
<div class="field {{if not (.Repository.EnableUnit $.UnitTypeWiki)}}disabled{{end}}" id="wiki_box"> | |||
<div class="field"> | |||
<div class="ui radio checkbox"> | |||
<input class="hidden enable-system-radio" tabindex="0" name="enable_external_wiki" type="radio" value="false" data-target="#external_wiki_box" {{if not .Repository.EnableExternalWiki}}checked{{end}}/> | |||
<input class="hidden enable-system-radio" tabindex="0" name="enable_external_wiki" type="radio" value="false" data-target="#external_wiki_box" {{if not (.Repository.EnableUnit $.UnitTypeExternalWiki)}}checked{{end}}/> | |||
<label>{{.i18n.Tr "repo.settings.use_internal_wiki"}}</label> | |||
</div> | |||
</div> | |||
<div class="field"> | |||
<div class="ui radio checkbox"> | |||
<input class="hidden enable-system-radio" tabindex="0" name="enable_external_wiki" type="radio" value="true" data-target="#external_wiki_box" {{if .Repository.EnableExternalWiki}}checked{{end}}/> | |||
<input class="hidden enable-system-radio" tabindex="0" name="enable_external_wiki" type="radio" value="true" data-target="#external_wiki_box" {{if .Repository.EnableUnit $.UnitTypeExternalWiki}}checked{{end}}/> | |||
<label>{{.i18n.Tr "repo.settings.use_external_wiki"}}</label> | |||
</div> | |||
</div> | |||
<div class="field {{if not .Repository.EnableExternalWiki}}disabled{{end}}" id="external_wiki_box"> | |||
<div class="field {{if not (.Repository.EnableUnit $.UnitTypeExternalWiki)}}disabled{{end}}" id="external_wiki_box"> | |||
<label for="external_wiki_url">{{.i18n.Tr "repo.settings.external_wiki_url"}}</label> | |||
<input id="external_wiki_url" name="external_wiki_url" type="url" value="{{.Repository.ExternalWikiURL}}"> | |||
<input id="external_wiki_url" name="external_wiki_url" type="url" value="{{(.Repository.MustGetUnit $.UnitTypeExternalWiki).ExternalWikiConfig.ExternalWikiURL}}"> | |||
<p class="help">{{.i18n.Tr "repo.settings.external_wiki_url_desc"}}</p> | |||
</div> | |||
</div> | |||
@@ -143,45 +143,47 @@ | |||
<div class="inline field"> | |||
<label>{{.i18n.Tr "repo.issues"}}</label> | |||
<div class="ui checkbox"> | |||
<input class="enable-system" name="enable_issues" type="checkbox" data-target="#issue_box" {{if .Repository.EnableIssues}}checked{{end}}> | |||
<input class="enable-system" name="enable_issues" type="checkbox" data-target="#issue_box" {{if or (.Repository.EnableUnit $.UnitTypeIssues) (.Repository.EnableUnit $.UnitTypeExternalTracker)}}checked{{end}}> | |||
<label>{{.i18n.Tr "repo.settings.issues_desc"}}</label> | |||
</div> | |||
</div> | |||
<div class="field {{if not .Repository.EnableIssues}}disabled{{end}}" id="issue_box"> | |||
<div class="field {{if not (.Repository.EnableUnit $.UnitTypeIssues)}}disabled{{end}}" id="issue_box"> | |||
<div class="field"> | |||
<div class="ui radio checkbox"> | |||
<input class="hidden enable-system-radio" tabindex="0" name="enable_external_tracker" type="radio" value="false" data-target="#external_issue_box" {{if not .Repository.EnableExternalTracker}}checked{{end}}/> | |||
<input class="hidden enable-system-radio" tabindex="0" name="enable_external_tracker" type="radio" value="false" data-target="#external_issue_box" {{if not (.Repository.EnableUnit $.UnitTypeExternalTracker)}}checked{{end}}/> | |||
<label>{{.i18n.Tr "repo.settings.use_internal_issue_tracker"}}</label> | |||
</div> | |||
</div> | |||
<div class="field"> | |||
<div class="ui radio checkbox"> | |||
<input class="hidden enable-system-radio" tabindex="0" name="enable_external_tracker" type="radio" value="true" data-target="#external_issue_box" {{if .Repository.EnableExternalTracker}}checked{{end}}/> | |||
<input class="hidden enable-system-radio" tabindex="0" name="enable_external_tracker" type="radio" value="true" data-target="#external_issue_box" {{if .Repository.EnableUnit $.UnitTypeExternalTracker}}checked{{end}}/> | |||
<label>{{.i18n.Tr "repo.settings.use_external_issue_tracker"}}</label> | |||
</div> | |||
</div> | |||
<div class="field {{if not .Repository.EnableExternalTracker}}disabled{{end}}" id="external_issue_box"> | |||
<div class="field {{if not (.Repository.EnableUnit $.UnitTypeExternalTracker)}}disabled{{end}}" id="external_issue_box"> | |||
<div class="field"> | |||
<label for="external_tracker_url">{{.i18n.Tr "repo.settings.external_tracker_url"}}</label> | |||
<input id="external_tracker_url" name="external_tracker_url" type="url" value="{{.Repository.ExternalTrackerURL}}"> | |||
<input id="external_tracker_url" name="external_tracker_url" type="url" value="{{(.Repository.MustGetUnit $.UnitTypeExternalTracker).ExternalTrackerConfig.ExternalTrackerURL}}"> | |||
<p class="help">{{.i18n.Tr "repo.settings.external_tracker_url_desc"}}</p> | |||
</div> | |||
<div class="field"> | |||
<label for="tracker_url_format">{{.i18n.Tr "repo.settings.tracker_url_format"}}</label> | |||
<input id="tracker_url_format" name="tracker_url_format" type="url" value="{{.Repository.ExternalTrackerFormat}}" placeholder="e.g. https://github.com/{user}/{repo}/issues/{index}"> | |||
<input id="tracker_url_format" name="tracker_url_format" type="url" value="{{(.Repository.MustGetUnit $.UnitTypeExternalTracker).ExternalTrackerConfig.ExternalTrackerFormat}}" placeholder="e.g. https://github.com/{user}/{repo}/issues/{index}"> | |||
<p class="help">{{.i18n.Tr "repo.settings.tracker_url_format_desc" | Str2html}}</p> | |||
</div> | |||
<div class="inline fields"> | |||
<label for="issue_style">{{.i18n.Tr "repo.settings.tracker_issue_style"}}</label> | |||
<div class="field"> | |||
<div class="ui radio checkbox"> | |||
<input class="hidden" tabindex="0" name="tracker_issue_style" type="radio" value="numeric" {{if eq .Repository.ExternalTrackerStyle "numeric"}}checked=""{{end}}/> | |||
{{$externalTracker := (.Repository.MustGetUnit $.UnitTypeExternalTracker)}} | |||
{{$externalTrackerStyle := $externalTracker.ExternalTrackerConfig.ExternalTrackerStyle}} | |||
<input class="hidden" tabindex="0" name="tracker_issue_style" type="radio" value="numeric" {{if $externalTrackerStyle}}{{if eq $externalTrackerStyle "numeric"}}checked=""{{end}}{{end}}/> | |||
<label>{{.i18n.Tr "repo.settings.tracker_issue_style.numeric"}} <span class="ui light grey text">(#1234)</span></label> | |||
</div> | |||
</div> | |||
<div class="field"> | |||
<div class="ui radio checkbox"> | |||
<input class="hidden" tabindex="0" name="tracker_issue_style" type="radio" value="alphanumeric" {{if eq .Repository.ExternalTrackerStyle "alphanumeric"}}checked=""{{end}}/> | |||
<input class="hidden" tabindex="0" name="tracker_issue_style" type="radio" value="alphanumeric" {{if $externalTrackerStyle}}{{if eq $externalTracker.ExternalTrackerConfig.ExternalTrackerStyle "alphanumeric"}}checked=""{{end}}{{end}} /> | |||
<label>{{.i18n.Tr "repo.settings.tracker_issue_style.alphanumeric"}} <span class="ui light grey text">(ABC-123, DEFG-234)</span></label> | |||
</div> | |||
</div> | |||
@@ -195,7 +197,7 @@ | |||
<div class="inline field"> | |||
<label>{{.i18n.Tr "repo.pulls"}}</label> | |||
<div class="ui checkbox"> | |||
<input name="enable_pulls" type="checkbox" {{if .Repository.EnablePulls}}checked{{end}}> | |||
<input name="enable_pulls" type="checkbox" {{if .Repository.EnableUnit $.UnitTypePullRequests}}checked{{end}}> | |||
<label>{{.i18n.Tr "repo.settings.pulls_desc"}}</label> | |||
</div> | |||
</div> | |||
@@ -236,7 +238,7 @@ | |||
</div> | |||
</div> | |||
{{if .Repository.EnableWiki}} | |||
{{if .Repository.EnableUnit $.UnitTypeWiki}} | |||
<div class="ui divider"></div> | |||
<div class="item"> | |||
@@ -370,7 +372,7 @@ | |||
</div> | |||
</div> | |||
{{if .Repository.EnableWiki}} | |||
{{if .Repository.EnableUnit $.UnitTypeWiki}} | |||
<div class="ui small modal" id="delete-wiki-modal"> | |||
<div class="header"> | |||
{{.i18n.Tr "repo.settings.wiki-delete"}} | |||