mirror of https://github.com/gophish/gophish
Initial suport for pagination
This commit adds support for pagination for users (Users and Groups menu item).pagination-support
parent
8b8e88b077
commit
90d46eb463
|
@ -4,11 +4,13 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
ctx "github.com/gophish/gophish/context"
|
ctx "github.com/gophish/gophish/context"
|
||||||
log "github.com/gophish/gophish/logger"
|
log "github.com/gophish/gophish/logger"
|
||||||
"github.com/gophish/gophish/models"
|
"github.com/gophish/gophish/models"
|
||||||
|
"github.com/gophish/gophish/util"
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
"github.com/jinzhu/gorm"
|
"github.com/jinzhu/gorm"
|
||||||
)
|
)
|
||||||
|
@ -28,12 +30,31 @@ func (as *Server) Groups(w http.ResponseWriter, r *http.Request) {
|
||||||
case r.Method == "POST":
|
case r.Method == "POST":
|
||||||
g := models.Group{}
|
g := models.Group{}
|
||||||
// Put the request into a group
|
// Put the request into a group
|
||||||
|
|
||||||
|
// Check if content is CSV
|
||||||
|
// NB We can only upload one file at a time for new group creation, as three seperate POSTs are sent in quick succesion. We can't
|
||||||
|
// identify which group the 1+n files should be assigned to. Need to give this more thought, perhaps we can upload multiple files in one POST,
|
||||||
|
// or assign a temporary tracking token to link them together.
|
||||||
|
var csvmode = false
|
||||||
|
contentType := r.Header.Get("Content-Type")
|
||||||
|
if strings.HasPrefix(contentType, "multipart/form-data") {
|
||||||
|
csvmode = true
|
||||||
|
targets, groupname, err := util.ParseCSV(r)
|
||||||
|
if err != nil {
|
||||||
|
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
g.Name = groupname
|
||||||
|
g.Targets = targets
|
||||||
|
} else {
|
||||||
|
// else decode as JSON
|
||||||
err := json.NewDecoder(r.Body).Decode(&g)
|
err := json.NewDecoder(r.Body).Decode(&g)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
JSONResponse(w, models.Response{Success: false, Message: "Invalid JSON structure"}, http.StatusBadRequest)
|
JSONResponse(w, models.Response{Success: false, Message: "Invalid JSON structure"}, http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
_, err = models.GetGroupByName(g.Name, ctx.Get(r, "user_id").(int64))
|
}
|
||||||
|
_, err := models.GetGroupByName(g.Name, ctx.Get(r, "user_id").(int64))
|
||||||
if err != gorm.ErrRecordNotFound {
|
if err != gorm.ErrRecordNotFound {
|
||||||
JSONResponse(w, models.Response{Success: false, Message: "Group name already in use"}, http.StatusConflict)
|
JSONResponse(w, models.Response{Success: false, Message: "Group name already in use"}, http.StatusConflict)
|
||||||
return
|
return
|
||||||
|
@ -45,6 +66,12 @@ func (as *Server) Groups(w http.ResponseWriter, r *http.Request) {
|
||||||
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusBadRequest)
|
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// With CSV we don't return the entire target list, in line with the new pagination server side processing. To maintain backwards API capabiltiy the JSON request
|
||||||
|
// will still return the full list.
|
||||||
|
if csvmode == true {
|
||||||
|
JSONResponse(w, models.GroupSummary{Id: g.Id, Name: g.Name, ModifiedDate: g.ModifiedDate, NumTargets: int64(len(g.Targets))}, http.StatusCreated)
|
||||||
|
return
|
||||||
|
}
|
||||||
JSONResponse(w, g, http.StatusCreated)
|
JSONResponse(w, g, http.StatusCreated)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -68,14 +95,51 @@ func (as *Server) GroupsSummary(w http.ResponseWriter, r *http.Request) {
|
||||||
func (as *Server) Group(w http.ResponseWriter, r *http.Request) {
|
func (as *Server) Group(w http.ResponseWriter, r *http.Request) {
|
||||||
vars := mux.Vars(r)
|
vars := mux.Vars(r)
|
||||||
id, _ := strconv.ParseInt(vars["id"], 0, 64)
|
id, _ := strconv.ParseInt(vars["id"], 0, 64)
|
||||||
g, err := models.GetGroup(id, ctx.Get(r, "user_id").(int64))
|
|
||||||
|
// Paramters passed by DataTables for pagination are handled below
|
||||||
|
v := r.URL.Query()
|
||||||
|
search := v.Get("search[value]")
|
||||||
|
|
||||||
|
sortcolumn := v.Get("order[0][column]")
|
||||||
|
sortdir := v.Get("order[0][dir]")
|
||||||
|
sortby := v.Get("columns[" + sortcolumn + "][data]")
|
||||||
|
order := sortby + " " + sortdir // e.g "first_name asc"
|
||||||
|
|
||||||
|
start, err := strconv.ParseInt(v.Get("start"), 0, 64)
|
||||||
|
if err != nil {
|
||||||
|
start = -1 // Default. gorm will ignore with this value.
|
||||||
|
}
|
||||||
|
length, err := strconv.ParseInt(v.Get("length"), 0, 64)
|
||||||
|
if err != nil {
|
||||||
|
length = -1 // Default. gorm will ignore with this value.
|
||||||
|
}
|
||||||
|
draw, err := strconv.ParseInt(v.Get("draw"), 0, 64)
|
||||||
|
if err != nil {
|
||||||
|
draw = -1 // If the draw value is missing we can assume this is not a DataTable request and return regular API result
|
||||||
|
}
|
||||||
|
|
||||||
|
g, err := models.GetGroup(id, ctx.Get(r, "user_id").(int64), start, length, search, order)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
JSONResponse(w, models.Response{Success: false, Message: "Group not found"}, http.StatusNotFound)
|
JSONResponse(w, models.Response{Success: false, Message: "Group not found"}, http.StatusNotFound)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
switch {
|
switch {
|
||||||
case r.Method == "GET":
|
case r.Method == "GET":
|
||||||
|
|
||||||
|
// If draw paratmer is -1 return regular API response, otherwise return pagination response
|
||||||
|
if draw == -1 {
|
||||||
JSONResponse(w, g, http.StatusOK)
|
JSONResponse(w, g, http.StatusOK)
|
||||||
|
} else {
|
||||||
|
// Handle pagination for DataTable
|
||||||
|
gs, _ := models.GetGroupSummary(id, ctx.Get(r, "user_id").(int64)) // We need to get the total number of records of the group
|
||||||
|
dT := models.DataTable{Draw: draw, RecordsTotal: gs.NumTargets, RecordsFiltered: gs.NumTargets}
|
||||||
|
dT.Data = make([]interface{}, len(g.Targets)) // Pseudocode of 'dT.Data = g.Targets'. https://golang.org/doc/faq#convert_slice_of_interface
|
||||||
|
for i, v := range g.Targets {
|
||||||
|
dT.Data[i] = v
|
||||||
|
}
|
||||||
|
JSONResponse(w, dT, http.StatusOK)
|
||||||
|
}
|
||||||
|
|
||||||
case r.Method == "DELETE":
|
case r.Method == "DELETE":
|
||||||
err = models.DeleteGroup(&g)
|
err = models.DeleteGroup(&g)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -86,12 +150,30 @@ func (as *Server) Group(w http.ResponseWriter, r *http.Request) {
|
||||||
case r.Method == "PUT":
|
case r.Method == "PUT":
|
||||||
// Change this to get from URL and uid (don't bother with id in r.Body)
|
// Change this to get from URL and uid (don't bother with id in r.Body)
|
||||||
g = models.Group{}
|
g = models.Group{}
|
||||||
|
|
||||||
|
//Check if content is CSV
|
||||||
|
var csvmode = false
|
||||||
|
contentType := r.Header.Get("Content-Type")
|
||||||
|
if strings.HasPrefix(contentType, "multipart/form-data") {
|
||||||
|
csvmode = true
|
||||||
|
targets, groupname, err := util.ParseCSV(r)
|
||||||
|
if err != nil {
|
||||||
|
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// We need to fetch all the existing targets for this group, so as to not overwrite them below
|
||||||
|
et, _ := models.GetTargets(id, -1, -1, "", "")
|
||||||
|
g.Targets = append(targets, et...)
|
||||||
|
g.Name = groupname
|
||||||
|
g.Id = id // ID isn't supplied in the CSV file upload. Perhaps we could use the filename paramter for this? I'm not sure if this is necessary though.
|
||||||
|
} else { // else JSON
|
||||||
err = json.NewDecoder(r.Body).Decode(&g)
|
err = json.NewDecoder(r.Body).Decode(&g)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("error decoding group: %v", err)
|
log.Errorf("error decoding group: %v", err)
|
||||||
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusInternalServerError)
|
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if g.Id != id {
|
if g.Id != id {
|
||||||
JSONResponse(w, models.Response{Success: false, Message: "Error: /:id and group_id mismatch"}, http.StatusInternalServerError)
|
JSONResponse(w, models.Response{Success: false, Message: "Error: /:id and group_id mismatch"}, http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
|
@ -103,6 +185,12 @@ func (as *Server) Group(w http.ResponseWriter, r *http.Request) {
|
||||||
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusBadRequest)
|
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// With CSV we don't return the entire target list, in line with the new pagination server side processing. To maintain backwards API capabiltiy the JSON request
|
||||||
|
// will still return the full list.
|
||||||
|
if csvmode == true {
|
||||||
|
JSONResponse(w, models.GroupSummary{Id: g.Id, Name: g.Name, ModifiedDate: g.ModifiedDate, NumTargets: int64(len(g.Targets))}, http.StatusCreated)
|
||||||
|
return
|
||||||
|
}
|
||||||
JSONResponse(w, g, http.StatusOK)
|
JSONResponse(w, g, http.StatusOK)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -121,3 +209,81 @@ func (as *Server) GroupSummary(w http.ResponseWriter, r *http.Request) {
|
||||||
JSONResponse(w, g, http.StatusOK)
|
JSONResponse(w, g, http.StatusOK)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GroupTarget handles interactions with individual targets
|
||||||
|
func (as *Server) GroupTarget(w http.ResponseWriter, r *http.Request) {
|
||||||
|
vars := mux.Vars(r)
|
||||||
|
gid, _ := strconv.ParseInt(vars["id"], 0, 64) // group id
|
||||||
|
|
||||||
|
// Ensure the group belongs to the user
|
||||||
|
_, err := models.GetGroup(gid, ctx.Get(r, "user_id").(int64), 0, 0, "", "")
|
||||||
|
if err != nil {
|
||||||
|
JSONResponse(w, models.Response{Success: false, Message: "Group not found"}, http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
t := models.Target{}
|
||||||
|
err = json.NewDecoder(r.Body).Decode(&t)
|
||||||
|
if err != nil {
|
||||||
|
JSONResponse(w, models.Response{Success: false, Message: "Unable to decode target"}, http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
switch {
|
||||||
|
case r.Method == "PUT":
|
||||||
|
// Add an individual target to a group
|
||||||
|
err = models.AddTargetToGroup(t, gid)
|
||||||
|
if err != nil {
|
||||||
|
JSONResponse(w, models.Response{Success: false, Message: "Unable to add target to group"}, http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
JSONResponse(w, models.Response{Success: true, Message: "Added target to group"}, http.StatusCreated)
|
||||||
|
case r.Method == "DELETE":
|
||||||
|
|
||||||
|
err := models.DeleteTarget(&t, gid, ctx.Get(r, "user_id").(int64)) // We pass the group id to update modified date, and userid to ensure user owner the target
|
||||||
|
if err != nil {
|
||||||
|
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
JSONResponse(w, models.Response{Success: true, Message: "Deleted target"}, http.StatusCreated)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GroupRename handles renaming of a group (without supplying all the targets, as in Group() PUT above)
|
||||||
|
func (as *Server) GroupRename(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
|
vars := mux.Vars(r)
|
||||||
|
id, _ := strconv.ParseInt(vars["id"], 0, 64) // group id
|
||||||
|
g, err := models.GetGroup(id, ctx.Get(r, "user_id").(int64), 0, 0, "", "")
|
||||||
|
if err != nil {
|
||||||
|
JSONResponse(w, models.Response{Success: false, Message: "Group not found"}, http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case r.Method == "PUT":
|
||||||
|
g = models.Group{}
|
||||||
|
err = json.NewDecoder(r.Body).Decode(&g)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("error decoding group: %v", err)
|
||||||
|
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if g.Id != id {
|
||||||
|
JSONResponse(w, models.Response{Success: false, Message: "Error: /:id and group_id mismatch"}, http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_, err := models.GetGroupByName(g.Name, ctx.Get(r, "user_id").(int64))
|
||||||
|
if err != gorm.ErrRecordNotFound {
|
||||||
|
JSONResponse(w, models.Response{Success: false, Message: "Group name already in use"}, http.StatusConflict)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
g.ModifiedDate = time.Now().UTC()
|
||||||
|
g.UserId = ctx.Get(r, "user_id").(int64)
|
||||||
|
err = models.UpdateGroup(&g)
|
||||||
|
if err != nil {
|
||||||
|
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
JSONResponse(w, models.Response{Success: true, Message: "Group renamed"}, http.StatusCreated)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -41,7 +41,7 @@ type emailResponse struct {
|
||||||
|
|
||||||
// ImportGroup imports a CSV of group members
|
// ImportGroup imports a CSV of group members
|
||||||
func (as *Server) ImportGroup(w http.ResponseWriter, r *http.Request) {
|
func (as *Server) ImportGroup(w http.ResponseWriter, r *http.Request) {
|
||||||
ts, err := util.ParseCSV(r)
|
ts, _, err := util.ParseCSV(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
JSONResponse(w, models.Response{Success: false, Message: "Error parsing CSV"}, http.StatusInternalServerError)
|
JSONResponse(w, models.Response{Success: false, Message: "Error parsing CSV"}, http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
|
|
|
@ -71,6 +71,9 @@ func (as *Server) registerRoutes() {
|
||||||
router.HandleFunc("/groups/summary", as.GroupsSummary)
|
router.HandleFunc("/groups/summary", as.GroupsSummary)
|
||||||
router.HandleFunc("/groups/{id:[0-9]+}", as.Group)
|
router.HandleFunc("/groups/{id:[0-9]+}", as.Group)
|
||||||
router.HandleFunc("/groups/{id:[0-9]+}/summary", as.GroupSummary)
|
router.HandleFunc("/groups/{id:[0-9]+}/summary", as.GroupSummary)
|
||||||
|
router.HandleFunc("/groups/{id:[0-9]+}/rename", as.GroupRename)
|
||||||
|
router.HandleFunc("/groups/{id:[0-9]+}/target/add", as.GroupTarget)
|
||||||
|
router.HandleFunc("/groups/{id:[0-9]+}/target/delete", as.GroupTarget)
|
||||||
router.HandleFunc("/templates/", as.Templates)
|
router.HandleFunc("/templates/", as.Templates)
|
||||||
router.HandleFunc("/templates/{id:[0-9]+}", as.Template)
|
router.HandleFunc("/templates/{id:[0-9]+}", as.Template)
|
||||||
router.HandleFunc("/pages/", as.Pages)
|
router.HandleFunc("/pages/", as.Pages)
|
||||||
|
|
116
models/group.go
116
models/group.go
|
@ -4,6 +4,7 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/mail"
|
"net/mail"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
log "github.com/gophish/gophish/logger"
|
log "github.com/gophish/gophish/logger"
|
||||||
|
@ -46,7 +47,7 @@ type GroupTarget struct {
|
||||||
// Target contains the fields needed for individual targets specified by the user
|
// Target contains the fields needed for individual targets specified by the user
|
||||||
// Groups contain 1..* Targets, but 1 Target may belong to 1..* Groups
|
// Groups contain 1..* Targets, but 1 Target may belong to 1..* Groups
|
||||||
type Target struct {
|
type Target struct {
|
||||||
Id int64 `json:"-"`
|
Id int64 `json:"id"`
|
||||||
BaseRecipient
|
BaseRecipient
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -59,6 +60,15 @@ type BaseRecipient struct {
|
||||||
Position string `json:"position"`
|
Position string `json:"position"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DataTable is used to return a JSON object suitable for consumption by DataTables
|
||||||
|
// when using pagination
|
||||||
|
type DataTable struct {
|
||||||
|
Draw int64 `json:"draw"`
|
||||||
|
RecordsTotal int64 `json:"recordsTotal"`
|
||||||
|
RecordsFiltered int64 `json:"recordsFiltered"`
|
||||||
|
Data []interface{} `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
// FormatAddress returns the email address to use in the "To" header of the email
|
// FormatAddress returns the email address to use in the "To" header of the email
|
||||||
func (r *BaseRecipient) FormatAddress() string {
|
func (r *BaseRecipient) FormatAddress() string {
|
||||||
addr := r.Email
|
addr := r.Email
|
||||||
|
@ -114,7 +124,7 @@ func GetGroups(uid int64) ([]Group, error) {
|
||||||
return gs, err
|
return gs, err
|
||||||
}
|
}
|
||||||
for i := range gs {
|
for i := range gs {
|
||||||
gs[i].Targets, err = GetTargets(gs[i].Id)
|
gs[i].Targets, err = GetTargets(gs[i].Id, -1, -1, "", "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error(err)
|
log.Error(err)
|
||||||
}
|
}
|
||||||
|
@ -144,14 +154,15 @@ func GetGroupSummaries(uid int64) (GroupSummaries, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetGroup returns the group, if it exists, specified by the given id and user_id.
|
// GetGroup returns the group, if it exists, specified by the given id and user_id.
|
||||||
func GetGroup(id int64, uid int64) (Group, error) {
|
// Filter on number of results and starting point with 'start' and 'length' for pagination.
|
||||||
|
func GetGroup(id int64, uid int64, start int64, length int64, search string, order string) (Group, error) {
|
||||||
g := Group{}
|
g := Group{}
|
||||||
err := db.Where("user_id=? and id=?", uid, id).Find(&g).Error
|
err := db.Where("user_id=? and id=?", uid, id).Find(&g).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error(err)
|
log.Error(err)
|
||||||
return g, err
|
return g, err
|
||||||
}
|
}
|
||||||
g.Targets, err = GetTargets(g.Id)
|
g.Targets, err = GetTargets(g.Id, start, length, search, order)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error(err)
|
log.Error(err)
|
||||||
}
|
}
|
||||||
|
@ -183,7 +194,7 @@ func GetGroupByName(n string, uid int64) (Group, error) {
|
||||||
log.Error(err)
|
log.Error(err)
|
||||||
return g, err
|
return g, err
|
||||||
}
|
}
|
||||||
g.Targets, err = GetTargets(g.Id)
|
g.Targets, err = GetTargets(g.Id, -1, -1, "", "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error(err)
|
log.Error(err)
|
||||||
}
|
}
|
||||||
|
@ -226,7 +237,7 @@ func PutGroup(g *Group) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
// Fetch group's existing targets from database.
|
// Fetch group's existing targets from database.
|
||||||
ts, err := GetTargets(g.Id)
|
ts, err := GetTargets(g.Id, -1, -1, "", "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WithFields(logrus.Fields{
|
log.WithFields(logrus.Fields{
|
||||||
"group_id": g.Id,
|
"group_id": g.Id,
|
||||||
|
@ -312,6 +323,63 @@ func DeleteGroup(g *Group) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DeleteTarget deletes a single target from a group given by target ID
|
||||||
|
func DeleteTarget(t *Target, gid int64, uid int64) error {
|
||||||
|
|
||||||
|
targetOwner, err := GetTargetOwner(t.Id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if targetOwner != uid {
|
||||||
|
return errors.New("No such target id (wrong owner)")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = db.Delete(t).Error
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = db.Where("target_id=?", t.Id).Delete(&GroupTarget{}).Error
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// Update group modification date
|
||||||
|
err = db.Debug().Model(&Group{}).Where("id=?", gid).Update("ModifiedDate", time.Now().UTC()).Error
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateGroup updates a given group (without updating the targets)
|
||||||
|
// Note: I thought about putting this in the Group() function, but we'd have to skip the validation and have a boolean
|
||||||
|
// indicating we just want to rename the group.
|
||||||
|
func UpdateGroup(g *Group) error {
|
||||||
|
if g.Name == "" {
|
||||||
|
return ErrGroupNameNotSpecified
|
||||||
|
}
|
||||||
|
err := db.Save(g).Error
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddTargetToGroup adds a single given target to a group by group ID
|
||||||
|
func AddTargetToGroup(nt Target, gid int64) error {
|
||||||
|
// Check if target already exists in group
|
||||||
|
tmpt, err := GetTargetByEmail(gid, nt.Email)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// If target exists in group, update it.
|
||||||
|
if len(tmpt) > 0 {
|
||||||
|
nt.Id = tmpt[0].Id
|
||||||
|
err = UpdateTarget(db, nt)
|
||||||
|
} else {
|
||||||
|
err = insertTargetIntoGroup(db, nt, gid)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = db.Model(&Group{}).Where("id=?", gid).Update("ModifiedDate", time.Now().UTC()).Error
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
func insertTargetIntoGroup(tx *gorm.DB, t Target, gid int64) error {
|
func insertTargetIntoGroup(tx *gorm.DB, t Target, gid int64) error {
|
||||||
if _, err := mail.ParseAddress(t.Email); err != nil {
|
if _, err := mail.ParseAddress(t.Email); err != nil {
|
||||||
log.WithFields(logrus.Fields{
|
log.WithFields(logrus.Fields{
|
||||||
|
@ -357,8 +425,40 @@ func UpdateTarget(tx *gorm.DB, target Target) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetTargets performs a many-to-many select to get all the Targets for a Group
|
// GetTargets performs a many-to-many select to get all the Targets for a Group
|
||||||
func GetTargets(gid int64) ([]Target, error) {
|
// Start, length, and search can be supplied, or -1, -1, "" to ignore
|
||||||
|
func GetTargets(gid int64, start int64, length int64, search string, order string) ([]Target, error) {
|
||||||
|
|
||||||
ts := []Target{}
|
ts := []Target{}
|
||||||
err := db.Table("targets").Select("targets.id, targets.email, targets.first_name, targets.last_name, targets.position").Joins("left join group_targets gt ON targets.id = gt.target_id").Where("gt.group_id=?", gid).Scan(&ts).Error
|
var err error
|
||||||
|
|
||||||
|
order = strings.TrimSpace(order)
|
||||||
|
search = strings.TrimSpace(search)
|
||||||
|
if order == "" {
|
||||||
|
order = "targets.first_name asc"
|
||||||
|
} else {
|
||||||
|
order = "targets." + order
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Rather than having two queries create a partial query and include the search options. Haven't been able to figure out how yet.
|
||||||
|
if search != "" {
|
||||||
|
search = "%" + search + "%"
|
||||||
|
err = db.Order(order).Table("targets").Select("targets.id, targets.email, targets.first_name, targets.last_name, targets.position").Joins("left join group_targets gt ON targets.id = gt.target_id").Where("gt.group_id=?", gid).Where("targets.first_name LIKE ? OR targets.last_name LIKE ? OR targets.email LIKE ? or targets.position LIKE ?", search, search, search, search, search).Offset(start).Limit(length).Scan(&ts).Error
|
||||||
|
} else {
|
||||||
|
err = db.Order(order).Table("targets").Select("targets.id, targets.email, targets.first_name, targets.last_name, targets.position").Joins("left join group_targets gt ON targets.id = gt.target_id").Where("gt.group_id=?", gid).Offset(start).Limit(length).Scan(&ts).Error
|
||||||
|
}
|
||||||
return ts, err
|
return ts, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetTargetByEmail gets a single target from a group by email address and group id
|
||||||
|
func GetTargetByEmail(gid int64, email string) ([]Target, error) {
|
||||||
|
ts := []Target{}
|
||||||
|
err := db.Table("targets").Select("targets.id, targets.email, targets.first_name, targets.last_name, targets.position").Joins("left join group_targets gt ON targets.id = gt.target_id").Where("gt.group_id=?", gid).Where("targets.email=?", email).First(&ts).Error
|
||||||
|
return ts, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTargetOwner returns the user id owner of a given target id
|
||||||
|
func GetTargetOwner(tid int64) (int64, error) {
|
||||||
|
g := Group{}
|
||||||
|
err := db.Table("groups").Select("groups.user_id").Joins("left join group_targets on group_targets.group_id = groups.id").Where("group_targets.target_id = ?", tid).Scan(&g).Error
|
||||||
|
return g.UserId, err
|
||||||
|
}
|
||||||
|
|
|
@ -137,6 +137,26 @@ var api = {
|
||||||
// delete() - Deletes a group at DELETE /groups/:id
|
// delete() - Deletes a group at DELETE /groups/:id
|
||||||
delete: function (id) {
|
delete: function (id) {
|
||||||
return query("/groups/" + id, "DELETE", {}, false)
|
return query("/groups/" + id, "DELETE", {}, false)
|
||||||
|
},
|
||||||
|
// summary() - Queries the API for GET /groups/:id/summary
|
||||||
|
summary: function (id) {
|
||||||
|
return query("/groups/" + id + "/summary", "GET", {}, true)
|
||||||
|
},
|
||||||
|
// rename() - Renames a group
|
||||||
|
rename: function(id, group) {
|
||||||
|
return query("/groups/" + id + "/rename", "PUT", group, false)
|
||||||
|
},
|
||||||
|
// add() - Adds a single target to a group
|
||||||
|
addtarget: function(id, target) {
|
||||||
|
return query("/groups/" + id + "/target/add", "PUT", target, false)
|
||||||
|
},
|
||||||
|
// delete() - Deletes a single targert by id at DELETE /groups/:id
|
||||||
|
deletetarget: function (id, target) {
|
||||||
|
return query("/groups/" + id + "/target/delete", "DELETE", target, false)
|
||||||
|
},
|
||||||
|
// post() - Posts a CSV group to POST /groups/:id/csv
|
||||||
|
post: function (id, data) {
|
||||||
|
return query("/groups/" + id + "/csv", "POST", data, false)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// templates contains the endpoints for /templates
|
// templates contains the endpoints for /templates
|
||||||
|
|
|
@ -1,99 +1,82 @@
|
||||||
var groups = []
|
var groups = []
|
||||||
|
|
||||||
// Save attempts to POST or PUT to /groups/
|
|
||||||
function save(id) {
|
|
||||||
var targets = []
|
|
||||||
$.each($("#targetsTable").DataTable().rows().data(), function (i, target) {
|
|
||||||
targets.push({
|
|
||||||
first_name: unescapeHtml(target[0]),
|
|
||||||
last_name: unescapeHtml(target[1]),
|
|
||||||
email: unescapeHtml(target[2]),
|
|
||||||
position: unescapeHtml(target[3])
|
|
||||||
})
|
|
||||||
})
|
|
||||||
var group = {
|
|
||||||
name: $("#name").val(),
|
|
||||||
targets: targets
|
|
||||||
}
|
|
||||||
// Submit the group
|
|
||||||
if (id != -1) {
|
|
||||||
// If we're just editing an existing group,
|
|
||||||
// we need to PUT /groups/:id
|
|
||||||
group.id = id
|
|
||||||
api.groupId.put(group)
|
|
||||||
.success(function (data) {
|
|
||||||
successFlash("Group updated successfully!")
|
|
||||||
load()
|
|
||||||
dismiss()
|
|
||||||
$("#modal").modal('hide')
|
|
||||||
})
|
|
||||||
.error(function (data) {
|
|
||||||
modalError(data.responseJSON.message)
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
// Else, if this is a new group, POST it
|
|
||||||
// to /groups
|
|
||||||
api.groups.post(group)
|
|
||||||
.success(function (data) {
|
|
||||||
successFlash("Group added successfully!")
|
|
||||||
load()
|
|
||||||
dismiss()
|
|
||||||
$("#modal").modal('hide')
|
|
||||||
})
|
|
||||||
.error(function (data) {
|
|
||||||
modalError(data.responseJSON.message)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function dismiss() {
|
function dismiss() {
|
||||||
$("#targetsTable").dataTable().DataTable().clear().draw()
|
$("#targetsTable").dataTable().DataTable().clear().draw()
|
||||||
$("#name").val("")
|
$("#name").val("")
|
||||||
$("#modal\\.flashes").empty()
|
$("#modal\\.flashes").empty()
|
||||||
}
|
}
|
||||||
|
|
||||||
function edit(id) {
|
function instantiateDataTable(id) {
|
||||||
targets = $("#targetsTable").dataTable({
|
$('#targetsTable').dataTable( {
|
||||||
destroy: true, // Destroy any other instantiated table - http://datatables.net/manual/tech-notes/3#destroy
|
destroy: true, // Destroy any other instantiated table - http://datatables.net/manual/tech-notes/3#destroy
|
||||||
|
select: true,
|
||||||
columnDefs: [{
|
columnDefs: [{
|
||||||
orderable: false,
|
orderable: false,
|
||||||
targets: "no-sort"
|
targets: "no-sort"
|
||||||
}]
|
}],
|
||||||
})
|
"processing": true,
|
||||||
$("#modalSubmit").unbind('click').click(function () {
|
"serverSide": true,
|
||||||
save(id)
|
"ajax": {
|
||||||
})
|
url: "/api/groups/" + id,
|
||||||
if (id == -1) {
|
'beforeSend': function (request) {
|
||||||
var group = {}
|
request.setRequestHeader("Authorization", "Bearer " + user.api_key);
|
||||||
} else {
|
|
||||||
api.groupId.get(id)
|
|
||||||
.success(function (group) {
|
|
||||||
$("#name").val(group.name)
|
|
||||||
targetRows = []
|
|
||||||
$.each(group.targets, function (i, record) {
|
|
||||||
targetRows.push([
|
|
||||||
escapeHtml(record.first_name),
|
|
||||||
escapeHtml(record.last_name),
|
|
||||||
escapeHtml(record.email),
|
|
||||||
escapeHtml(record.position),
|
|
||||||
'<span style="cursor:pointer;"><i class="fa fa-trash-o"></i></span>'
|
|
||||||
])
|
|
||||||
});
|
|
||||||
targets.DataTable().rows.add(targetRows).draw()
|
|
||||||
})
|
|
||||||
.error(function () {
|
|
||||||
errorFlash("Error fetching group")
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
columns: [
|
||||||
|
{ data: 'first_name', render: escapeHtml},
|
||||||
|
{ data: 'last_name', render: escapeHtml },
|
||||||
|
{ data: 'email', render: escapeHtml },
|
||||||
|
{ data: 'position', render: escapeHtml },
|
||||||
|
{ data: null,
|
||||||
|
render: function ( data, type, row ) {
|
||||||
|
return '<span style="cursor:pointer;"><i class="fa fa-trash-o" id="' + data.id + '"></i></span>';
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
|
||||||
|
function edit(id) {
|
||||||
|
|
||||||
|
$("#groupid").val(id)
|
||||||
|
|
||||||
|
if (id == -1 ){ // New group
|
||||||
|
$("#targetsTable_wrapper").hide()
|
||||||
|
$("#targetsTable").hide()
|
||||||
|
} else {
|
||||||
|
api.groupId.summary(id)
|
||||||
|
.success(function (group) {
|
||||||
|
$("#name").val(escapeHtml(group.name))
|
||||||
|
})
|
||||||
|
.error(function (data) {
|
||||||
|
modalError("Error fetching group name")
|
||||||
|
})
|
||||||
|
instantiateDataTable(id)
|
||||||
|
}
|
||||||
|
|
||||||
// Handle file uploads
|
// Handle file uploads
|
||||||
|
csvurl = "/api/groups/" // New group
|
||||||
|
method = "POST"
|
||||||
|
if (id != -1) {
|
||||||
|
csvurl = "/api/groups/" + id // Update existing group
|
||||||
|
method = "PUT"
|
||||||
|
}
|
||||||
$("#csvupload").fileupload({
|
$("#csvupload").fileupload({
|
||||||
url: "/api/import/group",
|
url: csvurl,
|
||||||
|
method: method,
|
||||||
dataType: "json",
|
dataType: "json",
|
||||||
beforeSend: function (xhr) {
|
beforeSend: function (xhr) {
|
||||||
xhr.setRequestHeader('Authorization', 'Bearer ' + user.api_key);
|
xhr.setRequestHeader('Authorization', 'Bearer ' + user.api_key);
|
||||||
},
|
},
|
||||||
add: function (e, data) {
|
add: function (e, data) {
|
||||||
$("#modal\\.flashes").empty()
|
$("#modal\\.flashes").empty()
|
||||||
|
name = $("#name").val()
|
||||||
|
data.paramName = escapeHtml(name) // Send group name
|
||||||
|
|
||||||
|
if (name == "") {
|
||||||
|
modalError("No group name supplied")
|
||||||
|
$("#name").focus();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
var acceptFileTypes = /(csv|txt)$/i;
|
var acceptFileTypes = /(csv|txt)$/i;
|
||||||
var filename = data.originalFiles[0]['name']
|
var filename = data.originalFiles[0]['name']
|
||||||
if (filename && !acceptFileTypes.test(filename.split(".").pop())) {
|
if (filename && !acceptFileTypes.test(filename.split(".").pop())) {
|
||||||
|
@ -103,15 +86,30 @@ function edit(id) {
|
||||||
data.submit();
|
data.submit();
|
||||||
},
|
},
|
||||||
done: function (e, data) {
|
done: function (e, data) {
|
||||||
$.each(data.result, function (i, record) {
|
if (!('id' in data.result)) {
|
||||||
addTarget(
|
modalError("Failed to upload CSV file")
|
||||||
record.first_name,
|
} else {
|
||||||
record.last_name,
|
$("#targetsTable_wrapper").show()
|
||||||
record.email,
|
$("#targetsTable").show()
|
||||||
record.position);
|
edit(data.result.id)
|
||||||
});
|
load()
|
||||||
targets.DataTable().draw();
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveGroupName() {
|
||||||
|
id = parseInt($("#groupid").val())
|
||||||
|
if (id == -1) { return }
|
||||||
|
name = $("#name").val() // Check for length etc + handle escapes
|
||||||
|
data = {"id": id, "name":name}
|
||||||
|
|
||||||
|
api.groupId.rename(id, data)
|
||||||
|
.success(function (msg) {
|
||||||
|
load()
|
||||||
|
})
|
||||||
|
.error(function (data) {
|
||||||
|
modalError(data.responseJSON.message)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -140,7 +138,6 @@ var downloadCSVTemplate = function () {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
var deleteGroup = function (id) {
|
var deleteGroup = function (id) {
|
||||||
var group = groups.find(function (x) {
|
var group = groups.find(function (x) {
|
||||||
return x.id === id
|
return x.id === id
|
||||||
|
@ -184,33 +181,46 @@ var deleteGroup = function (id) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function addTarget(firstNameInput, lastNameInput, emailInput, positionInput) {
|
function addTarget(firstNameInput, lastNameInput, emailInput, positionInput) {
|
||||||
// Create new data row.
|
|
||||||
var email = escapeHtml(emailInput).toLowerCase();
|
|
||||||
var newRow = [
|
|
||||||
escapeHtml(firstNameInput),
|
|
||||||
escapeHtml(lastNameInput),
|
|
||||||
email,
|
|
||||||
escapeHtml(positionInput),
|
|
||||||
'<span style="cursor:pointer;"><i class="fa fa-trash-o"></i></span>'
|
|
||||||
];
|
|
||||||
|
|
||||||
// Check table to see if email already exists.
|
$("#modal\\.flashes").empty()
|
||||||
var targetsTable = targets.DataTable();
|
groupId = $("#groupid").val()
|
||||||
var existingRowIndex = targetsTable
|
target = {
|
||||||
.column(2, {
|
"email": emailInput.toLowerCase(),
|
||||||
order: "index"
|
"first_name": firstNameInput,
|
||||||
}) // Email column has index of 2
|
"last_name": lastNameInput,
|
||||||
.data()
|
"position": positionInput
|
||||||
.indexOf(email);
|
}
|
||||||
// Update or add new row as necessary.
|
|
||||||
if (existingRowIndex >= 0) {
|
if (groupId == -1){ // Create new group with target
|
||||||
targetsTable
|
groupname = $("#name").val()
|
||||||
.row(existingRowIndex, {
|
groupname = escapeHtml(groupname)
|
||||||
order: "index"
|
group = {"name": groupname, targets: [target]}
|
||||||
})
|
|
||||||
.data(newRow);
|
api.groups.post(group)
|
||||||
|
.success(function (data) {
|
||||||
|
//ajax fertch and show table
|
||||||
|
if (!('id' in data)) {
|
||||||
|
modalError("Failed to add target")
|
||||||
} else {
|
} else {
|
||||||
targetsTable.row.add(newRow);
|
instantiateDataTable(data.id)
|
||||||
|
$('#targetsTable').DataTable().draw('page');
|
||||||
|
$('#targetsTable_wrapper').show()
|
||||||
|
$('#targetsTable').show()
|
||||||
|
$("#groupid").val(data.id)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.error(function (data) {
|
||||||
|
modalError(data.responseJSON.message)
|
||||||
|
})
|
||||||
|
|
||||||
|
} else { // Add single target to existing group
|
||||||
|
api.groupId.addtarget(groupId, target)
|
||||||
|
.success(function (data){
|
||||||
|
load()
|
||||||
|
})
|
||||||
|
.error(function (data) {
|
||||||
|
modalError(data.responseJSON.message)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -262,6 +272,14 @@ $(document).ready(function () {
|
||||||
// Setup the event listeners
|
// Setup the event listeners
|
||||||
// Handle manual additions
|
// Handle manual additions
|
||||||
$("#targetForm").submit(function () {
|
$("#targetForm").submit(function () {
|
||||||
|
|
||||||
|
// Validate group name is present
|
||||||
|
if ($("#name").val() == "") {
|
||||||
|
modalError("No group name supplied")
|
||||||
|
$("#name").focus();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// Validate the form data
|
// Validate the form data
|
||||||
var targetForm = document.getElementById("targetForm")
|
var targetForm = document.getElementById("targetForm")
|
||||||
if (!targetForm.checkValidity()) {
|
if (!targetForm.checkValidity()) {
|
||||||
|
@ -273,19 +291,35 @@ $(document).ready(function () {
|
||||||
$("#lastName").val(),
|
$("#lastName").val(),
|
||||||
$("#email").val(),
|
$("#email").val(),
|
||||||
$("#position").val());
|
$("#position").val());
|
||||||
targets.DataTable().draw();
|
|
||||||
|
$('#targetsTable').DataTable().draw();
|
||||||
|
|
||||||
// Reset user input.
|
// Reset user input.
|
||||||
$("#targetForm>div>input").val('');
|
$("#targetForm>div>input").val('');
|
||||||
$("#firstName").focus();
|
$("#firstName").focus();
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Handle Deletion
|
// Handle Deletion
|
||||||
$("#targetsTable").on("click", "span>i.fa-trash-o", function () {
|
$("#targetsTable").on("click", "span>i.fa-trash-o", function () {
|
||||||
targets.DataTable()
|
// We allow emtpy groups with this new pagination model. TODO: Do we need to revisit this?
|
||||||
|
targetId=parseInt(this.id)
|
||||||
|
groupId=$("#groupid").val()
|
||||||
|
target = {"id" : targetId}
|
||||||
|
|
||||||
|
api.groupId.deletetarget(groupId, target)
|
||||||
|
.success(function (msg) {
|
||||||
|
load()
|
||||||
|
})
|
||||||
|
.error(function (data) {
|
||||||
|
modalError("Failed to delete user. Please try again later.")
|
||||||
|
})
|
||||||
|
|
||||||
|
$('#targetsTable').DataTable()
|
||||||
.row($(this).parents('tr'))
|
.row($(this).parents('tr'))
|
||||||
.remove()
|
.remove()
|
||||||
.draw();
|
.draw('page');
|
||||||
|
|
||||||
});
|
});
|
||||||
$("#modal").on("hide.bs.modal", function () {
|
$("#modal").on("hide.bs.modal", function () {
|
||||||
dismiss();
|
dismiss();
|
||||||
|
|
|
@ -49,14 +49,20 @@
|
||||||
<div class="row" id="modal.flashes"></div>
|
<div class="row" id="modal.flashes"></div>
|
||||||
<label class="control-label" for="name">Name:</label>
|
<label class="control-label" for="name">Name:</label>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
|
<div class="input-group">
|
||||||
<input type="text" class="form-control" ng-model="group.name" placeholder="Group name" id="name"
|
<input type="text" class="form-control" ng-model="group.name" placeholder="Group name" id="name"
|
||||||
autofocus />
|
autofocus />
|
||||||
|
<span class="input-group-btn">
|
||||||
|
<button class="btn btn-default" onclick="saveGroupName()" type="button">Save</button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<input id="groupid" type="hidden"/>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<span class="btn btn-danger btn-file" data-toggle="tooltip" data-placement="right"
|
<span class="btn btn-danger btn-file" data-toggle="tooltip" data-placement="right"
|
||||||
title="Supports CSV files" id="fileUpload">
|
title="Supports CSV files" id="fileUpload">
|
||||||
<i class="fa fa-plus"></i> Bulk Import Users
|
<i class="fa fa-plus"></i> Bulk Import Users
|
||||||
<input type="file" id="csvupload" multiple>
|
<input type="file" id="csvupload" name=""> <!-- Removed 'multiple' keyword due to new CSV upload functionality. Fails with new group creation. See Groups function in api/group.go -->
|
||||||
</span>
|
</span>
|
||||||
<span id="csv-template" class="text-muted small">
|
<span id="csv-template" class="text-muted small">
|
||||||
<i class="fa fa-file-excel-o"></i> Download CSV Template</span>
|
<i class="fa fa-file-excel-o"></i> Download CSV Template</span>
|
||||||
|
@ -83,6 +89,7 @@
|
||||||
</div>
|
</div>
|
||||||
<br />
|
<br />
|
||||||
<table id="targetsTable" class="table table-hover table-striped table-condensed">
|
<table id="targetsTable" class="table table-hover table-striped table-condensed">
|
||||||
|
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>First Name</th>
|
<th>First Name</th>
|
||||||
|
@ -90,17 +97,20 @@
|
||||||
<th>Email</th>
|
<th>Email</th>
|
||||||
<th>Position</th>
|
<th>Position</th>
|
||||||
<th class="no-sort"></th>
|
<th class="no-sort"></th>
|
||||||
<tbody>
|
</tr>
|
||||||
</tbody>
|
</thead>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
|
|
||||||
<button type="button" class="btn btn-primary" id="modalSubmit">Save changes</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{end}} {{define "scripts"}}
|
{{end}} {{define "scripts"}}
|
||||||
|
<script>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</script>
|
||||||
<script src="/js/dist/app/groups.min.js"></script>
|
<script src="/js/dist/app/groups.min.js"></script>
|
||||||
{{end}}
|
{{end}}
|
|
@ -44,11 +44,12 @@ func ParseMail(r *http.Request) (email.Email, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ParseCSV contains the logic to parse the user provided csv file containing Target entries
|
// ParseCSV contains the logic to parse the user provided csv file containing Target entries
|
||||||
func ParseCSV(r *http.Request) ([]models.Target, error) {
|
func ParseCSV(r *http.Request) ([]models.Target, string, error) {
|
||||||
mr, err := r.MultipartReader()
|
mr, err := r.MultipartReader()
|
||||||
ts := []models.Target{}
|
ts := []models.Target{}
|
||||||
|
name := ""
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ts, err
|
return ts, name, err
|
||||||
}
|
}
|
||||||
for {
|
for {
|
||||||
part, err := mr.NextPart()
|
part, err := mr.NextPart()
|
||||||
|
@ -60,6 +61,7 @@ func ParseCSV(r *http.Request) ([]models.Target, error) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
defer part.Close()
|
defer part.Close()
|
||||||
|
name = part.FormName()
|
||||||
reader := csv.NewReader(part)
|
reader := csv.NewReader(part)
|
||||||
reader.TrimLeadingSpace = true
|
reader.TrimLeadingSpace = true
|
||||||
record, err := reader.Read()
|
record, err := reader.Read()
|
||||||
|
@ -121,7 +123,7 @@ func ParseCSV(r *http.Request) ([]models.Target, error) {
|
||||||
ts = append(ts, t)
|
ts = append(ts, t)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return ts, nil
|
return ts, name, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// CheckAndCreateSSL is a helper to setup self-signed certificates for the administrative interface.
|
// CheckAndCreateSSL is a helper to setup self-signed certificates for the administrative interface.
|
||||||
|
|
Loading…
Reference in New Issue