user/settings: complete repositories panel (#4312)

master
Unknwon 2017-03-23 14:28:31 -04:00
parent beee6e03b1
commit 66c1e6b0e8
No known key found for this signature in database
GPG Key ID: 25B575AE3213B2B3
16 changed files with 175 additions and 122 deletions

View File

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

View File

@ -258,6 +258,7 @@ ssh_keys = SSH Keys
social = Social Accounts
applications = Applications
orgs = Organizations
repos = Repositories
delete = Delete Account
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.
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?
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_prompt = The operation will delete your account permanently, and <strong>CANNOT</strong> be undone!
confirm_delete_account = Confirm Deletion

View File

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

View File

@ -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
// conceptually impossible error handling.
// 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)
}
// 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) {
return x.Count(&Repository{OwnerID: u.ID})
}

View File

@ -7,6 +7,8 @@ package models
import (
"fmt"
log "gopkg.in/clog.v1"
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
func (repo *Repository) IsCollaborator(uid int64) (bool, error) {
// IsCollaborator returns true if the user is a collaborator of the repository.
func IsCollaborator(repoID, userID int64) bool {
collaboration := &Collaboration{
RepoID: repo.ID,
UserID: uid,
RepoID: repoID,
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.
@ -186,10 +196,14 @@ func (repo *Repository) ChangeCollaborationAccessMode(userID int64, mode AccessM
}
// 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{
RepoID: repo.ID,
UserID: uid,
UserID: userID,
}
sess := x.NewSession()
@ -206,3 +220,7 @@ func (repo *Repository) DeleteCollaboration(uid int64) (err error) {
return sess.Commit()
}
func (repo *Repository) DeleteCollaboration(userID int64) error {
return DeleteCollaboration(repo, userID)
}

File diff suppressed because one or more lines are too long

View File

@ -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",
"creatorBuild": "19115",
"creatorBuild": "19127",
"files": {
"\/css\/github.min.css": {
"fileType": 16,
@ -66,7 +66,7 @@
"fileType": 32768,
"ignore": 0,
"ignoreWasSetByUser": 0,
"initialSize": 4048,
"initialSize": 514087,
"inputAbbreviatedPath": "\/img\/avatar_default.png",
"outputAbbreviatedPath": "\/img\/avatar_default.png",
"outputPathIsOutsideProject": 0,

View File

@ -2841,16 +2841,26 @@ footer .ui.language .menu {
.user.settings .email.list .item:not(:first-child) .button {
margin-top: -10px;
}
.user.settings .orgs.non-empty {
.user.settings.organizations .orgs.non-empty {
padding: 0;
}
.user.settings .orgs .item {
.user.settings.organizations .orgs .item {
padding: 10px;
}
.user.settings .orgs .item .button {
.user.settings.organizations .orgs .item .button {
margin-top: 5px;
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 {
display: block;
}

View File

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

View File

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

View File

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

View File

@ -7,11 +7,9 @@ package user
import (
"fmt"
"io/ioutil"
"net/url"
"strings"
"github.com/Unknwon/com"
"github.com/Unknwon/paginater"
log "gopkg.in/clog.v1"
"github.com/gogits/gogs/models"
@ -32,7 +30,7 @@ const (
SETTINGS_SOCIAL base.TplName = "user/settings/social"
SETTINGS_APPLICATIONS base.TplName = "user/settings/applications"
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"
NOTIFICATION base.TplName = "user/notification"
SECURITY base.TplName = "user/security"
@ -446,6 +444,9 @@ func SettingsLeaveOrganization(ctx *context.Context) {
err := models.RemoveOrgUser(ctx.QueryInt64("id"), ctx.User.ID)
if models.IsErrLastOrgOwner(err) {
ctx.Flash.Error(ctx.Tr("form.last_org_owner"))
} else {
ctx.Handle(500, "RemoveOrgUser", err)
return
}
ctx.JSON(200, map[string]interface{}{
@ -454,63 +455,39 @@ func SettingsLeaveOrganization(ctx *context.Context) {
}
func SettingsRepos(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("admin.repositories")
ctx.Data["Title"] = ctx.Tr("settings")
ctx.Data["PageIsSettingsRepositories"] = true
keyword := ctx.Query("q")
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,
})
repos, err := models.GetUserAndCollaborativeRepositories(ctx.User.ID)
if err != nil {
ctx.Handle(500, "SearchRepositoryByName", err)
ctx.Handle(500, "GetUserAndCollaborativeRepositories", err)
return
}
if err = models.RepositoryList(repos).LoadAttributes(); err != nil {
ctx.Handle(500, "LoadAttributes", err)
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.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"))
if err != nil {
ctx.Handle(500, "GetRepositoryByID", err)
ctx.NotFoundOrServerError("GetRepositoryByID", errors.IsRepoNotExist, err)
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"))
ctx.JSON(200, map[string]interface{}{
"redirect": setting.AppSubUrl + "/user/settings/repos?page=" + ctx.Query("page") + "&q=" + url.QueryEscape(ctx.Query("q")),
})
} 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)
if err = repo.DeleteCollaboration(ctx.User.ID); err != nil {
ctx.Handle(500, "DeleteCollaboration", err)
return
}
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) {

View File

@ -1 +1 @@
0.10.25.0322
0.10.26.0323

View File

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

View File

@ -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" .}}

View File

@ -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" .}}