Commit 3fcb74f9 authored by jiangpeng's avatar jiangpeng

dapp/vote:classify vote with status

parent 3dc1c260
...@@ -13,6 +13,7 @@ import ( ...@@ -13,6 +13,7 @@ import (
func createGroupCMD() *cobra.Command { func createGroupCMD() *cobra.Command {
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "createGroup", Use: "createGroup",
Aliases: []string{"cg"},
Short: "create tx(create vote group)", Short: "create tx(create vote group)",
Run: createGroup, Run: createGroup,
Example: "createGroup -n=group1 -a=admin1 -m=member1 -m=member2", Example: "createGroup -n=group1 -a=admin1 -m=member1 -m=member2",
...@@ -64,6 +65,7 @@ func createGroup(cmd *cobra.Command, args []string) { ...@@ -64,6 +65,7 @@ func createGroup(cmd *cobra.Command, args []string) {
func updateGroupCMD() *cobra.Command { func updateGroupCMD() *cobra.Command {
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "updateGroup", Use: "updateGroup",
Aliases: []string{"ug"},
Short: "create tx(update group members or admin)", Short: "create tx(update group members or admin)",
Run: updateGroup, Run: updateGroup,
Example: "updateGroup -g=id -a=addMember1 -a=addMember2 -r=removeMember1 ...", Example: "updateGroup -g=id -a=addMember1 -a=addMember2 -r=removeMember1 ...",
...@@ -74,11 +76,11 @@ func updateGroupCMD() *cobra.Command { ...@@ -74,11 +76,11 @@ func updateGroupCMD() *cobra.Command {
func updateGroupFlags(cmd *cobra.Command) { func updateGroupFlags(cmd *cobra.Command) {
cmd.Flags().StringP("groupID", "g", "", "group id") cmd.Flags().StringP("groupID", "g", "", "group id")
cmd.Flags().StringArrayP("addMembers", "a", nil, "group member address array for adding") cmd.Flags().StringArrayP("addMembers", "m", nil, "group member address array for adding")
cmd.Flags().UintSliceP("weights", "w", nil, "member vote weight array for adding") cmd.Flags().UintSliceP("weights", "w", nil, "member vote weight array for adding")
cmd.Flags().StringArrayP("removeMembers", "r", nil, "group member address array for removing") cmd.Flags().StringArrayP("removeMembers", "v", nil, "group member address array for removing")
cmd.Flags().StringArrayP("addAdmins", "d", nil, "group admin address array for adding") cmd.Flags().StringArrayP("addAdmins", "a", nil, "group admin address array for adding")
cmd.Flags().StringArrayP("removeAdmins", "m", nil, "group admin address array for removing") cmd.Flags().StringArrayP("removeAdmins", "r", nil, "group admin address array for removing")
markRequired(cmd, "groupID") markRequired(cmd, "groupID")
} }
...@@ -120,9 +122,10 @@ func updateGroup(cmd *cobra.Command, args []string) { ...@@ -120,9 +122,10 @@ func updateGroup(cmd *cobra.Command, args []string) {
func createVoteCMD() *cobra.Command { func createVoteCMD() *cobra.Command {
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "createVote", Use: "createVote",
Short: "create tx(create vote)", Aliases: []string{"ctv"},
Run: createVote, Short: "create tx(create vote)",
Run: createVote,
} }
createVoteFlags(cmd) createVoteFlags(cmd)
return cmd return cmd
...@@ -180,9 +183,10 @@ func createVote(cmd *cobra.Command, args []string) { ...@@ -180,9 +183,10 @@ func createVote(cmd *cobra.Command, args []string) {
func commitVoteCMD() *cobra.Command { func commitVoteCMD() *cobra.Command {
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "commitVote", Use: "commitVote",
Short: "create tx(commit vote)", Aliases: []string{"cmv"},
Run: commitVote, Short: "create tx(commit vote)",
Run: commitVote,
} }
commitVoteFlags(cmd) commitVoteFlags(cmd)
return cmd return cmd
...@@ -211,9 +215,10 @@ func commitVote(cmd *cobra.Command, args []string) { ...@@ -211,9 +215,10 @@ func commitVote(cmd *cobra.Command, args []string) {
func closeVoteCMD() *cobra.Command { func closeVoteCMD() *cobra.Command {
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "closeVote", Use: "closeVote",
Short: "create tx(close vote)", Aliases: []string{"csv"},
Run: closeVote, Short: "create tx(close vote)",
Run: closeVote,
} }
closeVoteFlags(cmd) closeVoteFlags(cmd)
return cmd return cmd
...@@ -239,9 +244,10 @@ func closeVote(cmd *cobra.Command, args []string) { ...@@ -239,9 +244,10 @@ func closeVote(cmd *cobra.Command, args []string) {
func updateMemberCMD() *cobra.Command { func updateMemberCMD() *cobra.Command {
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "updateMember", Use: "updateMember",
Short: "create tx(update member name)", Aliases: []string{"um"},
Run: updateMember, Short: "create tx(update member name)",
Run: updateMember,
} }
updateMemberFlags(cmd) updateMemberFlags(cmd)
return cmd return cmd
......
...@@ -12,6 +12,7 @@ import ( ...@@ -12,6 +12,7 @@ import (
func groupInfoCMD() *cobra.Command { func groupInfoCMD() *cobra.Command {
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "groupInfo", Use: "groupInfo",
Aliases: []string{"gf"},
Short: "get group infos", Short: "get group infos",
Run: groupInfo, Run: groupInfo,
Example: "groupInfo -g=id1 -g=id2...", Example: "groupInfo -g=id1 -g=id2...",
...@@ -41,9 +42,10 @@ func groupInfo(cmd *cobra.Command, args []string) { ...@@ -41,9 +42,10 @@ func groupInfo(cmd *cobra.Command, args []string) {
func voteInfoCMD() *cobra.Command { func voteInfoCMD() *cobra.Command {
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "voteInfo", Use: "voteInfo",
Short: "get vote info", Aliases: []string{"vf"},
Run: voteInfo, Short: "get vote info",
Run: voteInfo,
} }
voteInfoFlags(cmd) voteInfoFlags(cmd)
return cmd return cmd
...@@ -70,9 +72,10 @@ func voteInfo(cmd *cobra.Command, args []string) { ...@@ -70,9 +72,10 @@ func voteInfo(cmd *cobra.Command, args []string) {
func memberInfoCMD() *cobra.Command { func memberInfoCMD() *cobra.Command {
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "memberInfo", Use: "memberInfo",
Short: "get member info", Aliases: []string{"mf"},
Run: memberInfo, Short: "get member info",
Run: memberInfo,
} }
memberInfoFlags(cmd) memberInfoFlags(cmd)
return cmd return cmd
...@@ -99,9 +102,10 @@ func memberInfo(cmd *cobra.Command, args []string) { ...@@ -99,9 +102,10 @@ func memberInfo(cmd *cobra.Command, args []string) {
func listGroupCMD() *cobra.Command { func listGroupCMD() *cobra.Command {
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "listGroup", Use: "listGroup",
Short: "show group list", Aliases: []string{"lg"},
Run: listGroup, Short: "show group list",
Run: listGroup,
} }
listCmdFlags(cmd) listCmdFlags(cmd)
return cmd return cmd
...@@ -113,33 +117,38 @@ func listGroup(cmd *cobra.Command, args []string) { ...@@ -113,33 +117,38 @@ func listGroup(cmd *cobra.Command, args []string) {
func listVoteCMD() *cobra.Command { func listVoteCMD() *cobra.Command {
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "listVote", Use: "listVote",
Short: "show vote list", Aliases: []string{"lv"},
Run: listVote, Short: "show vote list",
Run: listVote,
} }
listVoteFlags(cmd) listVoteFlags(cmd)
return cmd return cmd
} }
func listVoteFlags(cmd *cobra.Command) { func listVoteFlags(cmd *cobra.Command) {
cmd.Flags().StringP("groupID", "g", "", "list vote belongs to specified group, list all if not set") cmd.Flags().StringP("groupID", "g", "", "list vote belongs to specified group, list all if not set")
cmd.Flags().Uint32P("status", "t", 0, "vote status")
listCmdFlags(cmd) listCmdFlags(cmd)
} }
func listVote(cmd *cobra.Command, args []string) { func listVote(cmd *cobra.Command, args []string) {
groupID, _ := cmd.Flags().GetString("groupID") groupID, _ := cmd.Flags().GetString("groupID")
status, _ := cmd.Flags().GetUint32("status")
listReq := getListReq(cmd) listReq := getListReq(cmd)
req := &vty.ReqListVote{ req := &vty.ReqListVote{
GroupID: groupID, GroupID: groupID,
ListReq: listReq, ListReq: listReq,
Status: status,
} }
sendQueryRPC(cmd, "ListVote", req, &vty.ReplyVoteList{}) sendQueryRPC(cmd, "ListVote", req, &vty.ReplyVoteList{})
} }
func listMemberCMD() *cobra.Command { func listMemberCMD() *cobra.Command {
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "listMember", Use: "listMember",
Short: "show member list", Aliases: []string{"lm"},
Run: listMember, Short: "show member list",
Run: listMember,
} }
listCmdFlags(cmd) listCmdFlags(cmd)
return cmd return cmd
...@@ -151,7 +160,7 @@ func listMember(cmd *cobra.Command, args []string) { ...@@ -151,7 +160,7 @@ func listMember(cmd *cobra.Command, args []string) {
func listCmdFlags(cmd *cobra.Command) { func listCmdFlags(cmd *cobra.Command) {
cmd.Flags().StringP("startItem", "s", "", "list start item id, default nil value") cmd.Flags().StringP("startItem", "s", "", "list start item id, default nil value")
cmd.Flags().Uint32P("count", "c", 10, "list count, default 10") cmd.Flags().Uint32P("count", "c", 5, "list count, default 5")
cmd.Flags().Uint32P("direction", "d", 1, "list direction, default 1 (Ascending order)") cmd.Flags().Uint32P("direction", "d", 1, "list direction, default 1 (Ascending order)")
} }
......
...@@ -147,6 +147,7 @@ func (a *action) createVote(create *vty.CreateVote) (*types.Receipt, error) { ...@@ -147,6 +147,7 @@ func (a *action) createVote(create *vty.CreateVote) (*types.Receipt, error) {
vote.Name = create.Name vote.Name = create.Name
vote.GroupID = create.GroupID vote.GroupID = create.GroupID
vote.Description = create.Description vote.Description = create.Description
vote.Creator = a.fromAddr
vote.VoteOptions = make([]*vty.VoteOption, 0) vote.VoteOptions = make([]*vty.VoteOption, 0)
for _, option := range create.VoteOptions { for _, option := range create.VoteOptions {
vote.VoteOptions = append(vote.VoteOptions, &vty.VoteOption{Option: option}) vote.VoteOptions = append(vote.VoteOptions, &vty.VoteOption{Option: option})
......
...@@ -96,7 +96,7 @@ func (v *vote) ExecLocal_CommitVote(payload *vty.CommitVote, tx *types.Transacti ...@@ -96,7 +96,7 @@ func (v *vote) ExecLocal_CommitVote(payload *vty.CommitVote, tx *types.Transacti
//implement code, add customize kv to dbSet... //implement code, add customize kv to dbSet...
commitInfo := decodeCommitInfo(receiptData.Logs[0].Log) commitInfo := decodeCommitInfo(receiptData.Logs[0].Log)
table := newVoteTable(v.GetLocalDB()) table := newVoteTable(v.GetLocalDB())
row, err := table.GetData([]byte(formatVoteID(payload.GetVoteID()))) row, err := table.GetData([]byte(payload.GetVoteID()))
if err != nil { if err != nil {
elog.Error("execLocal commitVote", "txHash", hex.EncodeToString(tx.Hash()), "voteTable get", err) elog.Error("execLocal commitVote", "txHash", hex.EncodeToString(tx.Hash()), "voteTable get", err)
return nil, err return nil, err
...@@ -127,7 +127,7 @@ func (v *vote) ExecLocal_CommitVote(payload *vty.CommitVote, tx *types.Transacti ...@@ -127,7 +127,7 @@ func (v *vote) ExecLocal_CommitVote(payload *vty.CommitVote, tx *types.Transacti
func (v *vote) ExecLocal_CloseVote(payload *vty.CloseVote, tx *types.Transaction, receiptData *types.ReceiptData, index int) (*types.LocalDBSet, error) { func (v *vote) ExecLocal_CloseVote(payload *vty.CloseVote, tx *types.Transaction, receiptData *types.ReceiptData, index int) (*types.LocalDBSet, error) {
dbSet := &types.LocalDBSet{} dbSet := &types.LocalDBSet{}
table := newVoteTable(v.GetLocalDB()) table := newVoteTable(v.GetLocalDB())
row, err := table.GetData([]byte(formatVoteID(payload.GetVoteID()))) row, err := table.GetData([]byte(payload.GetVoteID()))
if err != nil { if err != nil {
elog.Error("execLocal closeVote", "txHash", hex.EncodeToString(tx.Hash()), "voteTable get", err) elog.Error("execLocal closeVote", "txHash", hex.EncodeToString(tx.Hash()), "voteTable get", err)
return nil, err return nil, err
......
...@@ -76,7 +76,9 @@ func (v *vote) Query_GetVotes(in *vty.ReqStrings) (types.Message, error) { ...@@ -76,7 +76,9 @@ func (v *vote) Query_GetVotes(in *vty.ReqStrings) (types.Message, error) {
} }
voteList = append(voteList, info) voteList = append(voteList, info)
} }
return classifyVoteList(voteList), nil reply := &vty.ReplyVoteList{CurrentTimestamp: types.Now().Unix()}
reply.VoteList = filterVoteWithStatus(voteList, 0, reply.CurrentTimestamp)
return reply, nil
} }
...@@ -124,13 +126,18 @@ func (v *vote) Query_ListGroup(in *vty.ReqListItem) (types.Message, error) { ...@@ -124,13 +126,18 @@ func (v *vote) Query_ListGroup(in *vty.ReqListItem) (types.Message, error) {
table := newGroupTable(v.GetLocalDB()) table := newGroupTable(v.GetLocalDB())
var primaryKey []byte var primaryKey []byte
primaryKey = append(primaryKey, []byte(in.StartItemID)...) primaryKey = append(primaryKey, []byte(in.StartItemID)...)
list := &vty.GroupInfos{}
rows, err := table.ListIndex(groupTablePrimary, nil, primaryKey, in.Count, in.Direction) rows, err := table.ListIndex(groupTablePrimary, nil, primaryKey, in.Count, in.Direction)
// 已经没有数据,直接返回
if err == types.ErrNotFound {
return list, nil
}
if err != nil { if err != nil {
elog.Error("query listGroup", "err", err, "param", in) elog.Error("query listGroup", "err", err, "param", in)
return nil, err return nil, err
} }
list := &vty.GroupInfos{GroupList: make([]*vty.GroupInfo, 0, len(rows))} list.GroupList = make([]*vty.GroupInfo, 0, len(rows))
for _, row := range rows { for _, row := range rows {
info, ok := row.Data.(*vty.GroupInfo) info, ok := row.Data.(*vty.GroupInfo)
if !ok { if !ok {
...@@ -157,7 +164,16 @@ func (v *vote) Query_ListVote(in *vty.ReqListVote) (types.Message, error) { ...@@ -157,7 +164,16 @@ func (v *vote) Query_ListVote(in *vty.ReqListVote) (types.Message, error) {
prefix = []byte(groupID) prefix = []byte(groupID)
} }
primaryKey = append(primaryKey, []byte(in.GetListReq().GetStartItemID())...) primaryKey = append(primaryKey, []byte(in.GetListReq().GetStartItemID())...)
rows, err := table.ListIndex(indexName, prefix, primaryKey, in.GetListReq().Count, in.GetListReq().Direction) reply := &vty.ReplyVoteList{CurrentTimestamp: types.Now().Unix()}
listCount := in.ListReq.GetCount()
listMore:
rows, err := table.ListIndex(indexName, prefix, primaryKey, listCount, in.GetListReq().Direction)
// 已经没有数据,直接返回
if err == types.ErrNotFound {
return reply, nil
}
if err != nil { if err != nil {
elog.Error("query listVote", "err", err, "param", in) elog.Error("query listVote", "err", err, "param", in)
return nil, err return nil, err
...@@ -171,8 +187,16 @@ func (v *vote) Query_ListVote(in *vty.ReqListVote) (types.Message, error) { ...@@ -171,8 +187,16 @@ func (v *vote) Query_ListVote(in *vty.ReqListVote) (types.Message, error) {
} }
list = append(list, info) list = append(list, info)
} }
primaryKey = append(primaryKey[:0], []byte(list[len(list)-1].ID)...)
list = filterVoteWithStatus(list, in.Status, reply.CurrentTimestamp)
reply.VoteList = append(reply.VoteList, list...)
//经过筛选后,数量小于请求数量,则需要再次list, 需要满足len(rows)==listCount, 否则表示已经没有数据
if len(rows) == int(listCount) && int(listCount) > len(list) {
listCount -= int32(len(list))
goto listMore
}
return classifyVoteList(list), nil return reply, nil
} }
func (v *vote) Query_ListMember(in *vty.ReqListItem) (types.Message, error) { func (v *vote) Query_ListMember(in *vty.ReqListItem) (types.Message, error) {
...@@ -183,13 +207,18 @@ func (v *vote) Query_ListMember(in *vty.ReqListItem) (types.Message, error) { ...@@ -183,13 +207,18 @@ func (v *vote) Query_ListMember(in *vty.ReqListItem) (types.Message, error) {
table := newMemberTable(v.GetLocalDB()) table := newMemberTable(v.GetLocalDB())
var primaryKey []byte var primaryKey []byte
primaryKey = append(primaryKey, []byte(in.StartItemID)...) primaryKey = append(primaryKey, []byte(in.StartItemID)...)
list := &vty.MemberInfos{}
rows, err := table.ListIndex(memberTablePrimary, nil, primaryKey, in.Count, in.Direction) rows, err := table.ListIndex(memberTablePrimary, nil, primaryKey, in.Count, in.Direction)
// 已经没有数据,直接返回
if err == types.ErrNotFound {
return list, nil
}
if err != nil { if err != nil {
elog.Error("query listMember", "err", err, "param", in) elog.Error("query listMember", "err", err, "param", in)
return nil, err return nil, err
} }
list := &vty.MemberInfos{MemberList: make([]*vty.MemberInfo, 0, len(rows))} list.MemberList = make([]*vty.MemberInfo, 0, len(rows))
for _, row := range rows { for _, row := range rows {
info, ok := row.Data.(*vty.MemberInfo) info, ok := row.Data.(*vty.MemberInfo)
if !ok { if !ok {
......
...@@ -91,14 +91,12 @@ func decodeCommitInfo(data []byte) *vty.CommitInfo { ...@@ -91,14 +91,12 @@ func decodeCommitInfo(data []byte) *vty.CommitInfo {
return info return info
} }
func classifyVoteList(voteList []*vty.VoteInfo) *vty.ReplyVoteList { func filterVoteWithStatus(voteList []*vty.VoteInfo, status uint32, currentTime int64) []*vty.VoteInfo {
reply := &vty.ReplyVoteList{} var filterList []*vty.VoteInfo
currentTime := types.Now().Unix()
for _, voteInfo := range voteList { for _, voteInfo := range voteList {
if voteInfo.Status == voteStatusClosed { if voteInfo.Status == voteStatusClosed {
continue
} else if voteInfo.BeginTimestamp > currentTime { } else if voteInfo.BeginTimestamp > currentTime {
voteInfo.Status = voteStatusPending voteInfo.Status = voteStatusPending
} else if voteInfo.EndTimestamp > currentTime { } else if voteInfo.EndTimestamp > currentTime {
...@@ -106,8 +104,14 @@ func classifyVoteList(voteList []*vty.VoteInfo) *vty.ReplyVoteList { ...@@ -106,8 +104,14 @@ func classifyVoteList(voteList []*vty.VoteInfo) *vty.ReplyVoteList {
} else { } else {
voteInfo.Status = voteStatusFinished voteInfo.Status = voteStatusFinished
} }
//remove vote info with other status
if status == voteInfo.Status {
filterList = append(filterList, voteInfo)
}
}
//设置了状态筛选,返回对应的筛选列表
if status > 0 {
return filterList
} }
reply.CurrentTimestamp = currentTime return voteList
reply.VoteList = voteList
return reply
} }
...@@ -124,13 +124,14 @@ message ReqStrings { ...@@ -124,13 +124,14 @@ message ReqStrings {
//列表请求结构 //列表请求结构
message ReqListItem { message ReqListItem {
string startItemID = 1; //列表开始的ID,如请求组列表即groupID,不包含在结果中 string startItemID = 1; //列表开始的ID,如请求组列表即groupID,不包含在结果中
int32 count = 2; //请求列表项数量 int32 count = 2; //请求列表项数量, 0表示请求所有
int32 direction = 3; // 0表示根据ID降序,1表示升序 int32 direction = 3; // 0表示根据ID降序,1表示升序,目前ID和区块高度正相关
} }
message ReqListVote { message ReqListVote {
string groupID = 1; //所属组ID string groupID = 1; //指定所属组ID
ReqListItem listReq = 2; //列表请求 ReqListItem listReq = 2; //列表请求
uint32 status = 3; //指定投票状态
} }
message ReplyVoteList { message ReplyVoteList {
......
...@@ -353,9 +353,9 @@ curl -kd '{"method":"Chain33.Query","params":[{"execer":"vote","funcName":"GetM ...@@ -353,9 +353,9 @@ curl -kd '{"method":"Chain33.Query","params":[{"execer":"vote","funcName":"GetM
```proto ```proto
//列表请求结构 //列表请求结构
message ReqListItem { message ReqListItem {
string startItemID = 1; //列表开始的投票组ID,不包含在结果中 string startItemID = 1; //列表开始的ID,如请求组列表即groupID,不包含在结果中
int32 count = 2; //列表项单次请求数量 int32 count = 2; //请求列表项数量, 0表示请求所有
int32 direction = 3; // 0表示根据ID降序,1表示升序 int32 direction = 3; // 0表示根据ID降序,1表示升序,目前ID和区块高度正相关
} }
``` ```
...@@ -381,14 +381,15 @@ curl -kd '{"method":"Chain33.Query","params":[{"execer":"vote","funcName":"List ...@@ -381,14 +381,15 @@ curl -kd '{"method":"Chain33.Query","params":[{"execer":"vote","funcName":"List
```proto ```proto
//列表请求结构 //列表请求结构
message ReqListVote { message ReqListVote {
string groupID = 1; //所属组ID,不填时获取全局的投票列表 string groupID = 1; //指定所属组ID
ReqListItem listReq = 2; //列表请求 ReqListItem listReq = 2; //列表请求
uint32 status = 3; //指定投票状态
} }
message ReqListItem { message ReqListItem {
string startItemID = 1; //列表开始的投票ID,不包含在结果中 string startItemID = 1; //列表开始的ID,如请求组列表即groupID,不包含在结果中
int32 count = 2; //列表项单次请求数量 int32 count = 2; //请求列表项数量, 0表示请求所有
int32 direction = 3; // 0表示根据ID降序,1表示升序 int32 direction = 3; // 0表示根据ID降序,1表示升序,目前ID和区块高度正相关
} }
``` ```
...@@ -418,9 +419,9 @@ curl -kd '{"method":"Chain33.Query","params":[{"execer":"vote","funcName":"List ...@@ -418,9 +419,9 @@ curl -kd '{"method":"Chain33.Query","params":[{"execer":"vote","funcName":"List
```proto ```proto
//列表请求结构 //列表请求结构
message ReqListItem { message ReqListItem {
string startItemID = 1; //列表开始的用户ID(地址) string startItemID = 1; //列表开始的ID,如请求组列表即groupID,不包含在结果中
int32 count = 2; //列表项单次请求数量 int32 count = 2; //请求列表项数量, 0表示请求所有
int32 direction = 3; // 0表示根据ID降序,1表示升序 int32 direction = 3; // 0表示根据ID降序,1表示升序,目前ID和区块高度正相关
} }
``` ```
......
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment