#1146 finsih UI work for access mode of collaborators

Collaborators have write access as default, and can be changed via repository
collaboration settings page to change between read, write and admin.
This commit is contained in:
Unknwon 2016-03-05 18:08:42 -05:00
parent 05d8664f15
commit 045f14fbd0
16 changed files with 292 additions and 164 deletions

View File

@ -190,6 +190,8 @@ func runWeb(ctx *cli.Context) {
bindIgnErr := binding.BindIgnErr
// FIXME: not all routes need go through same middlewares.
// Especially some AJAX requests, we can reduce middleware number to improve performance.
// Routers.
m.Get("/", ignSignIn, routers.Home)
m.Get("/explore", ignSignIn, routers.Explore)
@ -400,7 +402,11 @@ func runWeb(ctx *cli.Context) {
m.Group("/settings", func() {
m.Combo("").Get(repo.Settings).
Post(bindIgnErr(auth.RepoSettingForm{}), repo.SettingsPost)
m.Combo("/collaboration").Get(repo.Collaboration).Post(repo.CollaborationPost)
m.Group("/collaboration", func() {
m.Combo("").Get(repo.Collaboration).Post(repo.CollaborationPost)
m.Post("/access_mode", repo.ChangeCollaborationAccessMode)
m.Post("/delete", repo.DeleteCollaboration)
})
m.Group("/hooks", func() {
m.Get("", repo.Webhooks)

View File

@ -221,8 +221,6 @@ still_own_repo = Your account still has ownership over at least one repository,
still_has_org = Your account still has membership in at least one organization, you have to leave or delete your memberships first.
org_still_own_repo = This organization still has ownership of repositories, you must delete or transfer them first.
still_own_user = This authentication is still in use by at least one user, please remove them from the authentication and try again.
target_branch_not_exist = Target branch does not exist.
[user]
@ -615,6 +613,9 @@ settings.transfer_succeed = Repository ownership has been transferred successful
settings.confirm_delete = Confirm Deletion
settings.add_collaborator = Add New Collaborator
settings.add_collaborator_success = New collaborator has been added.
settings.delete_collaborator = Delete
settings.collaborator_deletion = Collaborator Deletion
settings.collaborator_deletion_desc = This user will no longer have collaboration access to this repository after deletion. Do you want to continue?
settings.remove_collaborator_success = Collaborator has been removed.
settings.search_user_placeholder = Search user...
settings.org_not_allowed_to_be_collaborator = Organization is not allowed to be added as a collaborator.
@ -949,6 +950,7 @@ auths.update = Update Authentication Setting
auths.delete = Delete This Authentication
auths.delete_auth_title = Authentication Deletion
auths.delete_auth_desc = This authentication is going to be deleted, do you want to continue?
auths.still_in_used = This authentication is still used by some users, please delete or convert these users to another login type first.
auths.deletion_success = Authentication has been deleted successfully!
config.server_config = Server Configuration

View File

@ -13,11 +13,11 @@ import (
type AccessMode int
const (
ACCESS_MODE_NONE AccessMode = iota
ACCESS_MODE_READ
ACCESS_MODE_WRITE
ACCESS_MODE_ADMIN
ACCESS_MODE_OWNER
ACCESS_MODE_NONE AccessMode = iota // 0
ACCESS_MODE_READ // 1
ACCESS_MODE_WRITE // 2
ACCESS_MODE_ADMIN // 3
ACCESS_MODE_OWNER // 4
)
// Access represents the highest access level of a user to the repository. The only access type
@ -151,15 +151,14 @@ func (repo *Repository) refreshAccesses(e Engine, accessMap map[int64]AccessMode
return nil
}
// FIXME: should be able to have read-only access.
// Give all collaborators write access.
// refreshCollaboratorAccesses retrieves repository collaborations with their access modes.
func (repo *Repository) refreshCollaboratorAccesses(e Engine, accessMap map[int64]AccessMode) error {
collaborators, err := repo.getCollaborators(e)
collaborations, err := repo.getCollaborations(e)
if err != nil {
return fmt.Errorf("getCollaborators: %v", err)
return fmt.Errorf("getCollaborations: %v", err)
}
for _, c := range collaborators {
accessMap[c.Id] = ACCESS_MODE_WRITE
for _, c := range collaborations {
accessMap[c.UserID] = c.Mode
}
return nil
}

View File

@ -121,7 +121,7 @@ func createComment(e *xorm.Session, opts *CreateCommentOptions) (_ *Comment, err
return nil, err
}
// Compose comment action, could be plain comment, close or reopen issue.
// Compose comment action, could be plain comment, close or reopen issue/pull request.
// This object will be used to notify watchers in the end of function.
act := &Action{
ActUserID: opts.Doer.Id,
@ -179,6 +179,7 @@ func createComment(e *xorm.Session, opts *CreateCommentOptions) (_ *Comment, err
if err != nil {
return nil, err
}
case COMMENT_TYPE_CLOSE:
act.OpType = ACTION_CLOSE_ISSUE
if opts.Issue.IsPull {

View File

@ -330,7 +330,6 @@ func (repo *Repository) RepoRelLink() string {
return "/" + repo.MustOwner().Name + "/" + repo.Name
}
func (repo *Repository) ComposeCompareURL(oldCommitID, newCommitID string) string {
return fmt.Sprintf("%s/%s/compare/%s...%s", repo.MustOwner().Name, repo.Name, oldCommitID, newCommitID)
}
@ -1797,105 +1796,6 @@ func CheckRepoStats() {
// ***** END: Repository.NumForks *****
}
// _________ .__ .__ ___. __ .__
// \_ ___ \ ____ | | | | _____ \_ |__ ________________ _/ |_|__| ____ ____
// / \ \/ / _ \| | | | \__ \ | __ \ / _ \_ __ \__ \\ __\ |/ _ \ / \
// \ \___( <_> ) |_| |__/ __ \| \_\ ( <_> ) | \// __ \| | | ( <_> ) | \
// \______ /\____/|____/____(____ /___ /\____/|__| (____ /__| |__|\____/|___| /
// \/ \/ \/ \/ \/
// A Collaboration is a relation between an individual and a repository
type Collaboration struct {
ID int64 `xorm:"pk autoincr"`
RepoID int64 `xorm:"UNIQUE(s) INDEX NOT NULL"`
UserID int64 `xorm:"UNIQUE(s) INDEX NOT NULL"`
Created time.Time `xorm:"CREATED"`
}
// Add collaborator and accompanying access
func (repo *Repository) AddCollaborator(u *User) error {
collaboration := &Collaboration{
RepoID: repo.ID,
UserID: u.Id,
}
has, err := x.Get(collaboration)
if err != nil {
return err
} else if has {
return nil
}
if err = repo.GetOwner(); err != nil {
return fmt.Errorf("GetOwner: %v", err)
}
sess := x.NewSession()
defer sessionRelease(sess)
if err = sess.Begin(); err != nil {
return err
}
if _, err = sess.InsertOne(collaboration); err != nil {
return err
}
if repo.Owner.IsOrganization() {
err = repo.recalculateTeamAccesses(sess, 0)
} else {
err = repo.recalculateAccesses(sess)
}
if err != nil {
return fmt.Errorf("recalculateAccesses 'team=%v': %v", repo.Owner.IsOrganization(), err)
}
return sess.Commit()
}
func (repo *Repository) getCollaborators(e Engine) ([]*User, error) {
collaborations := make([]*Collaboration, 0)
if err := e.Find(&collaborations, &Collaboration{RepoID: repo.ID}); err != nil {
return nil, err
}
users := make([]*User, len(collaborations))
for i, c := range collaborations {
user, err := getUserByID(e, c.UserID)
if err != nil {
return nil, err
}
users[i] = user
}
return users, nil
}
// GetCollaborators returns the collaborators for a repository
func (repo *Repository) GetCollaborators() ([]*User, error) {
return repo.getCollaborators(x)
}
// Delete collaborator and accompanying access
func (repo *Repository) DeleteCollaborator(u *User) (err error) {
collaboration := &Collaboration{
RepoID: repo.ID,
UserID: u.Id,
}
sess := x.NewSession()
defer sessionRelease(sess)
if err = sess.Begin(); err != nil {
return err
}
if has, err := sess.Delete(collaboration); err != nil || has == 0 {
return err
} else if err = repo.recalculateAccesses(sess); err != nil {
return err
}
return sess.Commit()
}
// __ __ __ .__
// / \ / \_____ _/ |_ ____ | |__
// \ \/\/ /\__ \\ __\/ ___\| | \

View File

@ -0,0 +1,161 @@
// Copyright 2016 The Gogs 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 (
"fmt"
"time"
)
// Collaboration represent the relation between an individual and a repository.
type Collaboration struct {
ID int64 `xorm:"pk autoincr"`
RepoID int64 `xorm:"UNIQUE(s) INDEX NOT NULL"`
UserID int64 `xorm:"UNIQUE(s) INDEX NOT NULL"`
Mode AccessMode `xorm:"DEFAULT 2 NOT NULL"`
Created time.Time `xorm:"CREATED"`
}
func (c *Collaboration) ModeName() string {
switch c.Mode {
case ACCESS_MODE_READ:
return "Read"
case ACCESS_MODE_WRITE:
return "Write"
case ACCESS_MODE_ADMIN:
return "Admin"
}
return "Undefined"
}
// AddCollaborator adds new collaboration relation between an individual and a repository.
func (repo *Repository) AddCollaborator(u *User) error {
collaboration := &Collaboration{
RepoID: repo.ID,
UserID: u.Id,
}
has, err := x.Get(collaboration)
if err != nil {
return err
} else if has {
return nil
}
collaboration.Mode = ACCESS_MODE_WRITE
sess := x.NewSession()
defer sessionRelease(sess)
if err = sess.Begin(); err != nil {
return err
}
if _, err = sess.InsertOne(collaboration); err != nil {
return err
}
if repo.Owner.IsOrganization() {
err = repo.recalculateTeamAccesses(sess, 0)
} else {
err = repo.recalculateAccesses(sess)
}
if err != nil {
return fmt.Errorf("recalculateAccesses 'team=%v': %v", repo.Owner.IsOrganization(), err)
}
return sess.Commit()
}
func (repo *Repository) getCollaborations(e Engine) ([]*Collaboration, error) {
collaborations := make([]*Collaboration, 0)
return collaborations, e.Find(&collaborations, &Collaboration{RepoID: repo.ID})
}
// Collaborator represents a user with collaboration details.
type Collaborator struct {
*User
Collaboration *Collaboration
}
func (repo *Repository) getCollaborators(e Engine) ([]*Collaborator, error) {
collaborations, err := repo.getCollaborations(e)
if err != nil {
return nil, fmt.Errorf("getCollaborations: %v", err)
}
collaborators := make([]*Collaborator, len(collaborations))
for i, c := range collaborations {
user, err := getUserByID(e, c.UserID)
if err != nil {
return nil, err
}
collaborators[i] = &Collaborator{
User: user,
Collaboration: c,
}
}
return collaborators, nil
}
// GetCollaborators returns the collaborators for a repository
func (repo *Repository) GetCollaborators() ([]*Collaborator, error) {
return repo.getCollaborators(x)
}
// ChangeCollaborationAccessMode sets new access mode for the collaboration.
func (repo *Repository) ChangeCollaborationAccessMode(uid int64, mode AccessMode) error {
// Discard invalid input
if mode <= ACCESS_MODE_NONE || mode > ACCESS_MODE_OWNER {
return nil
}
collaboration := &Collaboration{
RepoID: repo.ID,
UserID: uid,
}
has, err := x.Get(collaboration)
if err != nil {
return fmt.Errorf("get collaboration: %v", err)
} else if !has {
return nil
}
collaboration.Mode = mode
sess := x.NewSession()
defer sessionRelease(sess)
if err = sess.Begin(); err != nil {
return err
}
if _, err = sess.Id(collaboration.ID).AllCols().Update(collaboration); err != nil {
return fmt.Errorf("update collaboration: %v", err)
} else if _, err = sess.Exec("UPDATE access SET mode = ? WHERE user_id = ? AND repo_id = ?", mode, uid, repo.ID); err != nil {
return fmt.Errorf("update access table: %v", err)
}
return sess.Commit()
}
// DeleteCollaboration removes collaboration relation between the user and repository.
func (repo *Repository) DeleteCollaboration(uid int64) (err error) {
collaboration := &Collaboration{
RepoID: repo.ID,
UserID: uid,
}
sess := x.NewSession()
defer sessionRelease(sess)
if err = sess.Begin(); err != nil {
return err
}
if has, err := sess.Delete(collaboration); err != nil || has == 0 {
return err
} else if err = repo.recalculateAccesses(sess); err != nil {
return err
}
return sess.Commit()
}

File diff suppressed because one or more lines are too long

View File

@ -20,15 +20,6 @@
"outputPathIsOutsideProject": 0,
"outputPathIsSetByUser": 0
},
"\/css\/gogs.min.css": {
"fileType": 16,
"ignore": 1,
"ignoreWasSetByUser": 0,
"inputAbbreviatedPath": "\/css\/gogs.min.css",
"outputAbbreviatedPath": "No Output Path",
"outputPathIsOutsideProject": 0,
"outputPathIsSetByUser": 0
},
"\/css\/semantic-2.1.8.min.css": {
"fileType": 16,
"ignore": 0,

View File

@ -5,7 +5,7 @@
background-size: contain;
}
body {
font-family: 'Helvetica Neue', Arial, Helvetica, sans-serif, '微软雅黑';
font-family: "Helvetica Neue", "Microsoft YaHei", Arial, Helvetica, sans-serif !important;
background-color: #fff;
overflow-y: scroll;
}
@ -104,6 +104,9 @@ code.wrap {
.ui.container.fluid.padded {
padding: 0 10px 0 10px;
}
.ui.form .ui.button {
font-weight: normal;
}
.ui .text.red {
color: #d95c5c !important;
}
@ -234,6 +237,10 @@ code.wrap {
.ui.status.buttons .octicon {
margin-right: 4px;
}
.ui.inline.delete-button {
padding: 8px 15px;
font-weight: normal;
}
.overflow.menu .items {
max-height: 300px;
overflow-y: auto;
@ -1984,10 +1991,11 @@ footer .container .links > *:first-child {
.repository.settings.collaboration .collaborator.list {
padding: 0;
}
.repository.settings.collaboration .collaborator.list .item {
padding: 10px 20px;
.repository.settings.collaboration .collaborator.list > .item {
margin: 0;
line-height: 2em;
}
.repository.settings.collaboration .collaborator.list .item:not(:last-child) {
.repository.settings.collaboration .collaborator.list > .item:not(:last-child) {
border-bottom: 1px solid #DDD;
}
.repository.settings.collaboration #repo-collab-form #search-user-box .results {

File diff suppressed because one or more lines are too long

View File

@ -458,6 +458,20 @@ function initRepository() {
}
}
function initRepositoryCollaboration(){
console.log('initRepositoryCollaboration');
// Change collaborator access mode
$('.access-mode.menu .item').click(function(){
var $menu = $(this).parent();
$.post($menu.data('url'), {
"_csrf": csrf,
"uid": $menu.data('uid'),
"mode": $(this).data('value')
})
});
}
function initWiki() {
if ($('.repository.wiki').length == 0) {
return;
@ -964,7 +978,8 @@ $(document).ready(function () {
initAdmin();
var routes = {
'div.user.settings': initUserSettings
'div.user.settings': initUserSettings,
'div.repository.settings.collaboration': initRepositoryCollaboration
};
var selector;

View File

@ -1,7 +1,7 @@
@footer-margin: 40px;
body {
font-family: 'Helvetica Neue',Arial,Helvetica,sans-serif,'微软雅黑';
font-family: "Helvetica Neue", "Microsoft YaHei", Arial, Helvetica, sans-serif !important;
background-color: #fff;
overflow-y: scroll;
}
@ -109,6 +109,12 @@ pre, code {
}
}
&.form {
.ui.button {
font-weight: normal;
}
}
.text {
&.red {
color: #d95c5c !important;
@ -260,6 +266,11 @@ pre, code {
margin-right: 4px;
}
}
&.inline.delete-button {
padding: 8px 15px;
font-weight: normal;
}
}
.overflow.menu {

View File

@ -1026,8 +1026,9 @@
.collaborator.list {
padding: 0;
.item {
padding: 10px 20px;
>.item {
margin: 0;
line-height: 2em;
&:not(:last-child) {
border-bottom: 1px solid #DDD;

View File

@ -5,6 +5,8 @@
package admin
import (
"fmt"
"github.com/Unknwon/com"
"github.com/go-xorm/core"
@ -218,11 +220,13 @@ func DeleteAuthSource(ctx *middleware.Context) {
if err = models.DeleteSource(source); err != nil {
switch err {
case models.ErrAuthenticationUserUsed:
ctx.Flash.Error("form.still_own_user")
ctx.Redirect(setting.AppSubUrl + "/admin/auths/" + ctx.Params(":authid"))
ctx.Flash.Error(ctx.Tr("admin.auths.still_in_used"))
default:
ctx.Handle(500, "DeleteSource", err)
ctx.Flash.Error(fmt.Sprintf("DeleteSource: %v", err))
}
ctx.JSON(200, map[string]interface{}{
"redirect": setting.AppSubUrl + "/admin/auths/" + ctx.Params(":authid"),
})
return
}
log.Trace("Authentication deleted by admin(%s): %d", ctx.User.Name, source.ID)

View File

@ -257,30 +257,13 @@ func Collaboration(ctx *middleware.Context) {
ctx.Data["Title"] = ctx.Tr("repo.settings")
ctx.Data["PageIsSettingsCollaboration"] = true
// Delete collaborator.
remove := strings.ToLower(ctx.Query("remove"))
if len(remove) > 0 && remove != ctx.Repo.Owner.LowerName {
u, err := models.GetUserByName(remove)
if err != nil {
ctx.Handle(500, "GetUserByName", err)
return
}
if err := ctx.Repo.Repository.DeleteCollaborator(u); err != nil {
ctx.Handle(500, "DeleteCollaborator", err)
return
}
ctx.Flash.Success(ctx.Tr("repo.settings.remove_collaborator_success"))
ctx.Redirect(ctx.Repo.RepoLink + "/settings/collaboration")
return
}
users, err := ctx.Repo.Repository.GetCollaborators()
if err != nil {
ctx.Handle(500, "GetCollaborators", err)
return
}
ctx.Data["Collaborators"] = users
ctx.HTML(200, COLLABORATION)
}
@ -332,6 +315,26 @@ func CollaborationPost(ctx *middleware.Context) {
ctx.Redirect(setting.AppSubUrl + ctx.Req.URL.Path)
}
func ChangeCollaborationAccessMode(ctx *middleware.Context) {
if err := ctx.Repo.Repository.ChangeCollaborationAccessMode(
ctx.QueryInt64("uid"),
models.AccessMode(ctx.QueryInt("mode"))); err != nil {
log.Error(4, "ChangeCollaborationAccessMode: %v", err)
}
}
func DeleteCollaboration(ctx *middleware.Context) {
if err := ctx.Repo.Repository.DeleteCollaboration(ctx.QueryInt64("id")); err != nil {
ctx.Flash.Error("DeleteCollaboration: " + err.Error())
} else {
ctx.Flash.Success(ctx.Tr("repo.settings.remove_collaborator_success"))
}
ctx.JSON(200, map[string]interface{}{
"redirect": ctx.Repo.RepoLink + "/settings/collaboration",
})
}
func parseOwnerAndRepo(ctx *middleware.Context) (*models.User, *models.Repository) {
owner, err := models.GetUserByName(ctx.Params(":username"))
if err != nil {

View File

@ -11,15 +11,31 @@
</h4>
<div class="ui attached segment collaborator list">
{{range .Collaborators}}
<div class="item">
{{if not (eq .Id $.Owner.Id)}}
<a href="{{$.RepoLink}}/settings/collaboration?remove={{.Name}}" class="ui right text red"><i class="fa fa-times"></i></a>
{{end}}
<div class="item ui grid">
<div class="ui five wide column">
<a href="{{AppSubUrl}}/{{.Name}}">
<img class="ui avatar image" src="{{.AvatarLink}}">
{{.DisplayName}}
</a>
</div>
<div class="ui eight wide column">
<span class="octicon octicon-shield"></span>
<div class="ui inline dropdown">
<div class="text">{{.Collaboration.ModeName}}</div>
<i class="dropdown icon"></i>
<div class="access-mode menu" data-url="{{$.Link}}/access_mode" data-uid="{{.Id}}">
<div class="item" data-text="Admin" data-value="3">Admin</div>
<div class="item" data-text="Write" data-value="2">Write</div>
<div class="item" data-text="Read" data-value="1">Read</div>
</div>
</div>
</div>
<div class="ui two wide column">
<button class="ui red tiny button inline text-thin delete-button" data-url="{{$.Link}}/delete" data-id="{{.Id}}">
{{$.i18n.Tr "repo.settings.delete_collaborator"}}
</button>
</div>
</div>
{{end}}
</div>
<div class="ui bottom attached segment">
@ -40,4 +56,15 @@
</div>
</div>
</div>
<div class="ui small basic delete modal">
<div class="ui icon header">
<i class="trash icon"></i>
{{.i18n.Tr "repo.settings.collaborator_deletion"}}
</div>
<div class="content">
<p>{{.i18n.Tr "repo.settings.collaborator_deletion_desc"}}</p>
</div>
{{template "base/delete_modal_actions" .}}
</div>
{{template "base/footer" .}}