#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.
tokarchuk/v1.17
Unknwon 9 years ago
parent 05d8664f15
commit 045f14fbd0
  1. 8
      cmd/web.go
  2. 6
      conf/locale/locale_en-US.ini
  3. 21
      models/access.go
  4. 3
      models/issue_comment.go
  5. 100
      models/repo.go
  6. 161
      models/repo_collaboration.go
  7. 4
      modules/bindata/bindata.go
  8. 9
      public/config.codekit
  9. 16
      public/css/gogs.css
  10. 1
      public/css/gogs.min.css
  11. 17
      public/js/gogs.js
  12. 13
      public/less/_base.less
  13. 5
      public/less/_repository.less
  14. 10
      routers/admin/auths.go
  15. 39
      routers/repo/setting.go
  16. 43
      templates/repo/settings/collaboration.tmpl

@ -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)

@ -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

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

@ -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 {

@ -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()
}
// __ __ __ .__
// / \ / \_____ _/ |_ ____ | |__
// \ \/\/ /\__ \\ __\/ ___\| | \

@ -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

@ -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,

@ -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

@ -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;

@ -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 {

@ -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;

@ -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)

@ -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 {

@ -11,14 +11,30 @@
</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}}
<a href="{{AppSubUrl}}/{{.Name}}">
<img class="ui avatar image" src="{{.AvatarLink}}">
{{.DisplayName}}
</a>
<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>
@ -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" .}}

Loading…
Cancel
Save