Browse Source

user/settings: complete repositories panel (#4312)

pull/4343/head
Unknwon 8 years ago
parent
commit
66c1e6b0e8
No known key found for this signature in database
GPG Key ID: 25B575AE3213B2B3
  1. 4
      cmd/web.go
  2. 8
      conf/locale/locale_en-US.ini
  3. 2
      gogs.go
  4. 33
      models/repo.go
  5. 32
      models/repo_collaboration.go
  6. 8
      modules/bindata/bindata.go
  7. 4
      public/config.codekit
  8. 16
      public/css/gogs.css
  9. 2
      public/js/gogs.js
  10. 12
      public/less/_user.less
  11. 8
      routers/api/v1/repo/collaborators.go
  12. 59
      routers/user/setting.go
  13. 2
      templates/.VERSION
  14. 4
      templates/user/settings/navbar.tmpl
  15. 49
      templates/user/settings/repos.tmpl
  16. 54
      templates/user/settings/repositories.tmpl

4
cmd/web.go

@ -255,9 +255,9 @@ func runWeb(ctx *cli.Context) error {
m.Get("", user.SettingsOrganizations) m.Get("", user.SettingsOrganizations)
m.Post("/leave", user.SettingsLeaveOrganization) m.Post("/leave", user.SettingsLeaveOrganization)
}) })
m.Group("/repos", func() { m.Group("/repositories", func() {
m.Get("", user.SettingsRepos) m.Get("", user.SettingsRepos)
m.Post("/delete", user.SettingsDeleteRepo) m.Post("/leave", user.SettingsLeaveRepo)
}) })
m.Route("/delete", "GET,POST", user.SettingsDelete) m.Route("/delete", "GET,POST", user.SettingsDelete)
}, reqSignIn, func(ctx *context.Context) { }, reqSignIn, func(ctx *context.Context) {

8
conf/locale/locale_en-US.ini

@ -258,6 +258,7 @@ ssh_keys = SSH Keys
social = Social Accounts social = Social Accounts
applications = Applications applications = Applications
orgs = Organizations orgs = Organizations
repos = Repositories
delete = Delete Account delete = Delete Account
uid = Uid uid = Uid
@ -343,9 +344,14 @@ access_token_deletion_desc = Delete this personal access token will remove all r
delete_token_success = Personal access token has been removed successfully! Don't forget to update your application as well. delete_token_success = Personal access token has been removed successfully! Don't forget to update your application as well.
orgs.none = You are not a member of any organizations. orgs.none = You are not a member of any organizations.
orgs.leave_title = Leave an organization orgs.leave_title = Leave organization
orgs.leave_desc = You will lose access to all repositories and teams after you left the organization. Do you want to continue? orgs.leave_desc = You will lose access to all repositories and teams after you left the organization. Do you want to continue?
repos.leave = Leave
repos.leave_title = Leave repository
repos.leave_desc = You will lose access to the repository after you left. Do you want to continue?
repos.leave_success = You have left repository '%s' successfully!
delete_account = Delete Your Account delete_account = Delete Your Account
delete_prompt = The operation will delete your account permanently, and <strong>CANNOT</strong> be undone! delete_prompt = The operation will delete your account permanently, and <strong>CANNOT</strong> be undone!
confirm_delete_account = Confirm Deletion confirm_delete_account = Confirm Deletion

2
gogs.go

@ -16,7 +16,7 @@ import (
"github.com/gogits/gogs/modules/setting" "github.com/gogits/gogs/modules/setting"
) )
const APP_VER = "0.10.25.0322" const APP_VER = "0.10.26.0323"
func init() { func init() {
setting.AppVer = APP_VER setting.AppVer = APP_VER

33
models/repo.go

@ -228,6 +228,21 @@ func (repo *Repository) AfterSet(colName string, _ xorm.Cell) {
} }
} }
func (repo *Repository) loadAttributes(e Engine) (err error) {
if repo.Owner == nil {
repo.Owner, err = getUserByID(e, repo.OwnerID)
if err != nil {
return fmt.Errorf("getUserByID [%d]: %v", repo.OwnerID, err)
}
}
return nil
}
func (repo *Repository) LoadAttributes() error {
return repo.loadAttributes(x)
}
// MustOwner always returns a valid *User object to avoid // MustOwner always returns a valid *User object to avoid
// conceptually impossible error handling. // conceptually impossible error handling.
// It creates a fake object that contains error deftail // It creates a fake object that contains error deftail
@ -1559,6 +1574,24 @@ func GetRecentUpdatedRepositories(page, pageSize int) (repos []*Repository, err
Where("is_private=?", false).Limit(pageSize).Desc("updated_unix").Find(&repos) Where("is_private=?", false).Limit(pageSize).Desc("updated_unix").Find(&repos)
} }
// GetUserAndCollaborativeRepositories returns list of repositories the user owns and collaborates.
func GetUserAndCollaborativeRepositories(userID int64) ([]*Repository, error) {
repos := make([]*Repository, 0, 10)
if err := x.Alias("repo").
Join("INNER", "collaboration", "collaboration.repo_id = repo.id").
Where("collaboration.user_id = ?", userID).
Find(&repos); err != nil {
return nil, fmt.Errorf("select collaborative repositories: %v", err)
}
ownRepos := make([]*Repository, 0, 10)
if err := x.Where("owner_id = ?", userID).Find(&ownRepos); err != nil {
return nil, fmt.Errorf("select own repositories: %v", err)
}
return append(repos, ownRepos...), nil
}
func getRepositoryCount(e Engine, u *User) (int64, error) { func getRepositoryCount(e Engine, u *User) (int64, error) {
return x.Count(&Repository{OwnerID: u.ID}) return x.Count(&Repository{OwnerID: u.ID})
} }

