Merge branch 'golint-fix' of github.com:reasonerjt/harbor into golint-fix

This commit is contained in:
Tan Jiang 2016-02-26 18:36:16 +08:00
commit 16fe9ace42
23 changed files with 106 additions and 47 deletions

View File

@ -22,6 +22,7 @@ import (
"github.com/astaxie/beego/orm" "github.com/astaxie/beego/orm"
) )
// AddAccessLog persists the access logs
func AddAccessLog(accessLog models.AccessLog) error { func AddAccessLog(accessLog models.AccessLog) error {
o := orm.NewOrm() o := orm.NewOrm()
p, err := o.Raw(`insert into access_log p, err := o.Raw(`insert into access_log
@ -37,6 +38,7 @@ func AddAccessLog(accessLog models.AccessLog) error {
return err return err
} }
//GetAccessLogs gets access logs according to different conditions
func GetAccessLogs(accessLog models.AccessLog) ([]models.AccessLog, error) { func GetAccessLogs(accessLog models.AccessLog) ([]models.AccessLog, error) {
o := orm.NewOrm() o := orm.NewOrm()
@ -92,6 +94,7 @@ func GetAccessLogs(accessLog models.AccessLog) ([]models.AccessLog, error) {
return accessLogList, nil return accessLogList, nil
} }
// AccessLog ...
func AccessLog(username, projectName, repoName, action string) error { func AccessLog(username, projectName, repoName, action string) error {
o := orm.NewOrm() o := orm.NewOrm()
sql := "insert into access_log (user_id, project_id, repo_name, operation, op_time) " + sql := "insert into access_log (user_id, project_id, repo_name, operation, op_time) " +

View File

@ -23,9 +23,10 @@ import (
"time" "time"
"github.com/astaxie/beego/orm" "github.com/astaxie/beego/orm"
_ "github.com/go-sql-driver/mysql" _ "github.com/go-sql-driver/mysql" //register mysql driver
) )
// NonExistUserID : if a user does not exist, the ID of the user will be 0.
const NonExistUserID = 0 const NonExistUserID = 0
func isIllegalLength(s string, min int, max int) bool { func isIllegalLength(s string, min int, max int) bool {
@ -47,6 +48,7 @@ func isContainIllegalChar(s string, illegalChar []string) bool {
return false return false
} }
// GenerateRandomString generates a random string
func GenerateRandomString() (string, error) { func GenerateRandomString() (string, error) {
o := orm.NewOrm() o := orm.NewOrm()
var uuid string var uuid string
@ -58,6 +60,7 @@ func GenerateRandomString() (string, error) {
} }
//InitDB initializes the database
func InitDB() { func InitDB() {
orm.RegisterDriver("mysql", orm.DRMySQL) orm.RegisterDriver("mysql", orm.DRMySQL)
addr := os.Getenv("MYSQL_HOST") addr := os.Getenv("MYSQL_HOST")

View File

@ -20,6 +20,7 @@ import (
"github.com/astaxie/beego/orm" "github.com/astaxie/beego/orm"
) )
// GetUserByProject gets all members of the project.
func GetUserByProject(projectID int64, queryUser models.User) ([]models.User, error) { func GetUserByProject(projectID int64, queryUser models.User) ([]models.User, error) {
o := orm.NewOrm() o := orm.NewOrm()
u := []models.User{} u := []models.User{}

View File

@ -26,6 +26,8 @@ import (
) )
//TODO:transaction, return err //TODO:transaction, return err
// AddProject adds a project to the database along with project roles information and access log records.
func AddProject(project models.Project) error { func AddProject(project models.Project) error {
if isIllegalLength(project.Name, 4, 30) { if isIllegalLength(project.Name, 4, 30) {
@ -82,6 +84,7 @@ func AddProject(project models.Project) error {
return err return err
} }
// IsProjectPublic ...
func IsProjectPublic(projectName string) bool { func IsProjectPublic(projectName string) bool {
project, err := GetProjectByName(projectName) project, err := GetProjectByName(projectName)
if err != nil { if err != nil {
@ -94,7 +97,7 @@ func IsProjectPublic(projectName string) bool {
return project.Public == 1 return project.Public == 1
} }
//Query the projects based on publicity and user, disregarding the names etc. // QueryProject querys the projects based on publicity and user, disregarding the names etc.
func QueryProject(query models.Project) ([]models.Project, error) { func QueryProject(query models.Project) ([]models.Project, error) {
o := orm.NewOrm() o := orm.NewOrm()
@ -133,6 +136,7 @@ func QueryProject(query models.Project) ([]models.Project, error) {
return r, nil return r, nil
} }
//ProjectExists returns whether the project exists according to its name of ID.
func ProjectExists(nameOrID interface{}) (bool, error) { func ProjectExists(nameOrID interface{}) (bool, error) {
o := orm.NewOrm() o := orm.NewOrm()
type dummy struct{} type dummy struct{}
@ -155,6 +159,7 @@ func ProjectExists(nameOrID interface{}) (bool, error) {
} }
// GetProjectByID ...
func GetProjectByID(projectID int64) (*models.Project, error) { func GetProjectByID(projectID int64) (*models.Project, error) {
o := orm.NewOrm() o := orm.NewOrm()
@ -175,6 +180,7 @@ func GetProjectByID(projectID int64) (*models.Project, error) {
} }
} }
// GetProjectByName ...
func GetProjectByName(projectName string) (*models.Project, error) { func GetProjectByName(projectName string) (*models.Project, error) {
o := orm.NewOrm() o := orm.NewOrm()
var p []models.Project var p []models.Project
@ -188,6 +194,7 @@ func GetProjectByName(projectName string) (*models.Project, error) {
} }
} }
// GetPermission gets roles that the user has according to the project.
func GetPermission(username, projectName string) (string, error) { func GetPermission(username, projectName string) (string, error) {
o := orm.NewOrm() o := orm.NewOrm()
@ -209,6 +216,7 @@ func GetPermission(username, projectName string) (string, error) {
} }
} }
// ToggleProjectPublicity toggles the publicity of the project.
func ToggleProjectPublicity(projectID int64, publicity int) error { func ToggleProjectPublicity(projectID int64, publicity int) error {
o := orm.NewOrm() o := orm.NewOrm()
sql := "update project set public = ? where project_id = ?" sql := "update project set public = ? where project_id = ?"
@ -216,6 +224,7 @@ func ToggleProjectPublicity(projectID int64, publicity int) error {
return err return err
} }
// QueryRelevantProjects returns all projects that the user is a member of.
func QueryRelevantProjects(userID int) ([]models.Project, error) { func QueryRelevantProjects(userID int) ([]models.Project, error) {
o := orm.NewOrm() o := orm.NewOrm()
sql := `SELECT distinct p.project_id, p.name, p.public FROM registry.project p sql := `SELECT distinct p.project_id, p.name, p.public FROM registry.project p

View File

@ -20,6 +20,7 @@ import (
"github.com/astaxie/beego/orm" "github.com/astaxie/beego/orm"
) )
// AddProjectRole ...
func AddProjectRole(projectRole models.ProjectRole) (int64, error) { func AddProjectRole(projectRole models.ProjectRole) (int64, error) {
o := orm.NewOrm() o := orm.NewOrm()
p, err := o.Raw("insert into project_role (project_id, role_id) values (?, ?)").Prepare() p, err := o.Raw("insert into project_role (project_id, role_id) values (?, ?)").Prepare()
@ -35,6 +36,7 @@ func AddProjectRole(projectRole models.ProjectRole) (int64, error) {
return id, err return id, err
} }
// AddUserProjectRole inserts role information to table project_role and user_project_role.
func AddUserProjectRole(userID int, projectID int64, roleID int) error { func AddUserProjectRole(userID int, projectID int64, roleID int) error {
o := orm.NewOrm() o := orm.NewOrm()
@ -76,6 +78,7 @@ func AddUserProjectRole(userID int, projectID int64, roleID int) error {
return err return err
} }
// DeleteUserProjectRoles ...
func DeleteUserProjectRoles(userID int, projectID int64) error { func DeleteUserProjectRoles(userID int, projectID int64) error {
o := orm.NewOrm() o := orm.NewOrm()
sql := `delete from user_project_role where user_id = ? and pr_id in sql := `delete from user_project_role where user_id = ? and pr_id in

View File

@ -24,6 +24,7 @@ import (
"github.com/astaxie/beego/orm" "github.com/astaxie/beego/orm"
) )
// Register is used for user to register, the password is encrypted before the record is inserted into database.
func Register(user models.User) (int64, error) { func Register(user models.User) (int64, error) {
err := validate(user) err := validate(user)
@ -99,6 +100,7 @@ func validate(user models.User) error {
return nil return nil
} }
// UserExists returns whether a user exists according username or Email.
func UserExists(user models.User, target string) (bool, error) { func UserExists(user models.User, target string) (bool, error) {
if user.Username == "" && user.Email == "" { if user.Username == "" && user.Email == "" {

View File

@ -20,6 +20,7 @@ import (
"github.com/astaxie/beego/orm" "github.com/astaxie/beego/orm"
) )
// GetUserProjectRoles returns roles that the user has according to the project.
func GetUserProjectRoles(userQuery models.User, projectID int64) ([]models.Role, error) { func GetUserProjectRoles(userQuery models.User, projectID int64) ([]models.Role, error) {
o := orm.NewOrm() o := orm.NewOrm()
@ -52,6 +53,7 @@ func GetUserProjectRoles(userQuery models.User, projectID int64) ([]models.Role,
return roleList, nil return roleList, nil
} }
// IsAdminRole returns whether the user is admin.
func IsAdminRole(userID int) (bool, error) { func IsAdminRole(userID int) (bool, error) {
//role_id == 1 means the user is system admin //role_id == 1 means the user is system admin
userQuery := models.User{UserID: userID, RoleID: models.SYSADMIN} userQuery := models.User{UserID: userID, RoleID: models.SYSADMIN}

View File

@ -25,6 +25,7 @@ import (
"github.com/astaxie/beego/orm" "github.com/astaxie/beego/orm"
) )
// GetUser ...
func GetUser(query models.User) (*models.User, error) { func GetUser(query models.User) (*models.User, error) {
o := orm.NewOrm() o := orm.NewOrm()
@ -65,6 +66,7 @@ func GetUser(query models.User) (*models.User, error) {
} }
} }
// LoginByDb is used for user to login with database auth mode.
func LoginByDb(auth models.AuthModel) (*models.User, error) { func LoginByDb(auth models.AuthModel) (*models.User, error) {
query := models.User{Username: auth.Principal, Email: auth.Principal} query := models.User{Username: auth.Principal, Email: auth.Principal}
@ -84,6 +86,7 @@ func LoginByDb(auth models.AuthModel) (*models.User, error) {
} }
// ListUsers lists all users according to different conditions.
func ListUsers(query models.User) ([]models.User, error) { func ListUsers(query models.User) ([]models.User, error) {
o := orm.NewOrm() o := orm.NewOrm()
u := []models.User{} u := []models.User{}
@ -106,6 +109,7 @@ func ListUsers(query models.User) ([]models.User, error) {
return u, err return u, err
} }
// ToggleUserAdminRole gives a user admim role.
func ToggleUserAdminRole(u models.User) error { func ToggleUserAdminRole(u models.User) error {
projectRole := models.ProjectRole{PrID: 1} //admin project role projectRole := models.ProjectRole{PrID: 1} //admin project role
@ -136,6 +140,7 @@ func ToggleUserAdminRole(u models.User) error {
return err return err
} }
// ChangeUserPassword ...
func ChangeUserPassword(u models.User, oldPassword ...string) error { func ChangeUserPassword(u models.User, oldPassword ...string) error {
o := orm.NewOrm() o := orm.NewOrm()
var err error var err error
@ -161,6 +166,7 @@ func ChangeUserPassword(u models.User, oldPassword ...string) error {
return err return err
} }
// ResetUserPassword ...
func ResetUserPassword(u models.User) error { func ResetUserPassword(u models.User) error {
o := orm.NewOrm() o := orm.NewOrm()
r, err := o.Raw(`update user set password=?, reset_uuid=? where reset_uuid=?`, utils.Encrypt(u.Password, u.Salt), "", u.ResetUUID).Exec() r, err := o.Raw(`update user set password=?, reset_uuid=? where reset_uuid=?`, utils.Encrypt(u.Password, u.Salt), "", u.ResetUUID).Exec()
@ -177,12 +183,14 @@ func ResetUserPassword(u models.User) error {
return err return err
} }
// UpdateUserResetUUID ...
func UpdateUserResetUUID(u models.User) error { func UpdateUserResetUUID(u models.User) error {
o := orm.NewOrm() o := orm.NewOrm()
_, err := o.Raw(`update user set reset_uuid=? where email=?`, u.ResetUUID, u.Email).Exec() _, err := o.Raw(`update user set reset_uuid=? where email=?`, u.ResetUUID, u.Email).Exec()
return err return err
} }
// CheckUserPassword checks whether the password is correct.
func CheckUserPassword(query models.User) (*models.User, error) { func CheckUserPassword(query models.User) (*models.User, error) {
currentUser, err := GetUser(query) currentUser, err := GetUser(query)
@ -223,6 +231,7 @@ func CheckUserPassword(query models.User) (*models.User, error) {
} }
} }
// DeleteUser ...
func DeleteUser(userID int) error { func DeleteUser(userID int) error {
o := orm.NewOrm() o := orm.NewOrm()
_, err := o.Raw(`update user set deleted = 1 where user_id = ?`, userID).Exec() _, err := o.Raw(`update user set deleted = 1 where user_id = ?`, userID).Exec()

View File

@ -18,6 +18,7 @@ import (
"time" "time"
) )
// AccessLog holds information about logs which are used to record the actions that user take to the resourses.
type AccessLog struct { type AccessLog struct {
LogID int `orm:"column(log_id)" json:"LogId"` LogID int `orm:"column(log_id)" json:"LogId"`
UserID int `orm:"column(user_id)" json:"UserId"` UserID int `orm:"column(user_id)" json:"UserId"`

View File

@ -1,19 +1,20 @@
/* /*
Copyright (c) 2016 VMware, Inc. All Rights Reserved. Copyright (c) 2016 VMware, Inc. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
You may obtain a copy of the License at You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0 http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
package models package models
// AuthModel holds information used to authenticate.
type AuthModel struct { type AuthModel struct {
Principal string Principal string
Password string Password string

View File

@ -18,10 +18,12 @@ import (
"time" "time"
) )
// Notification holds all events.
type Notification struct { type Notification struct {
Events []Event Events []Event
} }
// Event holds the details of a event.
type Event struct { type Event struct {
ID string `json:"Id"` ID string `json:"Id"`
TimeStamp time.Time TimeStamp time.Time
@ -31,6 +33,7 @@ type Event struct {
Actor *Actor Actor *Actor
} }
// Target holds information about the target of a event.
type Target struct { type Target struct {
MediaType string MediaType string
Digest string Digest string
@ -38,10 +41,12 @@ type Target struct {
URL string `json:"Url"` URL string `json:"Url"`
} }
// Actor holds information about actor.
type Actor struct { type Actor struct {
Name string Name string
} }
// Request holds information about a request.
type Request struct { type Request struct {
ID string `json:"Id"` ID string `json:"Id"`
Method string Method string

View File

@ -18,6 +18,7 @@ import (
"time" "time"
) )
// Project holds the details of a project.
type Project struct { type Project struct {
ProjectID int64 `orm:"column(project_id)" json:"ProjectId"` ProjectID int64 `orm:"column(project_id)" json:"ProjectId"`
OwnerID int `orm:"column(owner_id)" json:"OwnerId"` OwnerID int `orm:"column(owner_id)" json:"OwnerId"`

View File

@ -14,6 +14,7 @@
*/ */
package models package models
// ProjectRole holds information about the relationship of project and role.
type ProjectRole struct { type ProjectRole struct {
PrID int `orm:"column(pr_id)" json:"PrId"` PrID int `orm:"column(pr_id)" json:"PrId"`
ProjectID int64 `orm:"column(project_id)" json:"ProjectId"` ProjectID int64 `orm:"column(project_id)" json:"ProjectId"`

View File

@ -14,12 +14,7 @@
*/ */
package models package models
type V1Repo struct { // Repo holds information about repositories.
NumResults int
Query string
Results []RepoItem
}
type Repo struct { type Repo struct {
Repositories []string `json:"repositories"` Repositories []string `json:"repositories"`
} }

View File

@ -18,6 +18,7 @@ import (
"time" "time"
) )
// RepoItem holds manifest of an image.
type RepoItem struct { type RepoItem struct {
ID string `json:"Id"` ID string `json:"Id"`
Parent string `json:"Parent"` Parent string `json:"Parent"`

View File

@ -15,12 +15,17 @@
package models package models
const ( const (
SYSADMIN = 1 //SYSADMIN system administrator
SYSADMIN = 1
//PROJECTADMIN project administrator
PROJECTADMIN = 2 PROJECTADMIN = 2
DEVELOPER = 3 //DEVELOPER developer
GUEST = 4 DEVELOPER = 3
//GUEST guest
GUEST = 4
) )
// Role holds the details of a role.
type Role struct { type Role struct {
RoleID int `json:"role_id" orm:"column(role_id)"` RoleID int `json:"role_id" orm:"column(role_id)"`
RoleCode string `json:"role_code" orm:"column(role_code)"` RoleCode string `json:"role_code" orm:"column(role_code)"`

View File

@ -14,6 +14,7 @@
*/ */
package models package models
// Tag holds information about a tag.
type Tag struct { type Tag struct {
Version string `json:"version"` Version string `json:"version"`
ImageID string `json:"image_id"` ImageID string `json:"image_id"`

View File

@ -14,6 +14,7 @@
*/ */
package models package models
// User holds the details of a user.
type User struct { type User struct {
UserID int `orm:"column(user_id)" json:"UserId"` UserID int `orm:"column(user_id)" json:"UserId"`
Username string `orm:"column(username)"` Username string `orm:"column(username)"`

View File

@ -14,6 +14,7 @@
*/ */
package models package models
// UserProjectRole holds information about relationship of user, project and role.
type UserProjectRole struct { type UserProjectRole struct {
UprID int `orm:"column(upr_id)" json:"UprId"` UprID int `orm:"column(upr_id)" json:"UprId"`
UserID int `orm:"column(user_id)" json:"UserId"` UserID int `orm:"column(user_id)" json:"UserId"`

View File

@ -1,16 +1,16 @@
/* /*
Copyright (c) 2016 VMware, Inc. All Rights Reserved. Copyright (c) 2016 VMware, Inc. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
You may obtain a copy of the License at You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0 http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
package utils package utils
@ -21,6 +21,7 @@ import (
"golang.org/x/crypto/pbkdf2" "golang.org/x/crypto/pbkdf2"
) )
// Encrypt encrypts the content with salt
func Encrypt(content string, salt string) string { func Encrypt(content string, salt string) string {
return fmt.Sprintf("%x", pbkdf2.Key([]byte(content), []byte(salt), 4096, 16, sha1.New)) return fmt.Sprintf("%x", pbkdf2.Key([]byte(content), []byte(salt), 4096, 16, sha1.New))
} }

View File

@ -1,16 +1,16 @@
/* /*
Copyright (c) 2016 VMware, Inc. All Rights Reserved. Copyright (c) 2016 VMware, Inc. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
You may obtain a copy of the License at You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0 http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
package utils package utils
@ -23,12 +23,15 @@ import (
"github.com/astaxie/beego" "github.com/astaxie/beego"
) )
// Mail holds information about content of Email
type Mail struct { type Mail struct {
From string From string
To []string To []string
Subject string Subject string
Message string Message string
} }
// MailConfig holds information about Email configurations
type MailConfig struct { type MailConfig struct {
Identity string Identity string
Host string Host string
@ -39,6 +42,7 @@ type MailConfig struct {
var mc MailConfig var mc MailConfig
// SendMail sends Email according to the configurations
func (m Mail) SendMail() error { func (m Mail) SendMail() error {
if mc.Host == "" { if mc.Host == "" {

View File

@ -29,6 +29,7 @@ import (
const sessionCookie = "beegosessionID" const sessionCookie = "beegosessionID"
// BuildRegistryURL builds the URL of registry
func BuildRegistryURL(segments ...string) string { func BuildRegistryURL(segments ...string) string {
registryURL := os.Getenv("REGISTRY_URL") registryURL := os.Getenv("REGISTRY_URL")
if registryURL == "" { if registryURL == "" {
@ -45,6 +46,7 @@ func BuildRegistryURL(segments ...string) string {
return url return url
} }
// HTTPGet is used to call the API of registry. If a token is needed, it will get a token first.
func HTTPGet(URL, sessionID, username, password string) ([]byte, error) { func HTTPGet(URL, sessionID, username, password string) ([]byte, error) {
response, err := http.Get(URL) response, err := http.Get(URL)
if err != nil { if err != nil {

View File

@ -23,10 +23,12 @@ import (
"github.com/astaxie/beego" "github.com/astaxie/beego"
) )
// Repository holds information about repository
type Repository struct { type Repository struct {
Name string Name string
} }
// ParseBasicAuth parses the basic authorization
func ParseBasicAuth(authorization []string) (username, password string) { func ParseBasicAuth(authorization []string) (username, password string) {
if authorization == nil || len(authorization) == 0 { if authorization == nil || len(authorization) == 0 {
beego.Debug("Authorization header is not set.") beego.Debug("Authorization header is not set.")
@ -38,6 +40,7 @@ func ParseBasicAuth(authorization []string) (username, password string) {
return pair[0], pair[1] return pair[0], pair[1]
} }
// GetProject parses the repository and return the name of project.
func (r *Repository) GetProject() string { func (r *Repository) GetProject() string {
if !strings.ContainsRune(r.Name, '/') { if !strings.ContainsRune(r.Name, '/') {
return "" return ""
@ -45,18 +48,22 @@ func (r *Repository) GetProject() string {
return r.Name[0:strings.LastIndex(r.Name, "/")] return r.Name[0:strings.LastIndex(r.Name, "/")]
} }
// ProjectSorter holds an array of projects
type ProjectSorter struct { type ProjectSorter struct {
Projects []models.Project Projects []models.Project
} }
// Len returns the length of array in ProjectSorter
func (ps *ProjectSorter) Len() int { func (ps *ProjectSorter) Len() int {
return len(ps.Projects) return len(ps.Projects)
} }
// Less defines the comparison rules of project
func (ps *ProjectSorter) Less(i, j int) bool { func (ps *ProjectSorter) Less(i, j int) bool {
return ps.Projects[i].Name < ps.Projects[j].Name return ps.Projects[i].Name < ps.Projects[j].Name
} }
// Swap swaps the position of i and j
func (ps *ProjectSorter) Swap(i, j int) { func (ps *ProjectSorter) Swap(i, j int) {
ps.Projects[i], ps.Projects[j] = ps.Projects[j], ps.Projects[i] ps.Projects[i], ps.Projects[j] = ps.Projects[j], ps.Projects[i]
} }