mirror of
				https://gitea.com/gitea/tea.git
				synced 2025-10-31 01:05:26 +01:00 
			
		
		
		
	Implement more issue filters (#400)
This adds new filters to `tea issues ls` and `tea pr ls`, made available in SDK 0.15: ``` --state value Filter by state (all|open|closed) (default: open) --keyword value, -k value Filter by search string --labels value, -L value Comma-separated list of labels to match issues against. --milestones value, -m value Comma-separated list of milestones to match issues against. --author value, -A value --assignee value, -a value --mentions value, -M value --from value, -F value Filter by activity after this date --until value, -u value Filter by activity before this date ``` Note: I felt free to change parameter names as exposed by SDK & API, as the names exposed by them are partially bollocks (eg `mentioned_by`) and or inconsistent with usage in other commands (eg `tea times --until`) fixes #376, related #323 Co-authored-by: Norwin <git@nroo.de> Reviewed-on: https://gitea.com/gitea/tea/pulls/400 Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com> Reviewed-by: 6543 <6543@obermui.de> Co-authored-by: Norwin <noerw@noreply.gitea.io> Co-committed-by: Norwin <noerw@noreply.gitea.io>
This commit is contained in:
		| @@ -21,15 +21,19 @@ type CsvFlag struct { | |||||||
|  |  | ||||||
| // NewCsvFlag creates a CsvFlag, while setting its usage string and default values | // NewCsvFlag creates a CsvFlag, while setting its usage string and default values | ||||||
| func NewCsvFlag(name, usage string, aliases, availableValues, defaults []string) *CsvFlag { | func NewCsvFlag(name, usage string, aliases, availableValues, defaults []string) *CsvFlag { | ||||||
|  | 	var availableDesc string | ||||||
|  | 	if len(availableValues) != 0 { | ||||||
|  | 		availableDesc = " Available values:" | ||||||
|  | 	} | ||||||
| 	return &CsvFlag{ | 	return &CsvFlag{ | ||||||
| 		AvailableFields: availableValues, | 		AvailableFields: availableValues, | ||||||
| 		StringFlag: cli.StringFlag{ | 		StringFlag: cli.StringFlag{ | ||||||
| 			Name:    name, | 			Name:    name, | ||||||
| 			Aliases: aliases, | 			Aliases: aliases, | ||||||
| 			Value:   strings.Join(defaults, ","), | 			Value:   strings.Join(defaults, ","), | ||||||
| 			Usage: fmt.Sprintf(`Comma-separated list of %s. Available values: | 			Usage: fmt.Sprintf(`Comma-separated list of %s.%s | ||||||
| 			%s | 			%s | ||||||
| 		`, usage, strings.Join(availableValues, ",")), | 		`, usage, availableDesc, strings.Join(availableValues, ",")), | ||||||
| 		}, | 		}, | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|   | |||||||
| @@ -5,14 +5,6 @@ | |||||||
| package flags | package flags | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"fmt" |  | ||||||
| 	"strings" |  | ||||||
| 
 |  | ||||||
| 	"code.gitea.io/sdk/gitea" |  | ||||||
| 	"code.gitea.io/tea/modules/context" |  | ||||||
| 	"code.gitea.io/tea/modules/task" |  | ||||||
| 
 |  | ||||||
| 	"github.com/araddon/dateparse" |  | ||||||
| 	"github.com/urfave/cli/v2" | 	"github.com/urfave/cli/v2" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| @@ -44,13 +36,6 @@ var OutputFlag = cli.StringFlag{ | |||||||
| 	Usage:   "Output format. (csv, simple, table, tsv, yaml)", | 	Usage:   "Output format. (csv, simple, table, tsv, yaml)", | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // StateFlag provides flag to specify issue/pr state, defaulting to "open" |  | ||||||
| var StateFlag = cli.StringFlag{ |  | ||||||
| 	Name:        "state", |  | ||||||
| 	Usage:       "Filter by state (all|open|closed)", |  | ||||||
| 	DefaultText: "open", |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // PaginationPageFlag provides flag for pagination options | // PaginationPageFlag provides flag for pagination options | ||||||
| var PaginationPageFlag = cli.StringFlag{ | var PaginationPageFlag = cli.StringFlag{ | ||||||
| 	Name:    "page", | 	Name:    "page", | ||||||
| @@ -93,13 +78,6 @@ var AllDefaultFlags = append([]cli.Flag{ | |||||||
| 	&RemoteFlag, | 	&RemoteFlag, | ||||||
| }, LoginOutputFlags...) | }, LoginOutputFlags...) | ||||||
| 
 | 
 | ||||||
| // IssuePRFlags defines flags that should be available on issue & pr listing flags. |  | ||||||
| var IssuePRFlags = append([]cli.Flag{ |  | ||||||
| 	&StateFlag, |  | ||||||
| 	&PaginationPageFlag, |  | ||||||
| 	&PaginationLimitFlag, |  | ||||||
| }, AllDefaultFlags...) |  | ||||||
| 
 |  | ||||||
| // NotificationFlags defines flags that should be available on notifications. | // NotificationFlags defines flags that should be available on notifications. | ||||||
| var NotificationFlags = append([]cli.Flag{ | var NotificationFlags = append([]cli.Flag{ | ||||||
| 	NotificationStateFlag, | 	NotificationStateFlag, | ||||||
| @@ -121,82 +99,6 @@ var NotificationStateFlag = NewCsvFlag( | |||||||
| 	[]string{"unread", "pinned"}, | 	[]string{"unread", "pinned"}, | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| // IssuePREditFlags defines flags for properties of issues and PRs |  | ||||||
| var IssuePREditFlags = append([]cli.Flag{ |  | ||||||
| 	&cli.StringFlag{ |  | ||||||
| 		Name:    "title", |  | ||||||
| 		Aliases: []string{"t"}, |  | ||||||
| 	}, |  | ||||||
| 	&cli.StringFlag{ |  | ||||||
| 		Name:    "description", |  | ||||||
| 		Aliases: []string{"d"}, |  | ||||||
| 	}, |  | ||||||
| 	&cli.StringFlag{ |  | ||||||
| 		Name:    "assignees", |  | ||||||
| 		Aliases: []string{"a"}, |  | ||||||
| 		Usage:   "Comma-separated list of usernames to assign", |  | ||||||
| 	}, |  | ||||||
| 	&cli.StringFlag{ |  | ||||||
| 		Name:    "labels", |  | ||||||
| 		Aliases: []string{"L"}, |  | ||||||
| 		Usage:   "Comma-separated list of labels to assign", |  | ||||||
| 	}, |  | ||||||
| 	&cli.StringFlag{ |  | ||||||
| 		Name:    "deadline", |  | ||||||
| 		Aliases: []string{"D"}, |  | ||||||
| 		Usage:   "Deadline timestamp to assign", |  | ||||||
| 	}, |  | ||||||
| 	&cli.StringFlag{ |  | ||||||
| 		Name:    "milestone", |  | ||||||
| 		Aliases: []string{"m"}, |  | ||||||
| 		Usage:   "Milestone to assign", |  | ||||||
| 	}, |  | ||||||
| }, LoginRepoFlags...) |  | ||||||
| 
 |  | ||||||
| // GetIssuePREditFlags parses all IssuePREditFlags |  | ||||||
| func GetIssuePREditFlags(ctx *context.TeaContext) (*gitea.CreateIssueOption, error) { |  | ||||||
| 	opts := gitea.CreateIssueOption{ |  | ||||||
| 		Title:     ctx.String("title"), |  | ||||||
| 		Body:      ctx.String("description"), |  | ||||||
| 		Assignees: strings.Split(ctx.String("assignees"), ","), |  | ||||||
| 	} |  | ||||||
| 	var err error |  | ||||||
| 
 |  | ||||||
| 	date := ctx.String("deadline") |  | ||||||
| 	if date != "" { |  | ||||||
| 		t, err := dateparse.ParseAny(date) |  | ||||||
| 		if err != nil { |  | ||||||
| 			return nil, err |  | ||||||
| 		} |  | ||||||
| 		opts.Deadline = &t |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	client := ctx.Login.Client() |  | ||||||
| 
 |  | ||||||
| 	labelNames := strings.Split(ctx.String("labels"), ",") |  | ||||||
| 	if len(labelNames) != 0 { |  | ||||||
| 		if client == nil { |  | ||||||
| 			client = ctx.Login.Client() |  | ||||||
| 		} |  | ||||||
| 		if opts.Labels, err = task.ResolveLabelNames(client, ctx.Owner, ctx.Repo, labelNames); err != nil { |  | ||||||
| 			return nil, err |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	if milestoneName := ctx.String("milestone"); len(milestoneName) != 0 { |  | ||||||
| 		if client == nil { |  | ||||||
| 			client = ctx.Login.Client() |  | ||||||
| 		} |  | ||||||
| 		ms, _, err := client.GetMilestoneByName(ctx.Owner, ctx.Repo, milestoneName) |  | ||||||
| 		if err != nil { |  | ||||||
| 			return nil, fmt.Errorf("Milestone '%s' not found", milestoneName) |  | ||||||
| 		} |  | ||||||
| 		opts.Milestone = ms.ID |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	return &opts, nil |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // FieldsFlag generates a flag selecting printable fields. | // FieldsFlag generates a flag selecting printable fields. | ||||||
| // To retrieve the value, use f.GetValues() | // To retrieve the value, use f.GetValues() | ||||||
| func FieldsFlag(availableFields, defaultFields []string) *CsvFlag { | func FieldsFlag(availableFields, defaultFields []string) *CsvFlag { | ||||||
							
								
								
									
										161
									
								
								cmd/flags/issue_pr.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										161
									
								
								cmd/flags/issue_pr.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,161 @@ | |||||||
|  | // Copyright 2019 The Gitea 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 flags | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"fmt" | ||||||
|  | 	"strings" | ||||||
|  |  | ||||||
|  | 	"code.gitea.io/sdk/gitea" | ||||||
|  | 	"code.gitea.io/tea/modules/context" | ||||||
|  | 	"code.gitea.io/tea/modules/task" | ||||||
|  |  | ||||||
|  | 	"github.com/araddon/dateparse" | ||||||
|  | 	"github.com/urfave/cli/v2" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | // StateFlag provides flag to specify issue/pr state, defaulting to "open" | ||||||
|  | var StateFlag = cli.StringFlag{ | ||||||
|  | 	Name:        "state", | ||||||
|  | 	Usage:       "Filter by state (all|open|closed)", | ||||||
|  | 	DefaultText: "open", | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // MilestoneFilterFlag is a CSV flag used to filter issues by milestones | ||||||
|  | var MilestoneFilterFlag = NewCsvFlag( | ||||||
|  | 	"milestones", | ||||||
|  | 	"milestones to match issues against", | ||||||
|  | 	[]string{"m"}, nil, nil) | ||||||
|  |  | ||||||
|  | // LabelFilterFlag is a CSV flag used to filter issues by labels | ||||||
|  | var LabelFilterFlag = NewCsvFlag( | ||||||
|  | 	"labels", | ||||||
|  | 	"labels to match issues against", | ||||||
|  | 	[]string{"L"}, nil, nil) | ||||||
|  |  | ||||||
|  | // PRListingFlags defines flags that should be available on pr listing flags. | ||||||
|  | var PRListingFlags = append([]cli.Flag{ | ||||||
|  | 	&StateFlag, | ||||||
|  | 	&PaginationPageFlag, | ||||||
|  | 	&PaginationLimitFlag, | ||||||
|  | }, AllDefaultFlags...) | ||||||
|  |  | ||||||
|  | // IssueListingFlags defines flags that should be available on issue listing flags. | ||||||
|  | var IssueListingFlags = append([]cli.Flag{ | ||||||
|  | 	&StateFlag, | ||||||
|  | 	&cli.StringFlag{ | ||||||
|  | 		Name:        "kind", | ||||||
|  | 		Aliases:     []string{"K"}, | ||||||
|  | 		Usage:       "Wether to return `issues`, `pulls`, or `all` (you can use this to apply advanced search filters to PRs)", | ||||||
|  | 		DefaultText: "issues", | ||||||
|  | 	}, | ||||||
|  | 	&cli.StringFlag{ | ||||||
|  | 		Name:    "keyword", | ||||||
|  | 		Aliases: []string{"k"}, | ||||||
|  | 		Usage:   "Filter by search string", | ||||||
|  | 	}, | ||||||
|  | 	LabelFilterFlag, | ||||||
|  | 	MilestoneFilterFlag, | ||||||
|  | 	&cli.StringFlag{ | ||||||
|  | 		Name:    "author", | ||||||
|  | 		Aliases: []string{"A"}, | ||||||
|  | 	}, | ||||||
|  | 	&cli.StringFlag{ | ||||||
|  | 		Name:    "assignee", | ||||||
|  | 		Aliases: []string{"a"}, | ||||||
|  | 	}, | ||||||
|  | 	&cli.StringFlag{ | ||||||
|  | 		Name:    "mentions", | ||||||
|  | 		Aliases: []string{"M"}, | ||||||
|  | 	}, | ||||||
|  | 	&cli.StringFlag{ | ||||||
|  | 		Name:    "from", | ||||||
|  | 		Aliases: []string{"F"}, | ||||||
|  | 		Usage:   "Filter by activity after this date", | ||||||
|  | 	}, | ||||||
|  | 	&cli.StringFlag{ | ||||||
|  | 		Name:    "until", | ||||||
|  | 		Aliases: []string{"u"}, | ||||||
|  | 		Usage:   "Filter by activity before this date", | ||||||
|  | 	}, | ||||||
|  | 	&PaginationPageFlag, | ||||||
|  | 	&PaginationLimitFlag, | ||||||
|  | }, AllDefaultFlags...) | ||||||
|  |  | ||||||
|  | // IssuePREditFlags defines flags for properties of issues and PRs | ||||||
|  | var IssuePREditFlags = append([]cli.Flag{ | ||||||
|  | 	&cli.StringFlag{ | ||||||
|  | 		Name:    "title", | ||||||
|  | 		Aliases: []string{"t"}, | ||||||
|  | 	}, | ||||||
|  | 	&cli.StringFlag{ | ||||||
|  | 		Name:    "description", | ||||||
|  | 		Aliases: []string{"d"}, | ||||||
|  | 	}, | ||||||
|  | 	&cli.StringFlag{ | ||||||
|  | 		Name:    "assignees", | ||||||
|  | 		Aliases: []string{"a"}, | ||||||
|  | 		Usage:   "Comma-separated list of usernames to assign", | ||||||
|  | 	}, | ||||||
|  | 	&cli.StringFlag{ | ||||||
|  | 		Name:    "labels", | ||||||
|  | 		Aliases: []string{"L"}, | ||||||
|  | 		Usage:   "Comma-separated list of labels to assign", | ||||||
|  | 	}, | ||||||
|  | 	&cli.StringFlag{ | ||||||
|  | 		Name:    "deadline", | ||||||
|  | 		Aliases: []string{"D"}, | ||||||
|  | 		Usage:   "Deadline timestamp to assign", | ||||||
|  | 	}, | ||||||
|  | 	&cli.StringFlag{ | ||||||
|  | 		Name:    "milestone", | ||||||
|  | 		Aliases: []string{"m"}, | ||||||
|  | 		Usage:   "Milestone to assign", | ||||||
|  | 	}, | ||||||
|  | }, LoginRepoFlags...) | ||||||
|  |  | ||||||
|  | // GetIssuePREditFlags parses all IssuePREditFlags | ||||||
|  | func GetIssuePREditFlags(ctx *context.TeaContext) (*gitea.CreateIssueOption, error) { | ||||||
|  | 	opts := gitea.CreateIssueOption{ | ||||||
|  | 		Title:     ctx.String("title"), | ||||||
|  | 		Body:      ctx.String("description"), | ||||||
|  | 		Assignees: strings.Split(ctx.String("assignees"), ","), | ||||||
|  | 	} | ||||||
|  | 	var err error | ||||||
|  |  | ||||||
|  | 	date := ctx.String("deadline") | ||||||
|  | 	if date != "" { | ||||||
|  | 		t, err := dateparse.ParseAny(date) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return nil, err | ||||||
|  | 		} | ||||||
|  | 		opts.Deadline = &t | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	client := ctx.Login.Client() | ||||||
|  |  | ||||||
|  | 	labelNames := strings.Split(ctx.String("labels"), ",") | ||||||
|  | 	if len(labelNames) != 0 { | ||||||
|  | 		if client == nil { | ||||||
|  | 			client = ctx.Login.Client() | ||||||
|  | 		} | ||||||
|  | 		if opts.Labels, err = task.ResolveLabelNames(client, ctx.Owner, ctx.Repo, labelNames); err != nil { | ||||||
|  | 			return nil, err | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if milestoneName := ctx.String("milestone"); len(milestoneName) != 0 { | ||||||
|  | 		if client == nil { | ||||||
|  | 			client = ctx.Login.Client() | ||||||
|  | 		} | ||||||
|  | 		ms, _, err := client.GetMilestoneByName(ctx.Owner, ctx.Repo, milestoneName) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return nil, fmt.Errorf("Milestone '%s' not found", milestoneName) | ||||||
|  | 		} | ||||||
|  | 		opts.Milestone = ms.ID | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return &opts, nil | ||||||
|  | } | ||||||
| @@ -5,11 +5,15 @@ | |||||||
| package issues | package issues | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
|  | 	"fmt" | ||||||
|  | 	"time" | ||||||
|  |  | ||||||
| 	"code.gitea.io/tea/cmd/flags" | 	"code.gitea.io/tea/cmd/flags" | ||||||
| 	"code.gitea.io/tea/modules/context" | 	"code.gitea.io/tea/modules/context" | ||||||
| 	"code.gitea.io/tea/modules/print" | 	"code.gitea.io/tea/modules/print" | ||||||
|  |  | ||||||
| 	"code.gitea.io/sdk/gitea" | 	"code.gitea.io/sdk/gitea" | ||||||
|  | 	"github.com/araddon/dateparse" | ||||||
| 	"github.com/urfave/cli/v2" | 	"github.com/urfave/cli/v2" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| @@ -24,7 +28,7 @@ var CmdIssuesList = cli.Command{ | |||||||
| 	Usage:       "List issues of the repository", | 	Usage:       "List issues of the repository", | ||||||
| 	Description: `List issues of the repository`, | 	Description: `List issues of the repository`, | ||||||
| 	Action:      RunIssuesList, | 	Action:      RunIssuesList, | ||||||
| 	Flags:       append([]cli.Flag{issueFieldsFlag}, flags.IssuePRFlags...), | 	Flags:       append([]cli.Flag{issueFieldsFlag}, flags.IssueListingFlags...), | ||||||
| } | } | ||||||
|  |  | ||||||
| // RunIssuesList list issues | // RunIssuesList list issues | ||||||
| @@ -36,16 +40,57 @@ func RunIssuesList(cmd *cli.Context) error { | |||||||
| 	switch ctx.String("state") { | 	switch ctx.String("state") { | ||||||
| 	case "all": | 	case "all": | ||||||
| 		state = gitea.StateAll | 		state = gitea.StateAll | ||||||
| 	case "open": | 	case "", "open": | ||||||
| 		state = gitea.StateOpen | 		state = gitea.StateOpen | ||||||
| 	case "closed": | 	case "closed": | ||||||
| 		state = gitea.StateClosed | 		state = gitea.StateClosed | ||||||
|  | 	default: | ||||||
|  | 		return fmt.Errorf("unknown state '%s'", ctx.String("state")) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	kind := gitea.IssueTypeIssue | ||||||
|  | 	switch ctx.String("kind") { | ||||||
|  | 	case "", "issues", "issue": | ||||||
|  | 		kind = gitea.IssueTypeIssue | ||||||
|  | 	case "pulls", "pull", "pr": | ||||||
|  | 		kind = gitea.IssueTypePull | ||||||
|  | 	case "all": | ||||||
|  | 		kind = gitea.IssueTypeAll | ||||||
|  | 	default: | ||||||
|  | 		return fmt.Errorf("unknown kind '%s'", ctx.String("kind")) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	var err error | ||||||
|  | 	var from, until time.Time | ||||||
|  | 	if ctx.IsSet("from") { | ||||||
|  | 		from, err = dateparse.ParseLocal(ctx.String("from")) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return err | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	if ctx.IsSet("until") { | ||||||
|  | 		until, err = dateparse.ParseLocal(ctx.String("until")) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return err | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// ignore error, as we don't do any input validation on these flags | ||||||
|  | 	labels, _ := flags.LabelFilterFlag.GetValues(cmd) | ||||||
|  | 	milestones, _ := flags.MilestoneFilterFlag.GetValues(cmd) | ||||||
|  |  | ||||||
| 	issues, _, err := ctx.Login.Client().ListRepoIssues(ctx.Owner, ctx.Repo, gitea.ListIssueOption{ | 	issues, _, err := ctx.Login.Client().ListRepoIssues(ctx.Owner, ctx.Repo, gitea.ListIssueOption{ | ||||||
| 		ListOptions: ctx.GetListOptions(), | 		ListOptions: ctx.GetListOptions(), | ||||||
| 		State:       state, | 		State:       state, | ||||||
| 		Type:        gitea.IssueTypeIssue, | 		Type:        kind, | ||||||
|  | 		KeyWord:     ctx.String("keyword"), | ||||||
|  | 		CreatedBy:   ctx.String("author"), | ||||||
|  | 		AssignedBy:  ctx.String("assigned-to"), | ||||||
|  | 		MentionedBy: ctx.String("mentions"), | ||||||
|  | 		Labels:      labels, | ||||||
|  | 		Milestones:  milestones, | ||||||
|  | 		Since:       from, | ||||||
|  | 		Before:      until, | ||||||
| 	}) | 	}) | ||||||
|  |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
|   | |||||||
| @@ -24,7 +24,7 @@ var CmdPullsList = cli.Command{ | |||||||
| 	Usage:       "List pull requests of the repository", | 	Usage:       "List pull requests of the repository", | ||||||
| 	Description: `List pull requests of the repository`, | 	Description: `List pull requests of the repository`, | ||||||
| 	Action:      RunPullsList, | 	Action:      RunPullsList, | ||||||
| 	Flags:       append([]cli.Flag{pullFieldsFlag}, flags.IssuePRFlags...), | 	Flags:       append([]cli.Flag{pullFieldsFlag}, flags.PRListingFlags...), | ||||||
| } | } | ||||||
|  |  | ||||||
| // RunPullsList return list of pulls | // RunPullsList return list of pulls | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 Norwin
					Norwin