mirror of
https://github.com/goharbor/harbor.git
synced 2024-12-23 00:57:44 +01:00
Merge branch 'golint-fix' of github.com:reasonerjt/harbor into golint-fix
This commit is contained in:
commit
16fe9ace42
@ -22,6 +22,7 @@ import (
|
||||
"github.com/astaxie/beego/orm"
|
||||
)
|
||||
|
||||
// AddAccessLog persists the access logs
|
||||
func AddAccessLog(accessLog models.AccessLog) error {
|
||||
o := orm.NewOrm()
|
||||
p, err := o.Raw(`insert into access_log
|
||||
@ -37,6 +38,7 @@ func AddAccessLog(accessLog models.AccessLog) error {
|
||||
return err
|
||||
}
|
||||
|
||||
//GetAccessLogs gets access logs according to different conditions
|
||||
func GetAccessLogs(accessLog models.AccessLog) ([]models.AccessLog, error) {
|
||||
|
||||
o := orm.NewOrm()
|
||||
@ -92,6 +94,7 @@ func GetAccessLogs(accessLog models.AccessLog) ([]models.AccessLog, error) {
|
||||
return accessLogList, nil
|
||||
}
|
||||
|
||||
// AccessLog ...
|
||||
func AccessLog(username, projectName, repoName, action string) error {
|
||||
o := orm.NewOrm()
|
||||
sql := "insert into access_log (user_id, project_id, repo_name, operation, op_time) " +
|
||||
|
@ -23,9 +23,10 @@ import (
|
||||
"time"
|
||||
|
||||
"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
|
||||
|
||||
func isIllegalLength(s string, min int, max int) bool {
|
||||
@ -47,6 +48,7 @@ func isContainIllegalChar(s string, illegalChar []string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// GenerateRandomString generates a random string
|
||||
func GenerateRandomString() (string, error) {
|
||||
o := orm.NewOrm()
|
||||
var uuid string
|
||||
@ -58,6 +60,7 @@ func GenerateRandomString() (string, error) {
|
||||
|
||||
}
|
||||
|
||||
//InitDB initializes the database
|
||||
func InitDB() {
|
||||
orm.RegisterDriver("mysql", orm.DRMySQL)
|
||||
addr := os.Getenv("MYSQL_HOST")
|
||||
|
@ -20,6 +20,7 @@ import (
|
||||
"github.com/astaxie/beego/orm"
|
||||
)
|
||||
|
||||
// GetUserByProject gets all members of the project.
|
||||
func GetUserByProject(projectID int64, queryUser models.User) ([]models.User, error) {
|
||||
o := orm.NewOrm()
|
||||
u := []models.User{}
|
||||
|
@ -26,6 +26,8 @@ import (
|
||||
)
|
||||
|
||||
//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 {
|
||||
|
||||
if isIllegalLength(project.Name, 4, 30) {
|
||||
@ -82,6 +84,7 @@ func AddProject(project models.Project) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// IsProjectPublic ...
|
||||
func IsProjectPublic(projectName string) bool {
|
||||
project, err := GetProjectByName(projectName)
|
||||
if err != nil {
|
||||
@ -94,7 +97,7 @@ func IsProjectPublic(projectName string) bool {
|
||||
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) {
|
||||
o := orm.NewOrm()
|
||||
|
||||
@ -133,6 +136,7 @@ func QueryProject(query models.Project) ([]models.Project, error) {
|
||||
return r, nil
|
||||
}
|
||||
|
||||
//ProjectExists returns whether the project exists according to its name of ID.
|
||||
func ProjectExists(nameOrID interface{}) (bool, error) {
|
||||
o := orm.NewOrm()
|
||||
type dummy struct{}
|
||||
@ -155,6 +159,7 @@ func ProjectExists(nameOrID interface{}) (bool, error) {
|
||||
|
||||
}
|
||||
|
||||
// GetProjectByID ...
|
||||
func GetProjectByID(projectID int64) (*models.Project, error) {
|
||||
o := orm.NewOrm()
|
||||
|
||||
@ -175,6 +180,7 @@ func GetProjectByID(projectID int64) (*models.Project, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// GetProjectByName ...
|
||||
func GetProjectByName(projectName string) (*models.Project, error) {
|
||||
o := orm.NewOrm()
|
||||
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) {
|
||||
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 {
|
||||
o := orm.NewOrm()
|
||||
sql := "update project set public = ? where project_id = ?"
|
||||
@ -216,6 +224,7 @@ func ToggleProjectPublicity(projectID int64, publicity int) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// QueryRelevantProjects returns all projects that the user is a member of.
|
||||
func QueryRelevantProjects(userID int) ([]models.Project, error) {
|
||||
o := orm.NewOrm()
|
||||
sql := `SELECT distinct p.project_id, p.name, p.public FROM registry.project p
|
||||
|
@ -20,6 +20,7 @@ import (
|
||||
"github.com/astaxie/beego/orm"
|
||||
)
|
||||
|
||||
// AddProjectRole ...
|
||||
func AddProjectRole(projectRole models.ProjectRole) (int64, error) {
|
||||
o := orm.NewOrm()
|
||||
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
|
||||
}
|
||||
|
||||
// AddUserProjectRole inserts role information to table project_role and user_project_role.
|
||||
func AddUserProjectRole(userID int, projectID int64, roleID int) error {
|
||||
|
||||
o := orm.NewOrm()
|
||||
@ -76,6 +78,7 @@ func AddUserProjectRole(userID int, projectID int64, roleID int) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// DeleteUserProjectRoles ...
|
||||
func DeleteUserProjectRoles(userID int, projectID int64) error {
|
||||
o := orm.NewOrm()
|
||||
sql := `delete from user_project_role where user_id = ? and pr_id in
|
||||
|
@ -24,6 +24,7 @@ import (
|
||||
"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) {
|
||||
|
||||
err := validate(user)
|
||||
@ -99,6 +100,7 @@ func validate(user models.User) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// UserExists returns whether a user exists according username or Email.
|
||||
func UserExists(user models.User, target string) (bool, error) {
|
||||
|
||||
if user.Username == "" && user.Email == "" {
|
||||
|
@ -20,6 +20,7 @@ import (
|
||||
"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) {
|
||||
|
||||
o := orm.NewOrm()
|
||||
@ -52,6 +53,7 @@ func GetUserProjectRoles(userQuery models.User, projectID int64) ([]models.Role,
|
||||
return roleList, nil
|
||||
}
|
||||
|
||||
// IsAdminRole returns whether the user is admin.
|
||||
func IsAdminRole(userID int) (bool, error) {
|
||||
//role_id == 1 means the user is system admin
|
||||
userQuery := models.User{UserID: userID, RoleID: models.SYSADMIN}
|
||||
|
@ -25,6 +25,7 @@ import (
|
||||
"github.com/astaxie/beego/orm"
|
||||
)
|
||||
|
||||
// GetUser ...
|
||||
func GetUser(query models.User) (*models.User, error) {
|
||||
|
||||
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) {
|
||||
|
||||
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) {
|
||||
o := orm.NewOrm()
|
||||
u := []models.User{}
|
||||
@ -106,6 +109,7 @@ func ListUsers(query models.User) ([]models.User, error) {
|
||||
return u, err
|
||||
}
|
||||
|
||||
// ToggleUserAdminRole gives a user admim role.
|
||||
func ToggleUserAdminRole(u models.User) error {
|
||||
|
||||
projectRole := models.ProjectRole{PrID: 1} //admin project role
|
||||
@ -136,6 +140,7 @@ func ToggleUserAdminRole(u models.User) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// ChangeUserPassword ...
|
||||
func ChangeUserPassword(u models.User, oldPassword ...string) error {
|
||||
o := orm.NewOrm()
|
||||
var err error
|
||||
@ -161,6 +166,7 @@ func ChangeUserPassword(u models.User, oldPassword ...string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// ResetUserPassword ...
|
||||
func ResetUserPassword(u models.User) error {
|
||||
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()
|
||||
@ -177,12 +183,14 @@ func ResetUserPassword(u models.User) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// UpdateUserResetUUID ...
|
||||
func UpdateUserResetUUID(u models.User) error {
|
||||
o := orm.NewOrm()
|
||||
_, err := o.Raw(`update user set reset_uuid=? where email=?`, u.ResetUUID, u.Email).Exec()
|
||||
return err
|
||||
}
|
||||
|
||||
// CheckUserPassword checks whether the password is correct.
|
||||
func CheckUserPassword(query models.User) (*models.User, error) {
|
||||
|
||||
currentUser, err := GetUser(query)
|
||||
@ -223,6 +231,7 @@ func CheckUserPassword(query models.User) (*models.User, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// DeleteUser ...
|
||||
func DeleteUser(userID int) error {
|
||||
o := orm.NewOrm()
|
||||
_, err := o.Raw(`update user set deleted = 1 where user_id = ?`, userID).Exec()
|
||||
|
@ -18,6 +18,7 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// AccessLog holds information about logs which are used to record the actions that user take to the resourses.
|
||||
type AccessLog struct {
|
||||
LogID int `orm:"column(log_id)" json:"LogId"`
|
||||
UserID int `orm:"column(user_id)" json:"UserId"`
|
||||
|
@ -1,19 +1,20 @@
|
||||
/*
|
||||
Copyright (c) 2016 VMware, Inc. All Rights Reserved.
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
Copyright (c) 2016 VMware, Inc. All Rights Reserved.
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
package models
|
||||
|
||||
// AuthModel holds information used to authenticate.
|
||||
type AuthModel struct {
|
||||
Principal string
|
||||
Password string
|
||||
|
@ -18,10 +18,12 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// Notification holds all events.
|
||||
type Notification struct {
|
||||
Events []Event
|
||||
}
|
||||
|
||||
// Event holds the details of a event.
|
||||
type Event struct {
|
||||
ID string `json:"Id"`
|
||||
TimeStamp time.Time
|
||||
@ -31,6 +33,7 @@ type Event struct {
|
||||
Actor *Actor
|
||||
}
|
||||
|
||||
// Target holds information about the target of a event.
|
||||
type Target struct {
|
||||
MediaType string
|
||||
Digest string
|
||||
@ -38,10 +41,12 @@ type Target struct {
|
||||
URL string `json:"Url"`
|
||||
}
|
||||
|
||||
// Actor holds information about actor.
|
||||
type Actor struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
// Request holds information about a request.
|
||||
type Request struct {
|
||||
ID string `json:"Id"`
|
||||
Method string
|
||||
|
@ -18,6 +18,7 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// Project holds the details of a project.
|
||||
type Project struct {
|
||||
ProjectID int64 `orm:"column(project_id)" json:"ProjectId"`
|
||||
OwnerID int `orm:"column(owner_id)" json:"OwnerId"`
|
||||
|
@ -14,6 +14,7 @@
|
||||
*/
|
||||
package models
|
||||
|
||||
// ProjectRole holds information about the relationship of project and role.
|
||||
type ProjectRole struct {
|
||||
PrID int `orm:"column(pr_id)" json:"PrId"`
|
||||
ProjectID int64 `orm:"column(project_id)" json:"ProjectId"`
|
||||
|
@ -14,12 +14,7 @@
|
||||
*/
|
||||
package models
|
||||
|
||||
type V1Repo struct {
|
||||
NumResults int
|
||||
Query string
|
||||
Results []RepoItem
|
||||
}
|
||||
|
||||
// Repo holds information about repositories.
|
||||
type Repo struct {
|
||||
Repositories []string `json:"repositories"`
|
||||
}
|
||||
|
@ -18,6 +18,7 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// RepoItem holds manifest of an image.
|
||||
type RepoItem struct {
|
||||
ID string `json:"Id"`
|
||||
Parent string `json:"Parent"`
|
||||
|
@ -15,12 +15,17 @@
|
||||
package models
|
||||
|
||||
const (
|
||||
SYSADMIN = 1
|
||||
//SYSADMIN system administrator
|
||||
SYSADMIN = 1
|
||||
//PROJECTADMIN project administrator
|
||||
PROJECTADMIN = 2
|
||||
DEVELOPER = 3
|
||||
GUEST = 4
|
||||
//DEVELOPER developer
|
||||
DEVELOPER = 3
|
||||
//GUEST guest
|
||||
GUEST = 4
|
||||
)
|
||||
|
||||
// Role holds the details of a role.
|
||||
type Role struct {
|
||||
RoleID int `json:"role_id" orm:"column(role_id)"`
|
||||
RoleCode string `json:"role_code" orm:"column(role_code)"`
|
||||
|
@ -14,6 +14,7 @@
|
||||
*/
|
||||
package models
|
||||
|
||||
// Tag holds information about a tag.
|
||||
type Tag struct {
|
||||
Version string `json:"version"`
|
||||
ImageID string `json:"image_id"`
|
||||
|
@ -14,6 +14,7 @@
|
||||
*/
|
||||
package models
|
||||
|
||||
// User holds the details of a user.
|
||||
type User struct {
|
||||
UserID int `orm:"column(user_id)" json:"UserId"`
|
||||
Username string `orm:"column(username)"`
|
||||
|
@ -14,6 +14,7 @@
|
||||
*/
|
||||
package models
|
||||
|
||||
// UserProjectRole holds information about relationship of user, project and role.
|
||||
type UserProjectRole struct {
|
||||
UprID int `orm:"column(upr_id)" json:"UprId"`
|
||||
UserID int `orm:"column(user_id)" json:"UserId"`
|
||||
|
@ -1,16 +1,16 @@
|
||||
/*
|
||||
Copyright (c) 2016 VMware, Inc. All Rights Reserved.
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
Copyright (c) 2016 VMware, Inc. All Rights Reserved.
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
package utils
|
||||
|
||||
@ -21,6 +21,7 @@ import (
|
||||
"golang.org/x/crypto/pbkdf2"
|
||||
)
|
||||
|
||||
// Encrypt encrypts the content with salt
|
||||
func Encrypt(content string, salt string) string {
|
||||
return fmt.Sprintf("%x", pbkdf2.Key([]byte(content), []byte(salt), 4096, 16, sha1.New))
|
||||
}
|
||||
|
@ -1,16 +1,16 @@
|
||||
/*
|
||||
Copyright (c) 2016 VMware, Inc. All Rights Reserved.
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
Copyright (c) 2016 VMware, Inc. All Rights Reserved.
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
package utils
|
||||
|
||||
@ -23,12 +23,15 @@ import (
|
||||
"github.com/astaxie/beego"
|
||||
)
|
||||
|
||||
// Mail holds information about content of Email
|
||||
type Mail struct {
|
||||
From string
|
||||
To []string
|
||||
Subject string
|
||||
Message string
|
||||
}
|
||||
|
||||
// MailConfig holds information about Email configurations
|
||||
type MailConfig struct {
|
||||
Identity string
|
||||
Host string
|
||||
@ -39,6 +42,7 @@ type MailConfig struct {
|
||||
|
||||
var mc MailConfig
|
||||
|
||||
// SendMail sends Email according to the configurations
|
||||
func (m Mail) SendMail() error {
|
||||
|
||||
if mc.Host == "" {
|
||||
|
@ -29,6 +29,7 @@ import (
|
||||
|
||||
const sessionCookie = "beegosessionID"
|
||||
|
||||
// BuildRegistryURL builds the URL of registry
|
||||
func BuildRegistryURL(segments ...string) string {
|
||||
registryURL := os.Getenv("REGISTRY_URL")
|
||||
if registryURL == "" {
|
||||
@ -45,6 +46,7 @@ func BuildRegistryURL(segments ...string) string {
|
||||
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) {
|
||||
response, err := http.Get(URL)
|
||||
if err != nil {
|
||||
|
@ -23,10 +23,12 @@ import (
|
||||
"github.com/astaxie/beego"
|
||||
)
|
||||
|
||||
// Repository holds information about repository
|
||||
type Repository struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
// ParseBasicAuth parses the basic authorization
|
||||
func ParseBasicAuth(authorization []string) (username, password string) {
|
||||
if authorization == nil || len(authorization) == 0 {
|
||||
beego.Debug("Authorization header is not set.")
|
||||
@ -38,6 +40,7 @@ func ParseBasicAuth(authorization []string) (username, password string) {
|
||||
return pair[0], pair[1]
|
||||
}
|
||||
|
||||
// GetProject parses the repository and return the name of project.
|
||||
func (r *Repository) GetProject() string {
|
||||
if !strings.ContainsRune(r.Name, '/') {
|
||||
return ""
|
||||
@ -45,18 +48,22 @@ func (r *Repository) GetProject() string {
|
||||
return r.Name[0:strings.LastIndex(r.Name, "/")]
|
||||
}
|
||||
|
||||
// ProjectSorter holds an array of projects
|
||||
type ProjectSorter struct {
|
||||
Projects []models.Project
|
||||
}
|
||||
|
||||
// Len returns the length of array in ProjectSorter
|
||||
func (ps *ProjectSorter) Len() int {
|
||||
return len(ps.Projects)
|
||||
}
|
||||
|
||||
// Less defines the comparison rules of project
|
||||
func (ps *ProjectSorter) Less(i, j int) bool {
|
||||
return ps.Projects[i].Name < ps.Projects[j].Name
|
||||
}
|
||||
|
||||
// Swap swaps the position of i and j
|
||||
func (ps *ProjectSorter) Swap(i, j int) {
|
||||
ps.Projects[i], ps.Projects[j] = ps.Projects[j], ps.Projects[i]
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user