32
models/repo_collaboration.go

@ -7,6 +7,8 @@ package models
import ( import (
"fmt" "fmt"
log "gopkg.in/clog.v1"
api "github.com/gogits/go-gogs-client" api "github.com/gogits/go-gogs-client"
) )
@ -31,14 +33,22 @@ func (c *Collaboration) ModeI18nKey() string {
} }
} }
//IsCollaborator returns true if the user is a collaborator // IsCollaborator returns true if the user is a collaborator of the repository.
func (repo *Repository) IsCollaborator(uid int64) (bool, error) { func IsCollaborator(repoID, userID int64) bool {
collaboration := &Collaboration{ collaboration := &Collaboration{
RepoID: repo.ID, RepoID: repoID,
UserID: uid, UserID: userID,
} }
has, err := x.Get(collaboration)
if err != nil {
log.Error(2, "get collaboration [repo_id: %d, user_id: %d]: %v", repoID, userID, err)
return false
}
return has
}
return x.Get(collaboration) func (repo *Repository) IsCollaborator(userID int64) bool {
return IsCollaborator(repo.ID, userID)
} }
// AddCollaborator adds new collaboration to a repository with default access mode. // AddCollaborator adds new collaboration to a repository with default access mode.
@ -186,10 +196,14 @@ func (repo *Repository) ChangeCollaborationAccessMode(userID int64, mode AccessM
} }
// DeleteCollaboration removes collaboration relation between the user and repository. // DeleteCollaboration removes collaboration relation between the user and repository.
func (repo *Repository) DeleteCollaboration(uid int64) (err error) { func DeleteCollaboration(repo *Repository, userID int64) (err error) {
if !IsCollaborator(repo.ID, userID) {
return nil
}
collaboration := &Collaboration{ collaboration := &Collaboration{
RepoID: repo.ID, RepoID: repo.ID,
UserID: uid, UserID: userID,
} }
sess := x.NewSession() sess := x.NewSession()
@ -206,3 +220,7 @@ func (repo *Repository) DeleteCollaboration(uid int64) (err error) {
return sess.Commit() return sess.Commit()
} }
func (repo *Repository) DeleteCollaboration(userID int64) error {
return DeleteCollaboration(repo, userID)
}

8
modules/bindata/bindata.go

File diff suppressed because one or more lines are too long

4
public/config.codekit

@ -1,6 +1,6 @@
{ {
"CodeKitInfo": "This is a CodeKit 2.x project configuration file. It is designed to sync project settings across multiple machines. MODIFYING THE CONTENTS OF THIS FILE IS A POOR LIFE DECISION. If you do so, you will likely cause CodeKit to crash. This file is not useful unless accompanied by the project that created it in CodeKit 2. This file is not backwards-compatible with CodeKit 1.x. For more information, see: http:\/\/incident57.com\/codekit", "CodeKitInfo": "This is a CodeKit 2.x project configuration file. It is designed to sync project settings across multiple machines. MODIFYING THE CONTENTS OF THIS FILE IS A POOR LIFE DECISION. If you do so, you will likely cause CodeKit to crash. This file is not useful unless accompanied by the project that created it in CodeKit 2. This file is not backwards-compatible with CodeKit 1.x. For more information, see: http:\/\/incident57.com\/codekit",
"creatorBuild": "19115", "creatorBuild": "19127",
"files": { "files": {
"\/css\/github.min.css": { "\/css\/github.min.css": {
"fileType": 16, "fileType": 16,
@ -66,7 +66,7 @@
"fileType": 32768, "fileType": 32768,
"ignore": 0, "ignore": 0,
"ignoreWasSetByUser": 0, "ignoreWasSetByUser": 0,
"initialSize": 4048, "initialSize": 514087,
"inputAbbreviatedPath": "\/img\/avatar_default.png", "inputAbbreviatedPath": "\/img\/avatar_default.png",
"outputAbbreviatedPath": "\/img\/avatar_default.png", "outputAbbreviatedPath": "\/img\/avatar_default.png",
"outputPathIsOutsideProject": 0, "outputPathIsOutsideProject": 0,

16
public/css/gogs.css

@ -2841,16 +2841,26 @@ footer .ui.language .menu {
.user.settings .email.list .item:not(:first-child) .button { .user.settings .email.list .item:not(:first-child) .button {
margin-top: -10px; margin-top: -10px;
} }
.user.settings .orgs.non-empty { .user.settings.organizations .orgs.non-empty {
padding: 0; padding: 0;
} }
.user.settings .orgs .item { .user.settings.organizations .orgs .item {
padding: 10px; padding: 10px;
} }
.user.settings .orgs .item .button { .user.settings.organizations .orgs .item .button {
margin-top: 5px; margin-top: 5px;
margin-right: 8px; margin-right: 8px;
} }
.user.settings.repositories .repos {
padding: 0;
}
.user.settings.repositories .repos .item {
padding: 15px;
height: 46px;
}
.user.settings.repositories .repos .item .button {
margin-top: -5px;
}
.user.profile .ui.card .username { .user.profile .ui.card .username {
display: block; display: block;
} }

2
public/js/gogs.js

@ -1308,7 +1308,7 @@ $(document).ready(function () {
$.post($this.data('url'), { $.post($this.data('url'), {
"_csrf": csrf, "_csrf": csrf,
"id": $this.data("id") "id": $this.data("id")
}).done(function (data) { }).success(function (data) {
window.location.href = data.redirect; window.location.href = data.redirect;
}); });
} }

12
public/less/_user.less

@ -19,7 +19,7 @@
} }
} }
} }
.orgs { &.organizations .orgs {
&.non-empty { &.non-empty {
padding: 0; padding: 0;
} }
@ -31,6 +31,16 @@
} }
} }
} }
&.repositories .repos {
padding: 0;
.item {
padding: 15px;
height: 46px;
.button {
margin-top: -5px;
}
}
}
} }
&.profile { &.profile {

8
routers/api/v1/repo/collaborators.go

@ -67,13 +67,7 @@ func IsCollaborator(ctx *context.APIContext) {
return return
} }
is, err := ctx.Repo.Repository.IsCollaborator(collaborator.ID) if !ctx.Repo.Repository.IsCollaborator(collaborator.ID) {
if err != nil {
ctx.Error(500, "IsCollaboration", err)
return
}
if !is {
ctx.Status(404) ctx.Status(404)
} else { } else {
ctx.Status(204) ctx.Status(204)

59
routers/user/setting.go

@ -7,11 +7,9 @@ package user
import ( import (
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"net/url"
"strings" "strings"
"github.com/Unknwon/com" "github.com/Unknwon/com"
"github.com/Unknwon/paginater"
log "gopkg.in/clog.v1" log "gopkg.in/clog.v1"
"github.com/gogits/gogs/models" "github.com/gogits/gogs/models"
@ -32,7 +30,7 @@ const (
SETTINGS_SOCIAL base.TplName = "user/settings/social" SETTINGS_SOCIAL base.TplName = "user/settings/social"
SETTINGS_APPLICATIONS base.TplName = "user/settings/applications" SETTINGS_APPLICATIONS base.TplName = "user/settings/applications"
SETTINGS_ORGANIZATIONS base.TplName = "user/settings/organizations" SETTINGS_ORGANIZATIONS base.TplName = "user/settings/organizations"
SETTINGS_REPOS base.TplName = "user/settings/repos" SETTINGS_REPOSITORIES base.TplName = "user/settings/repositories"
SETTINGS_DELETE base.TplName = "user/settings/delete" SETTINGS_DELETE base.TplName = "user/settings/delete"
NOTIFICATION base.TplName = "user/notification" NOTIFICATION base.TplName = "user/notification"
SECURITY base.TplName = "user/security" SECURITY base.TplName = "user/security"
@ -446,6 +444,9 @@ func SettingsLeaveOrganization(ctx *context.Context) {
err := models.RemoveOrgUser(ctx.QueryInt64("id"), ctx.User.ID) err := models.RemoveOrgUser(ctx.QueryInt64("id"), ctx.User.ID)
if models.IsErrLastOrgOwner(err) { if models.IsErrLastOrgOwner(err) {
ctx.Flash.Error(ctx.Tr("form.last_org_owner")) ctx.Flash.Error(ctx.Tr("form.last_org_owner"))
} else {
ctx.Handle(500, "RemoveOrgUser", err)
return
} }
ctx.JSON(200, map[string]interface{}{ ctx.JSON(200, map[string]interface{}{
@ -454,63 +455,39 @@ func SettingsLeaveOrganization(ctx *context.Context) {
} }
func SettingsRepos(ctx *context.Context) { func SettingsRepos(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("settings")
ctx.Data["Title"] = ctx.Tr("admin.repositories")
ctx.Data["PageIsSettingsRepositories"] = true ctx.Data["PageIsSettingsRepositories"] = true
keyword := ctx.Query("q") repos, err := models.GetUserAndCollaborativeRepositories(ctx.User.ID)
page := ctx.QueryInt("page")
if page <= 0 {
page = 1
}
repos, count, err := models.SearchRepositoryByName(&models.SearchRepoOptions{
Keyword: keyword,
UserID: ctx.User.ID,
OrderBy: "lower_name",
Page: page,
PageSize: setting.UI.Admin.RepoPagingNum,
})
if err != nil { if err != nil {
ctx.Handle(500, "SearchRepositoryByName", err) ctx.Handle(500, "GetUserAndCollaborativeRepositories", err)
return return
} }
if err = models.RepositoryList(repos).LoadAttributes(); err != nil { if err = models.RepositoryList(repos).LoadAttributes(); err != nil {
ctx.Handle(500, "LoadAttributes", err) ctx.Handle(500, "LoadAttributes", err)
return return
} }
ctx.Data["Keyword"] = keyword
ctx.Data["Total"] = count
ctx.Data["Page"] = paginater.New(int(count), setting.UI.Admin.RepoPagingNum, page, 5)
ctx.Data["Repos"] = repos ctx.Data["Repos"] = repos
ctx.HTML(200, SETTINGS_REPOS)
ctx.HTML(200, SETTINGS_REPOSITORIES)
} }
func SettingsDeleteRepo(ctx *context.Context) { func SettingsLeaveRepo(ctx *context.Context) {
repo, err := models.GetRepositoryByID(ctx.QueryInt64("id")) repo, err := models.GetRepositoryByID(ctx.QueryInt64("id"))
if err != nil { if err != nil {
ctx.Handle(500, "GetRepositoryByID", err) ctx.NotFoundOrServerError("GetRepositoryByID", errors.IsRepoNotExist, err)
return return
} }
// make sure the user owns the repository or is an admin before allowing them to delete it
if repo.OwnerID == ctx.User.ID || ctx.User.IsAdmin {
if err := models.DeleteRepository(repo.MustOwner().ID, repo.ID); err != nil {
ctx.Handle(500, "DeleteRepository", err)
return
}
log.Trace("Repository deleted: %s/%s", repo.MustOwner().Name, repo.Name)
ctx.Flash.Success(ctx.Tr("repo.settings.deletion_success")) if err = repo.DeleteCollaboration(ctx.User.ID); err != nil {
ctx.JSON(200, map[string]interface{}{ ctx.Handle(500, "DeleteCollaboration", err)
"redirect": setting.AppSubUrl + "/user/settings/repos?page=" + ctx.Query("page") + "&q=" + url.QueryEscape(ctx.Query("q")), return
})
} else {
// logged in user doesn't have rights to delete this repository
err := errors.New("You do not have rights to delete repository '" + repo.FullName() + "'")
ctx.Handle(403, "SettingsDeleteRepo", err)
} }
ctx.Flash.Success(ctx.Tr("settings.repos.leave_success", repo.FullName()))
ctx.JSON(200, map[string]interface{}{
"redirect": setting.AppSubUrl + "/user/settings/repositories",
})
} }
func SettingsDelete(ctx *context.Context) { func SettingsDelete(ctx *context.Context) {

2
templates/.VERSION

@ -1 +1 @@
0.10.25.0322 0.10.26.0323

4
templates/user/settings/navbar.tmpl

@ -22,8 +22,8 @@
<a class="{{if .PageIsSettingsOrganizations}}active{{end}} item" href="{{AppSubUrl}}/user/settings/organizations"> <a class="{{if .PageIsSettingsOrganizations}}active{{end}} item" href="{{AppSubUrl}}/user/settings/organizations">
{{.i18n.Tr "settings.orgs"}} {{.i18n.Tr "settings.orgs"}}
</a> </a>
<a class="{{if .PageIsSettingsRepositories}}active{{end}} item" href="{{AppSubUrl}}/user/settings/repos"> <a class="{{if .PageIsSettingsRepositories}}active{{end}} item" href="{{AppSubUrl}}/user/settings/repositories">
{{.i18n.Tr "admin.repositories"}} {{.i18n.Tr "settings.repos"}}
</a> </a>
<a class="{{if .PageIsSettingsDelete}}active{{end}} item" href="{{AppSubUrl}}/user/settings/delete"> <a class="{{if .PageIsSettingsDelete}}active{{end}} item" href="{{AppSubUrl}}/user/settings/delete">
{{.i18n.Tr "settings.delete"}} {{.i18n.Tr "settings.delete"}}

49
templates/user/settings/repos.tmpl

@ -1,49 +0,0 @@
{{template "base/head" .}}
<div class="user">
<div class="ui container">
<div class="ui grid">
{{template "user/settings/navbar" .}}
<div class="twelve wide column content">
{{template "base/alert" .}}
<h4 class="ui top attached header">
{{.i18n.Tr "admin.repositories"}} ({{.i18n.Tr "admin.total" .Total}})
</h4>
<div class="ui attached segment">
{{template "admin/base/search" .}}
</div>
{{range .Repos}}
<div class="ui attached segment repos">
<div class="ui list">
<div class="item">
<a href="{{AppSubUrl}}/{{.Owner.Name}}/{{.Name}}">
<span class="octicon octicon-repo text light grey"></span>
{{.Owner.Name}}/{{.Name}}
</a>
<span class="ui text light grey">{{.Size | FileSize}}</span>
{{if .IsPrivate}}
<div class="right floated content">
<a class="ui red tiny button inline text-thin delete-button" href="" data-url="{{$.Link}}/leave?page={{$.Page.Current}}" data-id="{{.ID}}">{{$.i18n.Tr "settings.leave"}}</a>
</div>
{{end}}
</div>
</div>
</div>
{{end}}
{{template "admin/base/page" .}}
</div>
</div>
</div>
</div>
<div class="ui small basic leave modal">
<div class="ui icon header">
{{.i18n.Tr "teams.leave"}}
</div>
<div class="content">
<p>{{.i18n.Tr "teams.leave_desc"}}</p>
</div>
{{template "base/delete_modal_actions" .}}
</div>
{{template "base/footer" .}}

54
templates/user/settings/repositories.tmpl

@ -0,0 +1,54 @@
{{template "base/head" .}}
<div class="user settings repositories">
<div class="ui container">
<div class="ui grid">
{{template "user/settings/navbar" .}}
<div class="twelve wide column content">
{{template "base/alert" .}}
<h4 class="ui top attached header">
{{.i18n.Tr "settings.repos"}}
</h4>
<div class="ui attached segment repos">
<div class="ui middle aligned divided list">
{{range .Repos}}
<div class="item">
<span class="text light grey">
{{if .IsPrivate}}
<span class="text gold"><i class="octicon octicon-lock"></i></span>
{{else if .IsFork}}
<i class="octicon octicon-repo-forked"></i>
{{else if .IsMirror}}
<i class="octicon octicon-repo-clone"></i>
{{else}}
<i class="octicon octicon-repo"></i>
{{end}}
</span>
<a href="{{AppSubUrl}}/{{.Owner.Name}}/{{.Name}}">
{{.Owner.Name}}/{{.Name}}
</a>
<span class="ui text light grey">{{.Size | FileSize}}</span>
{{if not (eq .OwnerID $.SignedUserID)}}
<div class="right floated content">
<a class="ui red tiny button inline text-thin delete-button" href="" data-url="{{$.Link}}/leave" data-id="{{.ID}}">{{$.i18n.Tr "settings.repos.leave"}}</a>
</div>
{{end}}
</div>
{{end}}
</div>
</div>
</div>
</div>
</div>
</div>
<div class="ui small basic delete modal">
<div class="ui icon header">
{{.i18n.Tr "settings.repos.leave_title"}}
</div>
<div class="content">
<p>{{.i18n.Tr "settings.repos.leave_desc"}}</p>
</div>
{{template "base/delete_modal_actions" .}}
</div>
{{template "base/footer" .}}
Loading…
Cancel
Save