@@ -76,7 +76,6 @@ func runWeb(*cli.Context) { | |||
m.Get("/issues", reqSignIn, user.Issues) | |||
m.Get("/pulls", reqSignIn, user.Pulls) | |||
m.Get("/stars", reqSignIn, user.Stars) | |||
m.Get("/help", routers.Help) | |||
m.Group("/api", func(r martini.Router) { | |||
m.Group("/v1", func(r martini.Router) { | |||
@@ -191,9 +190,12 @@ func runWeb(*cli.Context) { | |||
r.Get("/new", repo.CreateIssue) | |||
r.Post("/new", bindIgnErr(auth.CreateIssueForm{}), repo.CreateIssuePost) | |||
r.Post("/:index", bindIgnErr(auth.CreateIssueForm{}), repo.UpdateIssue) | |||
r.Post("/:index/assignee", repo.UpdateAssignee) | |||
r.Post("/:index/label", repo.UpdateIssueLabel) | |||
r.Post("/:index/milestone", repo.UpdateIssueMilestone) | |||
r.Post("/:index/assignee", repo.UpdateAssignee) | |||
r.Post("/labels/new", bindIgnErr(auth.CreateLabelForm{}), repo.NewLabel) | |||
r.Post("/labels/edit", bindIgnErr(auth.CreateLabelForm{}), repo.UpdateLabel) | |||
r.Post("/labels/delete", repo.DeleteLabel) | |||
r.Get("/milestones", repo.Milestones) | |||
r.Get("/milestones/new", repo.NewMilestone) | |||
r.Post("/milestones/new", bindIgnErr(auth.CreateMilestoneForm{}), repo.NewMilestonePost) |
@@ -17,7 +17,7 @@ import ( | |||
"github.com/gogits/gogs/modules/base" | |||
) | |||
const APP_VER = "0.3.5.0521 Alpha" | |||
const APP_VER = "0.3.5.0523 Alpha" | |||
func init() { | |||
base.AppVer = APP_VER |
@@ -17,6 +17,7 @@ import ( | |||
var ( | |||
ErrIssueNotExist = errors.New("Issue does not exist") | |||
ErrLabelNotExist = errors.New("Label does not exist") | |||
ErrMilestoneNotExist = errors.New("Milestone does not exist") | |||
) | |||
@@ -28,14 +29,15 @@ type Issue struct { | |||
Name string | |||
Repo *Repository `xorm:"-"` | |||
PosterId int64 | |||
Poster *User `xorm:"-"` | |||
Poster *User `xorm:"-"` | |||
LabelIds string `xorm:"TEXT"` | |||
Labels []*Label `xorm:"-"` | |||
MilestoneId int64 | |||
AssigneeId int64 | |||
Assignee *User `xorm:"-"` | |||
IsRead bool `xorm:"-"` | |||
IsPull bool // Indicates whether is a pull request or not. | |||
IsClosed bool | |||
Labels string `xorm:"TEXT"` | |||
Content string `xorm:"TEXT"` | |||
RenderedContent string `xorm:"-"` | |||
Priority int | |||
@@ -54,11 +56,37 @@ func (i *Issue) GetPoster() (err error) { | |||
return err | |||
} | |||
func (i *Issue) GetLabels() error { | |||
if len(i.LabelIds) < 3 { | |||
return nil | |||
} | |||
strIds := strings.Split(strings.TrimSuffix(i.LabelIds[1:], "|"), "|$") | |||
i.Labels = make([]*Label, 0, len(strIds)) | |||
for _, strId := range strIds { | |||
id, _ := base.StrTo(strId).Int64() | |||
if id > 0 { | |||
l, err := GetLabelById(id) | |||
if err != nil { | |||
if err == ErrLabelNotExist { | |||
continue | |||
} | |||
return err | |||
} | |||
i.Labels = append(i.Labels, l) | |||
} | |||
} | |||
return nil | |||
} | |||
func (i *Issue) GetAssignee() (err error) { | |||
if i.AssigneeId == 0 { | |||
return nil | |||
} | |||
i.Assignee, err = GetUserById(i.AssigneeId) | |||
if err == ErrUserNotExist { | |||
return nil | |||
} | |||
return err | |||
} | |||
@@ -108,7 +136,7 @@ func GetIssueById(id int64) (*Issue, error) { | |||
} | |||
// GetIssues returns a list of issues by given conditions. | |||
func GetIssues(uid, rid, pid, mid int64, page int, isClosed bool, labels, sortType string) ([]Issue, error) { | |||
func GetIssues(uid, rid, pid, mid int64, page int, isClosed bool, labelIds, sortType string) ([]Issue, error) { | |||
sess := orm.Limit(20, (page-1)*20) | |||
if rid > 0 { | |||
@@ -127,9 +155,9 @@ func GetIssues(uid, rid, pid, mid int64, page int, isClosed bool, labels, sortTy | |||
sess.And("milestone_id=?", mid) | |||
} | |||
if len(labels) > 0 { | |||
for _, label := range strings.Split(labels, ",") { | |||
sess.And("labels like '%$" + label + "|%'") | |||
if len(labelIds) > 0 { | |||
for _, label := range strings.Split(labelIds, ",") { | |||
sess.And("label_ids like '%$" + label + "|%'") | |||
} | |||
} | |||
@@ -155,6 +183,13 @@ func GetIssues(uid, rid, pid, mid int64, page int, isClosed bool, labels, sortTy | |||
return issues, err | |||
} | |||
// GetIssuesByLabel returns a list of issues by given label and repository. | |||
func GetIssuesByLabel(repoId int64, label string) ([]*Issue, error) { | |||
issues := make([]*Issue, 0, 10) | |||
err := orm.Where("repo_id=?", repoId).And("label_ids like '%$" + label + "|%'").Find(&issues) | |||
return issues, err | |||
} | |||
// GetIssueCountByPoster returns number of issues of repository by poster. | |||
func GetIssueCountByPoster(uid, rid int64, isClosed bool) int64 { | |||
count, _ := orm.Where("repo_id=?", rid).And("poster_id=?", uid).And("is_closed=?", isClosed).Count(new(Issue)) | |||
@@ -175,7 +210,6 @@ type IssueUser struct { | |||
IssueId int64 | |||
RepoId int64 | |||
MilestoneId int64 | |||
Labels string `xorm:"TEXT"` | |||
IsRead bool | |||
IsAssigned bool | |||
IsMentioned bool | |||
@@ -400,6 +434,98 @@ func UpdateIssueUserPairsByMentions(uids []int64, iid int64) error { | |||
return nil | |||
} | |||
// .____ ___. .__ | |||
// | | _____ \_ |__ ____ | | | |||
// | | \__ \ | __ \_/ __ \| | | |||
// | |___ / __ \| \_\ \ ___/| |__ | |||
// |_______ (____ /___ /\___ >____/ | |||
// \/ \/ \/ \/ | |||
// Label represents a label of repository for issues. | |||
type Label struct { | |||
Id int64 | |||
RepoId int64 `xorm:"INDEX"` | |||
Name string | |||
Color string `xorm:"VARCHAR(7)"` | |||
NumIssues int | |||
NumClosedIssues int | |||
NumOpenIssues int `xorm:"-"` | |||
IsChecked bool `xorm:"-"` | |||
} | |||
// CalOpenIssues calculates the open issues of label. | |||
func (m *Label) CalOpenIssues() { | |||
m.NumOpenIssues = m.NumIssues - m.NumClosedIssues | |||
} | |||
// NewLabel creates new label of repository. | |||
func NewLabel(l *Label) error { | |||
_, err := orm.Insert(l) | |||
return err | |||
} | |||
// GetLabelById returns a label by given ID. | |||
func GetLabelById(id int64) (*Label, error) { | |||
l := &Label{Id: id} | |||
has, err := orm.Get(l) | |||
if err != nil { | |||
return nil, err | |||
} else if !has { | |||
return nil, ErrLabelNotExist | |||
} | |||
return l, nil | |||
} | |||
// GetLabels returns a list of labels of given repository ID. | |||
func GetLabels(repoId int64) ([]*Label, error) { | |||
labels := make([]*Label, 0, 10) | |||
err := orm.Where("repo_id=?", repoId).Find(&labels) | |||
return labels, err | |||
} | |||
// UpdateLabel updates label information. | |||
func UpdateLabel(l *Label) error { | |||
_, err := orm.Id(l.Id).Update(l) | |||
return err | |||
} | |||
// DeleteLabel delete a label of given repository. | |||
func DeleteLabel(repoId int64, strId string) error { | |||
id, _ := base.StrTo(strId).Int64() | |||
l, err := GetLabelById(id) | |||
if err != nil { | |||
if err == ErrLabelNotExist { | |||
return nil | |||
} | |||
return err | |||
} | |||
issues, err := GetIssuesByLabel(repoId, strId) | |||
if err != nil { | |||
return err | |||
} | |||
sess := orm.NewSession() | |||
defer sess.Close() | |||
if err = sess.Begin(); err != nil { | |||
return err | |||
} | |||
for _, issue := range issues { | |||
issue.LabelIds = strings.Replace(issue.LabelIds, "$"+strId+"|", "", -1) | |||
if _, err = sess.Id(issue.Id).AllCols().Update(issue); err != nil { | |||
sess.Rollback() | |||
return err | |||
} | |||
} | |||
if _, err = sess.Delete(l); err != nil { | |||
sess.Rollback() | |||
return err | |||
} | |||
return sess.Commit() | |||
} | |||
// _____ .__.__ __ | |||
// / \ |__| | ____ _______/ |_ ____ ____ ____ | |||
// / \ / \| | | _/ __ \ / ___/\ __\/ _ \ / \_/ __ \ | |||
@@ -611,42 +737,6 @@ func DeleteMilestone(m *Milestone) (err error) { | |||
return sess.Commit() | |||
} | |||
// .____ ___. .__ | |||
// | | _____ \_ |__ ____ | | | |||
// | | \__ \ | __ \_/ __ \| | | |||
// | |___ / __ \| \_\ \ ___/| |__ | |||
// |_______ (____ /___ /\___ >____/ | |||
// \/ \/ \/ \/ | |||
// Label represents a label of repository for issues. | |||
type Label struct { | |||
Id int64 | |||
RepoId int64 `xorm:"INDEX"` | |||
Name string | |||
Color string `xorm:"VARCHAR(7)"` | |||
NumIssues int | |||
NumClosedIssues int | |||
NumOpenIssues int `xorm:"-"` | |||
} | |||
// CalOpenIssues calculates the open issues of label. | |||
func (m *Label) CalOpenIssues() { | |||
m.NumOpenIssues = m.NumIssues - m.NumClosedIssues | |||
} | |||
// NewLabel creates new label of repository. | |||
func NewLabel(l *Label) error { | |||
_, err := orm.Insert(l) | |||
return err | |||
} | |||
// GetLabels returns a list of labels of given repository ID. | |||
func GetLabels(repoId int64) ([]*Label, error) { | |||
labels := make([]*Label, 0, 10) | |||
err := orm.Where("repo_id=?", repoId).Find(&labels) | |||
return labels, err | |||
} | |||
// _________ __ | |||
// \_ ___ \ ____ _____ _____ ____ _____/ |_ | |||
// / \ \/ / _ \ / \ / \_/ __ \ / \ __\ |
@@ -32,9 +32,8 @@ func Home(ctx *middleware.Context) { | |||
} | |||
for _, repo := range repos { | |||
repo.Owner, err = models.GetUserById(repo.OwnerId) | |||
if err != nil { | |||
ctx.Handle(500, "dashboard.Home(GetUserById)", err) | |||
if err = repo.GetOwner(); err != nil { | |||
ctx.Handle(500, "dashboard.Home(GetOwner)", err) | |||
return | |||
} | |||
} | |||
@@ -43,12 +42,6 @@ func Home(ctx *middleware.Context) { | |||
ctx.HTML(200, "home") | |||
} | |||
func Help(ctx *middleware.Context) { | |||
ctx.Data["PageIsHelp"] = true | |||
ctx.Data["Title"] = "Help" | |||
ctx.HTML(200, "help") | |||
} | |||
func NotFound(ctx *middleware.Context) { | |||
ctx.Data["PageIsNotFound"] = true | |||
ctx.Data["Title"] = "Page Not Found" |
@@ -25,7 +25,6 @@ import ( | |||
"github.com/gogits/gogs/modules/social" | |||
) | |||
// Check run mode(Default of martini is Dev). | |||
func checkRunMode() { | |||
switch base.Cfg.MustValue("", "RUN_MODE") { | |||
case "prod": |
@@ -197,7 +197,7 @@ func CreateIssuePost(ctx *middleware.Context, params martini.Params, form auth.C | |||
PosterId: ctx.User.Id, | |||
MilestoneId: form.MilestoneId, | |||
AssigneeId: form.AssigneeId, | |||
Labels: form.Labels, | |||
LabelIds: form.Labels, | |||
Content: form.Content, | |||
} | |||
if err := models.NewIssue(issue); err != nil { | |||
@@ -269,6 +269,17 @@ func CreateIssuePost(ctx *middleware.Context, params martini.Params, form auth.C | |||
ctx.Redirect(fmt.Sprintf("/%s/%s/issues/%d", params["username"], params["reponame"], issue.Index)) | |||
} | |||
func checkLabels(labels, allLabels []*models.Label) { | |||
for _, l := range labels { | |||
for _, l2 := range allLabels { | |||
if l.Id == l2.Id { | |||
l2.IsChecked = true | |||
break | |||
} | |||
} | |||
} | |||
} | |||
func ViewIssue(ctx *middleware.Context, params martini.Params) { | |||
idx, _ := base.StrTo(params["index"]).Int64() | |||
if idx == 0 { | |||
@@ -286,6 +297,19 @@ func ViewIssue(ctx *middleware.Context, params martini.Params) { | |||
return | |||
} | |||
// Get labels. | |||
if err = issue.GetLabels(); err != nil { | |||
ctx.Handle(500, "issue.ViewIssue(GetLabels)", err) | |||
return | |||
} | |||
labels, err := models.GetLabels(ctx.Repo.Repository.Id) | |||
if err != nil { | |||
ctx.Handle(500, "issue.ViewIssue(GetLabels.2)", err) | |||
return | |||
} | |||
checkLabels(issue.Labels, labels) | |||
ctx.Data["Labels"] = labels | |||
// Get assigned milestone. | |||
if issue.MilestoneId > 0 { | |||
ctx.Data["Milestone"], err = models.GetMilestoneById(issue.MilestoneId) | |||
@@ -364,13 +388,13 @@ func ViewIssue(ctx *middleware.Context, params martini.Params) { | |||
} | |||
func UpdateIssue(ctx *middleware.Context, params martini.Params, form auth.CreateIssueForm) { | |||
idx, err := base.StrTo(params["index"]).Int() | |||
if err != nil { | |||
idx, _ := base.StrTo(params["index"]).Int64() | |||
if idx <= 0 { | |||
ctx.Error(404) | |||
return | |||
} | |||
issue, err := models.GetIssueByIndex(ctx.Repo.Repository.Id, int64(idx)) | |||
issue, err := models.GetIssueByIndex(ctx.Repo.Repository.Id, idx) | |||
if err != nil { | |||
if err == models.ErrIssueNotExist { | |||
ctx.Handle(404, "issue.UpdateIssue", err) | |||
@@ -381,14 +405,14 @@ func UpdateIssue(ctx *middleware.Context, params martini.Params, form auth.Creat | |||
} | |||
if ctx.User.Id != issue.PosterId && !ctx.Repo.IsOwner { | |||
ctx.Handle(404, "issue.UpdateIssue", nil) | |||
ctx.Error(403) | |||
return | |||
} | |||
issue.Name = form.IssueName | |||
issue.MilestoneId = form.MilestoneId | |||
issue.AssigneeId = form.AssigneeId | |||
issue.Labels = form.Labels | |||
issue.LabelIds = form.Labels | |||
issue.Content = form.Content | |||
// try get content from text, ignore conflict with preview ajax | |||
if form.Content == "" { | |||
@@ -406,6 +430,55 @@ func UpdateIssue(ctx *middleware.Context, params martini.Params, form auth.Creat | |||
}) | |||
} | |||
func UpdateIssueLabel(ctx *middleware.Context, params martini.Params) { | |||
if !ctx.Repo.IsOwner { | |||
ctx.Error(403) | |||
return | |||
} | |||
idx, _ := base.StrTo(params["index"]).Int64() | |||
if idx <= 0 { | |||
ctx.Error(404) | |||
return | |||
} | |||
issue, err := models.GetIssueByIndex(ctx.Repo.Repository.Id, idx) | |||
if err != nil { | |||
if err == models.ErrIssueNotExist { | |||
ctx.Handle(404, "issue.UpdateIssueLabel", err) | |||
} else { | |||
ctx.Handle(500, "issue.UpdateIssueLabel(GetIssueByIndex)", err) | |||
} | |||
return | |||
} | |||
isAttach := ctx.Query("action") == "attach" | |||
labelStrId := ctx.Query("id") | |||
isHad := strings.Contains(issue.LabelIds, "$"+labelStrId+"|") | |||
isNeedUpdate := false | |||
if isAttach { | |||
if !isHad { | |||
issue.LabelIds += "$" + labelStrId + "|" | |||
isNeedUpdate = true | |||
} | |||
} else { | |||
if isHad { | |||
issue.LabelIds = strings.Replace(issue.LabelIds, "$"+labelStrId+"|", "", -1) | |||
isNeedUpdate = true | |||
} | |||
} | |||
if isNeedUpdate { | |||
if err = models.UpdateIssue(issue); err != nil { | |||
ctx.Handle(500, "issue.UpdateIssueLabel(UpdateIssue)", err) | |||
return | |||
} | |||
} | |||
ctx.JSON(200, map[string]interface{}{ | |||
"ok": true, | |||
}) | |||
} | |||
func UpdateIssueMilestone(ctx *middleware.Context) { | |||
if !ctx.Repo.IsOwner { | |||
ctx.Error(403) | |||
@@ -622,8 +695,37 @@ func NewLabel(ctx *middleware.Context, form auth.CreateLabelForm) { | |||
ctx.Redirect(ctx.Repo.RepoLink + "/issues") | |||
} | |||
func UpdateLabel(ctx *middleware.Context, params martini.Params) { | |||
func UpdateLabel(ctx *middleware.Context, params martini.Params, form auth.CreateLabelForm) { | |||
id, _ := base.StrTo(ctx.Query("id")).Int64() | |||
if id == 0 { | |||
ctx.Error(404) | |||
return | |||
} | |||
l := &models.Label{ | |||
Id: id, | |||
Name: form.Title, | |||
Color: form.Color, | |||
} | |||
if err := models.UpdateLabel(l); err != nil { | |||
ctx.Handle(500, "issue.UpdateLabel(UpdateLabel)", err) | |||
return | |||
} | |||
ctx.Redirect(ctx.Repo.RepoLink + "/issues") | |||
} | |||
func DeleteLabel(ctx *middleware.Context) { | |||
strIds := strings.Split(ctx.Query("remove"), ",") | |||
for _, strId := range strIds { | |||
if err := models.DeleteLabel(ctx.Repo.Repository.Id, strId); err != nil { | |||
ctx.Handle(500, "issue.DeleteLabel(DeleteLabel)", err) | |||
return | |||
} | |||
} | |||
ctx.JSON(200, map[string]interface{}{ | |||
"ok": true, | |||
}) | |||
} | |||
func Milestones(ctx *middleware.Context) { |
@@ -1,11 +0,0 @@ | |||
{{template "base/head" .}} | |||
{{template "base/navbar" .}} | |||
<div id="body-nav"> | |||
<div class="container"> | |||
<h3>Help</h3> | |||
</div> | |||
</div> | |||
<div id="body" class="container" data-page="user"> | |||
{{if .HasInfo}}<div class="alert alert-info">{{.InfoMsg}}</div>{{end}} | |||
</div> | |||
{{template "base/footer" .}} |
@@ -15,7 +15,7 @@ | |||
</div> | |||
<div class="label-filter"> | |||
<h4>Label</h4> | |||
<ul class="list-unstyled" id="label-list" data-ajax="/{url}"> | |||
<ul class="list-unstyled" id="label-list" data-ajax="{{$.RepoLink}}/issues/labels/delete"> | |||
{{range .Labels}} | |||
<li class="label-item" id="label-{{.Id}}" data-id="{{.Id}}"><a href="#"> | |||
<span class="pull-right count">{{if $.IsShowClosed}}{{.NumClosedIssues}}{{else}}{{.NumOpenIssues}}{{end}}</span> | |||
@@ -60,7 +60,7 @@ | |||
{{template "base/alert" .}} | |||
<div class="filter-option"> | |||
<div class="btn-group"> | |||
<a class="btn btn-default issue-open{{if not .IsShowClosed}} active{{end}}" href="{{.RepoLink}}/issues?type={{.ViewType}}">{{..IssueStats.OpenCount}} Open</a> | |||
<a class="btn btn-default issue-open{{if not .IsShowClosed}} active{{end}}" href="{{.RepoLink}}/issues?type={{.ViewType}}">{{.IssueStats.OpenCount}} Open</a> | |||
<a class="btn btn-default issue-close{{if .IsShowClosed}} active{{end}}" href="{{.RepoLink}}/issues?type={{.ViewType}}&state=closed">{{.IssueStats.ClosedCount}} Closed</a> | |||
</div> | |||
</div> |
@@ -118,7 +118,7 @@ | |||
</div> | |||
<div class="issue-bar col-md-2"> | |||
<div class="labels" data-ajax="{url}"> | |||
<div class="labels" data-ajax="{{.Issue.Index}}/label"> | |||
<div class="pull-right action"> | |||
<button class="btn btn-default btn-sm" data-toggle="dropdown"> | |||
<i class="fa fa-tags"></i> | |||
@@ -126,26 +126,24 @@ | |||
</button> | |||
<div class="dropdown-menu dropdown-menu-right no"> | |||
<ul class="list-unstyled"> | |||
<li class="checked" data-id="1"> | |||
<span class="check pull-left"><i class="fa fa-check"></i></span> | |||
<span class="color" style="background-color: #f59e00"></span> | |||
<span class="name">bug</span> | |||
</li> | |||
<li class="no-checked" data-id="2"> | |||
<span class="color" style="background-color: #f59e00"></span> | |||
<span class="name">bug</span> | |||
</li> | |||
<li class="no-checked" data-id="3"> | |||
<span class="color" style="background-color: #f59e00"></span> | |||
<span class="name">bug</span> | |||
{{range .Labels}} | |||
<li class="{{if not .IsChecked}}no-{{end}}checked" data-id="{{.Id}}"> | |||
{{if .IsChecked}}<span class="check pull-left"><i class="fa fa-check"></i></span>{{end}} | |||
<span class="color" style="background-color: {{.Color}}"></span> | |||
<span class="name">{{.Name}}</span> | |||
</li> | |||
{{end}} | |||
</ul> | |||
</div> | |||
</div> | |||
<h4>Labels</h4> | |||
<p id="label-1" class="label-item label-white" style="background-color: #e75316"><strong>bug</strong></p> | |||
<p id="label-2" class="label-item label-white" style="background-color: #e8f0ff"><strong>bug</strong></p> | |||
<p>Not yet</p> | |||
{{if .Issue.Labels}} | |||
{{range .Issue.Labels}} | |||
<p id="label-{{.Id}}" class="label-item label-white" style="background-color: {{.Color}}"><strong>{{.Name}}</strong></p> | |||
{{end}} | |||
{{else}} | |||
<p>None yet</p> | |||
{{end}} | |||
</div> | |||
<div class="milestone" data-milestone="{{.Milestone.Id}}" data-ajax="{{.Issue.Index}}/milestone"> | |||
<div class="pull-right action"> | |||
@@ -223,10 +221,7 @@ | |||
<h4>Assignee</h4> | |||
<p>{{if .Issue.Assignee}}<img src="{{.Issue.Assignee.AvatarLink}}"><strong>{{.Issue.Assignee.Name}}</strong>{{else}}No one assigned{{end}}</p> | |||
</div> | |||
</div><!-- | |||
<div class="col-md-3"> | |||
label dashboard | |||
</div>--> | |||
</div> | |||
</div> | |||
</div> | |||
</div> |