mirror of https://github.com/gophish/gophish
Merge pull request #1 from gophish/master
Catching up to the main branch from gophish/masterpull/91/head
commit
cf97520ddf
|
@ -3,11 +3,11 @@
|
||||||
gophish
|
gophish
|
||||||
=======
|
=======
|
||||||
|
|
||||||
[![Build Status](https://travis-ci.org/gophish/gophish.svg?branch=master)](https://travis-ci.org/gophish/gophish)
|
[![Build Status](https://travis-ci.org/gophish/gophish.svg?branch=master)](https://travis-ci.org/gophish/gophish) [![GoDoc](https://godoc.org/github.com/gophish/gophish?status.svg)](https://godoc.org/github.com/gophish/gophish)
|
||||||
|
|
||||||
Open-Source Phishing Toolkit
|
Open-Source Phishing Toolkit
|
||||||
|
|
||||||
Gophish is an open-source phishing toolkit designed for businesses and penetration testers. It provides the ability to quickly and easily setup and execute phishing engagements and security awareness training.
|
[Gophish](https://getgophish.com) is an open-source phishing toolkit designed for businesses and penetration testers. It provides the ability to quickly and easily setup and execute phishing engagements and security awareness training.
|
||||||
|
|
||||||
###Current Status
|
###Current Status
|
||||||
**Update 01/12/2016**
|
**Update 01/12/2016**
|
||||||
|
|
17
config.json
17
config.json
|
@ -1,10 +1,21 @@
|
||||||
{
|
{
|
||||||
"admin_url" : "127.0.0.1:3333",
|
"admin_server" : {
|
||||||
"phish_url" : "0.0.0.0:80",
|
"listen_url" : "127.0.0.1:3333",
|
||||||
|
"use_tls" : false,
|
||||||
|
"cert_path" : "example.crt",
|
||||||
|
"key_path" : "example.key"
|
||||||
|
},
|
||||||
|
"phish_server" : {
|
||||||
|
"listen_url" : "0.0.0.0:80",
|
||||||
|
"use_tls" : false,
|
||||||
|
"cert_path" : "example.crt",
|
||||||
|
"key_path": "example.key"
|
||||||
|
},
|
||||||
"smtp" : {
|
"smtp" : {
|
||||||
"host" : "smtp.example.com:25",
|
"host" : "smtp.example.com:25",
|
||||||
"user" : "username",
|
"user" : "username",
|
||||||
"pass" : "password"
|
"pass" : "password"
|
||||||
},
|
},
|
||||||
"dbpath" : "gophish.db"
|
"db_path" : "gophish.db",
|
||||||
|
"migrations_path" : "db/migrations/"
|
||||||
}
|
}
|
|
@ -13,12 +13,29 @@ type SMTPServer struct {
|
||||||
Password string `json:"password"`
|
Password string `json:"password"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AdminServer represents the Admin server configuration details
|
||||||
|
type AdminServer struct {
|
||||||
|
ListenURL string `json:"listen_url"`
|
||||||
|
UseTLS bool `json:"use_tls"`
|
||||||
|
CertPath string `json:"cert_path"`
|
||||||
|
KeyPath string `json:"key_path"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// PhishServer represents the Phish server configuration details
|
||||||
|
type PhishServer struct {
|
||||||
|
ListenURL string `json:"listen_url"`
|
||||||
|
UseTLS bool `json:"use_tls"`
|
||||||
|
CertPath string `json:"cert_path"`
|
||||||
|
KeyPath string `json:"key_path"`
|
||||||
|
}
|
||||||
|
|
||||||
// Config represents the configuration information.
|
// Config represents the configuration information.
|
||||||
type Config struct {
|
type Config struct {
|
||||||
AdminURL string `json:"admin_url"`
|
AdminConf AdminServer `json:"admin_server"`
|
||||||
PhishURL string `json:"phish_url"`
|
PhishConf PhishServer `json:"phish_server"`
|
||||||
SMTP SMTPServer `json:"smtp"`
|
SMTPConf SMTPServer `json:"smtp"`
|
||||||
DBPath string `json:"dbpath"`
|
DBPath string `json:"db_path"`
|
||||||
|
MigrationsPath string `json:"migrations_path"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var Conf Config
|
var Conf Config
|
||||||
|
|
|
@ -10,14 +10,14 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/PuerkitoBio/goquery"
|
"github.com/PuerkitoBio/goquery"
|
||||||
ctx "github.com/gorilla/context"
|
|
||||||
"github.com/gorilla/mux"
|
|
||||||
"github.com/jinzhu/gorm"
|
|
||||||
"github.com/jordan-wright/email"
|
|
||||||
"github.com/gophish/gophish/auth"
|
"github.com/gophish/gophish/auth"
|
||||||
"github.com/gophish/gophish/models"
|
"github.com/gophish/gophish/models"
|
||||||
"github.com/gophish/gophish/util"
|
"github.com/gophish/gophish/util"
|
||||||
"github.com/gophish/gophish/worker"
|
"github.com/gophish/gophish/worker"
|
||||||
|
ctx "github.com/gorilla/context"
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
"github.com/jinzhu/gorm"
|
||||||
|
"github.com/jordan-wright/email"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Worker is the worker that processes phishing events and updates campaigns.
|
// Worker is the worker that processes phishing events and updates campaigns.
|
||||||
|
@ -92,6 +92,7 @@ func API_Campaigns_Id(w http.ResponseWriter, r *http.Request) {
|
||||||
id, _ := strconv.ParseInt(vars["id"], 0, 64)
|
id, _ := strconv.ParseInt(vars["id"], 0, 64)
|
||||||
c, err := models.GetCampaign(id, ctx.Get(r, "user_id").(int64))
|
c, err := models.GetCampaign(id, ctx.Get(r, "user_id").(int64))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
Logger.Println(err)
|
||||||
JSONResponse(w, models.Response{Success: false, Message: "Campaign not found"}, http.StatusNotFound)
|
JSONResponse(w, models.Response{Success: false, Message: "Campaign not found"}, http.StatusNotFound)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,9 +9,9 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/gorilla/handlers"
|
|
||||||
"github.com/gophish/gophish/config"
|
"github.com/gophish/gophish/config"
|
||||||
"github.com/gophish/gophish/models"
|
"github.com/gophish/gophish/models"
|
||||||
|
"github.com/gorilla/handlers"
|
||||||
"github.com/stretchr/testify/suite"
|
"github.com/stretchr/testify/suite"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -26,13 +26,14 @@ var as *httptest.Server = httptest.NewUnstartedServer(handlers.CombinedLoggingHa
|
||||||
|
|
||||||
func (s *ControllersSuite) SetupSuite() {
|
func (s *ControllersSuite) SetupSuite() {
|
||||||
config.Conf.DBPath = ":memory:"
|
config.Conf.DBPath = ":memory:"
|
||||||
|
config.Conf.MigrationsPath = "../db/migrations/"
|
||||||
err := models.Setup()
|
err := models.Setup()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.T().Fatalf("Failed creating database: %v", err)
|
s.T().Fatalf("Failed creating database: %v", err)
|
||||||
}
|
}
|
||||||
s.Nil(err)
|
s.Nil(err)
|
||||||
// Setup the admin server for use in testing
|
// Setup the admin server for use in testing
|
||||||
as.Config.Addr = config.Conf.AdminURL
|
as.Config.Addr = config.Conf.AdminConf.ListenURL
|
||||||
as.Start()
|
as.Start()
|
||||||
// Get the API key to use for these tests
|
// Get the API key to use for these tests
|
||||||
u, err := models.GetUser(1)
|
u, err := models.GetUser(1)
|
||||||
|
|
|
@ -0,0 +1,28 @@
|
||||||
|
|
||||||
|
-- +goose Up
|
||||||
|
-- SQL in section 'Up' is executed when this migration is applied
|
||||||
|
CREATE TABLE IF NOT EXISTS "users" ("id" integer primary key autoincrement,"username" varchar(255) NOT NULL UNIQUE,"hash" varchar(255),"api_key" varchar(255) NOT NULL UNIQUE );
|
||||||
|
CREATE TABLE IF NOT EXISTS "templates" ("id" integer primary key autoincrement,"user_id" bigint,"name" varchar(255),"subject" varchar(255),"text" varchar(255),"html" varchar(255),"modified_date" datetime );
|
||||||
|
CREATE TABLE IF NOT EXISTS "targets" ("id" integer primary key autoincrement,"first_name" varchar(255),"last_name" varchar(255),"email" varchar(255),"position" varchar(255) );
|
||||||
|
CREATE TABLE IF NOT EXISTS "smtp" ("smtp_id" integer primary key autoincrement,"campaign_id" bigint,"host" varchar(255),"username" varchar(255),"from_address" varchar(255) );
|
||||||
|
CREATE TABLE IF NOT EXISTS "results" ("id" integer primary key autoincrement,"campaign_id" bigint,"user_id" bigint,"r_id" varchar(255),"email" varchar(255),"first_name" varchar(255),"last_name" varchar(255),"status" varchar(255) NOT NULL ,"ip" varchar(255),"latitude" real,"longitude" real );
|
||||||
|
CREATE TABLE IF NOT EXISTS "pages" ("id" integer primary key autoincrement,"user_id" bigint,"name" varchar(255),"html" varchar(255),"modified_date" datetime );
|
||||||
|
CREATE TABLE IF NOT EXISTS "groups" ("id" integer primary key autoincrement,"user_id" bigint,"name" varchar(255),"modified_date" datetime );
|
||||||
|
CREATE TABLE IF NOT EXISTS "group_targets" ("group_id" bigint,"target_id" bigint );
|
||||||
|
CREATE TABLE IF NOT EXISTS "events" ("id" integer primary key autoincrement,"campaign_id" bigint,"email" varchar(255),"time" datetime,"message" varchar(255) );
|
||||||
|
CREATE TABLE IF NOT EXISTS "campaigns" ("id" integer primary key autoincrement,"user_id" bigint,"name" varchar(255) NOT NULL ,"created_date" datetime,"completed_date" datetime,"template_id" bigint,"page_id" bigint,"status" varchar(255),"url" varchar(255) );
|
||||||
|
CREATE TABLE IF NOT EXISTS "attachments" ("id" integer primary key autoincrement,"template_id" bigint,"content" varchar(255),"type" varchar(255),"name" varchar(255) );
|
||||||
|
|
||||||
|
-- +goose Down
|
||||||
|
-- SQL section 'Down' is executed when this migration is rolled back
|
||||||
|
DROP TABLE "attachments";
|
||||||
|
DROP TABLE "campaigns";
|
||||||
|
DROP TABLE "events";
|
||||||
|
DROP TABLE "group_targets";
|
||||||
|
DROP TABLE "groups";
|
||||||
|
DROP TABLE "pages";
|
||||||
|
DROP TABLE "results";
|
||||||
|
DROP TABLE "smtp";
|
||||||
|
DROP TABLE "targets";
|
||||||
|
DROP TABLE "templates";
|
||||||
|
DROP TABLE "users";
|
33
gophish.go
33
gophish.go
|
@ -30,11 +30,12 @@ import (
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
"sync"
|
||||||
|
|
||||||
"github.com/gorilla/handlers"
|
|
||||||
"github.com/gophish/gophish/config"
|
"github.com/gophish/gophish/config"
|
||||||
"github.com/gophish/gophish/controllers"
|
"github.com/gophish/gophish/controllers"
|
||||||
"github.com/gophish/gophish/models"
|
"github.com/gophish/gophish/models"
|
||||||
|
"github.com/gorilla/handlers"
|
||||||
)
|
)
|
||||||
|
|
||||||
var Logger = log.New(os.Stdout, " ", log.Ldate|log.Ltime|log.Lshortfile)
|
var Logger = log.New(os.Stdout, " ", log.Ldate|log.Ltime|log.Lshortfile)
|
||||||
|
@ -45,9 +46,31 @@ func main() {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
}
|
}
|
||||||
|
wg := &sync.WaitGroup{}
|
||||||
|
wg.Add(1)
|
||||||
// Start the web servers
|
// Start the web servers
|
||||||
Logger.Printf("Admin server started at http://%s\n", config.Conf.AdminURL)
|
go func() {
|
||||||
go http.ListenAndServe(config.Conf.AdminURL, handlers.CombinedLoggingHandler(os.Stdout, controllers.CreateAdminRouter()))
|
defer wg.Done()
|
||||||
Logger.Printf("Phishing server started at http://%s\n", config.Conf.PhishURL)
|
if config.Conf.AdminConf.UseTLS { // use TLS for Admin web server if available
|
||||||
http.ListenAndServe(config.Conf.PhishURL, handlers.CombinedLoggingHandler(os.Stdout, controllers.CreatePhishingRouter()))
|
Logger.Printf("Starting admin server at https://%s\n", config.Conf.AdminConf.ListenURL)
|
||||||
|
Logger.Fatal(http.ListenAndServeTLS(config.Conf.AdminConf.ListenURL, config.Conf.AdminConf.CertPath, config.Conf.AdminConf.KeyPath,
|
||||||
|
handlers.CombinedLoggingHandler(os.Stdout, controllers.CreateAdminRouter())))
|
||||||
|
} else {
|
||||||
|
Logger.Printf("Starting admin server at http://%s\n", config.Conf.AdminConf.ListenURL)
|
||||||
|
Logger.Fatal(http.ListenAndServe(config.Conf.AdminConf.ListenURL, handlers.CombinedLoggingHandler(os.Stdout, controllers.CreateAdminRouter())))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
if config.Conf.PhishConf.UseTLS { // use TLS for Phish web server if available
|
||||||
|
Logger.Printf("Starting phishing server at https://%s\n", config.Conf.PhishConf.ListenURL)
|
||||||
|
Logger.Fatal(http.ListenAndServeTLS(config.Conf.PhishConf.ListenURL, config.Conf.PhishConf.CertPath, config.Conf.PhishConf.KeyPath,
|
||||||
|
handlers.CombinedLoggingHandler(os.Stdout, controllers.CreatePhishingRouter())))
|
||||||
|
} else {
|
||||||
|
Logger.Printf("Starting phishing server at http://%s\n", config.Conf.PhishConf.ListenURL)
|
||||||
|
Logger.Fatal(http.ListenAndServe(config.Conf.PhishConf.ListenURL, handlers.CombinedLoggingHandler(os.Stdout, controllers.CreatePhishingRouter())))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
wg.Wait()
|
||||||
}
|
}
|
||||||
|
|
|
@ -117,21 +117,28 @@ func GetCampaign(id int64, uid int64) (Campaign, error) {
|
||||||
c := Campaign{}
|
c := Campaign{}
|
||||||
err := db.Where("id = ?", id).Where("user_id = ?", uid).Find(&c).Error
|
err := db.Where("id = ?", id).Where("user_id = ?", uid).Find(&c).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
Logger.Printf("%s: campaign not found\n", err)
|
||||||
return c, err
|
return c, err
|
||||||
}
|
}
|
||||||
err = db.Model(&c).Related(&c.Results).Error
|
err = db.Model(&c).Related(&c.Results).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
Logger.Printf("%s: results not found for campaign\n", err)
|
||||||
return c, err
|
return c, err
|
||||||
}
|
}
|
||||||
err = db.Model(&c).Related(&c.Events).Error
|
err = db.Model(&c).Related(&c.Events).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
Logger.Printf("%s: events not found for campaign\n", err)
|
||||||
return c, err
|
return c, err
|
||||||
}
|
}
|
||||||
err = db.Table("templates").Where("id=?", c.TemplateId).Find(&c.Template).Error
|
err = db.Table("templates").Where("id=?", c.TemplateId).Find(&c.Template).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
Logger.Printf("%s: template not found for campaign\n", err)
|
||||||
return c, err
|
return c, err
|
||||||
}
|
}
|
||||||
err = db.Table("pages").Where("id=?", c.PageId).Find(&c.Page).Error
|
err = db.Table("pages").Where("id=?", c.PageId).Find(&c.Page).Error
|
||||||
|
if err != nil {
|
||||||
|
Logger.Printf("%s: page not found for campaign\n", err)
|
||||||
|
}
|
||||||
return c, err
|
return c, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -207,6 +214,7 @@ func PostCampaign(c *Campaign, uid int64) error {
|
||||||
|
|
||||||
//DeleteCampaign deletes the specified campaign
|
//DeleteCampaign deletes the specified campaign
|
||||||
func DeleteCampaign(id int64) error {
|
func DeleteCampaign(id int64) error {
|
||||||
|
Logger.Printf("Deleting campaign %d\n", id)
|
||||||
// Delete all the campaign results
|
// Delete all the campaign results
|
||||||
err := db.Where("campaign_id=?", id).Delete(&Result{}).Error
|
err := db.Where("campaign_id=?", id).Delete(&Result{}).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -8,6 +8,8 @@ import (
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
|
"bitbucket.org/liamstask/goose/lib/goose"
|
||||||
|
|
||||||
"github.com/gophish/gophish/config"
|
"github.com/gophish/gophish/config"
|
||||||
"github.com/jinzhu/gorm"
|
"github.com/jinzhu/gorm"
|
||||||
_ "github.com/mattn/go-sqlite3" // Blank import needed to import sqlite3
|
_ "github.com/mattn/go-sqlite3" // Blank import needed to import sqlite3
|
||||||
|
@ -62,6 +64,24 @@ func Setup() error {
|
||||||
if _, err = os.Stat(config.Conf.DBPath); err != nil || config.Conf.DBPath == ":memory:" {
|
if _, err = os.Stat(config.Conf.DBPath); err != nil || config.Conf.DBPath == ":memory:" {
|
||||||
create_db = true
|
create_db = true
|
||||||
}
|
}
|
||||||
|
// Setup the goose configuration
|
||||||
|
migrateConf := &goose.DBConf{
|
||||||
|
MigrationsDir: config.Conf.MigrationsPath,
|
||||||
|
Env: "production",
|
||||||
|
Driver: goose.DBDriver{
|
||||||
|
Name: "sqlite3",
|
||||||
|
OpenStr: config.Conf.DBPath,
|
||||||
|
Import: "github.com/mattn/go-sqlite3",
|
||||||
|
Dialect: &goose.Sqlite3Dialect{},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
// Get the latest possible migration
|
||||||
|
latest, err := goose.GetMostRecentDBVersion(migrateConf.MigrationsDir)
|
||||||
|
if err != nil {
|
||||||
|
Logger.Println(err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// Open our database connection
|
||||||
db, err = gorm.Open("sqlite3", config.Conf.DBPath)
|
db, err = gorm.Open("sqlite3", config.Conf.DBPath)
|
||||||
db.LogMode(false)
|
db.LogMode(false)
|
||||||
db.SetLogger(Logger)
|
db.SetLogger(Logger)
|
||||||
|
@ -69,20 +89,14 @@ func Setup() error {
|
||||||
Logger.Println(err)
|
Logger.Println(err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
//If the file already exists, delete it and recreate it
|
// Migrate up to the latest version
|
||||||
|
err = goose.RunMigrationsOnDb(migrateConf, migrateConf.MigrationsDir, latest, db.DB())
|
||||||
|
if err != nil {
|
||||||
|
Logger.Println(err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
//If the database didn't exist, we need to create the admin user
|
||||||
if create_db {
|
if create_db {
|
||||||
Logger.Printf("Database not found... creating db at %s\n", config.Conf.DBPath)
|
|
||||||
db.CreateTable(User{})
|
|
||||||
db.CreateTable(Target{})
|
|
||||||
db.CreateTable(Result{})
|
|
||||||
db.CreateTable(Group{})
|
|
||||||
db.CreateTable(GroupTarget{})
|
|
||||||
db.CreateTable(Template{})
|
|
||||||
db.CreateTable(Attachment{})
|
|
||||||
db.CreateTable(Page{})
|
|
||||||
db.CreateTable(SMTP{})
|
|
||||||
db.CreateTable(Event{})
|
|
||||||
db.CreateTable(Campaign{})
|
|
||||||
//Create the default user
|
//Create the default user
|
||||||
initUser := User{
|
initUser := User{
|
||||||
Username: "admin",
|
Username: "admin",
|
||||||
|
@ -92,6 +106,7 @@ func Setup() error {
|
||||||
err = db.Save(&initUser).Error
|
err = db.Save(&initUser).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
Logger.Println(err)
|
Logger.Println(err)
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
|
|
@ -16,6 +16,7 @@ var _ = check.Suite(&ModelsSuite{})
|
||||||
|
|
||||||
func (s *ModelsSuite) SetUpSuite(c *check.C) {
|
func (s *ModelsSuite) SetUpSuite(c *check.C) {
|
||||||
config.Conf.DBPath = ":memory:"
|
config.Conf.DBPath = ":memory:"
|
||||||
|
config.Conf.MigrationsPath = "../db/migrations/"
|
||||||
err := Setup()
|
err := Setup()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Fatalf("Failed creating database: %v", err)
|
c.Fatalf("Failed creating database: %v", err)
|
||||||
|
|
|
@ -341,3 +341,33 @@
|
||||||
float:none !important;
|
float:none !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/* Table Styling */
|
||||||
|
.modal-content .dataTable tbody td {
|
||||||
|
font-size: 16px;/* Smaller font on modal tables */
|
||||||
|
}
|
||||||
|
.dataTables_info{
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sort Icons */
|
||||||
|
table.dataTable thead .sorting:after, table.dataTable thead .sorting_asc:after, table.dataTable thead .sorting_desc:after {
|
||||||
|
font-family: 'FontAwesome' !important;
|
||||||
|
position: relative !important;
|
||||||
|
display: initial !important;
|
||||||
|
top: initial!important;
|
||||||
|
right: initial!important;
|
||||||
|
left: 6px;
|
||||||
|
color: #1abc9c;
|
||||||
|
}
|
||||||
|
table.dataTable thead .sorting:after{
|
||||||
|
content: "\f0dc" !important;
|
||||||
|
color: initial;
|
||||||
|
}
|
||||||
|
table.dataTable thead .sorting_asc:after {
|
||||||
|
content: "\f0de" !important;
|
||||||
|
opacity: .8 !important;
|
||||||
|
}
|
||||||
|
table.dataTable thead .sorting_desc:after {
|
||||||
|
content: "\f0dd" !important;
|
||||||
|
opacity: .8 !important;
|
||||||
|
}
|
||||||
|
|
|
@ -2,254 +2,287 @@ var map = null
|
||||||
|
|
||||||
// statuses is a helper map to point result statuses to ui classes
|
// statuses is a helper map to point result statuses to ui classes
|
||||||
var statuses = {
|
var statuses = {
|
||||||
"Email Sent" : {
|
"Email Sent": {
|
||||||
slice: "ct-slice-donut-sent",
|
slice: "ct-slice-donut-sent",
|
||||||
legend: "ct-legend-sent",
|
legend: "ct-legend-sent",
|
||||||
label: "label-success"
|
label: "label-success"
|
||||||
},
|
},
|
||||||
"Email Opened" : {
|
"Email Opened": {
|
||||||
slice: "ct-slice-donut-opened",
|
slice: "ct-slice-donut-opened",
|
||||||
legend: "ct-legend-opened",
|
legend: "ct-legend-opened",
|
||||||
label: "label-warning"
|
label: "label-warning"
|
||||||
},
|
},
|
||||||
"Clicked Link" : {
|
"Clicked Link": {
|
||||||
slice: "ct-slice-donut-clicked",
|
slice: "ct-slice-donut-clicked",
|
||||||
legend: "ct-legend-clicked",
|
legend: "ct-legend-clicked",
|
||||||
label: "label-danger"
|
label: "label-danger"
|
||||||
},
|
},
|
||||||
"Success" : {
|
"Success": {
|
||||||
slice: "ct-slice-donut-clicked",
|
slice: "ct-slice-donut-clicked",
|
||||||
legend: "ct-legend-clicked",
|
legend: "ct-legend-clicked",
|
||||||
label: "label-danger"
|
label: "label-danger"
|
||||||
},
|
},
|
||||||
"Error" : {
|
"Error": {
|
||||||
slice: "ct-slice-donut-error",
|
slice: "ct-slice-donut-error",
|
||||||
legend: "ct-legend-error",
|
legend: "ct-legend-error",
|
||||||
label: "label-default"
|
label: "label-default"
|
||||||
},
|
},
|
||||||
"Unknown" : {
|
"Unknown": {
|
||||||
slice: "ct-slice-donut-error",
|
slice: "ct-slice-donut-error",
|
||||||
legend: "ct-legend-error",
|
legend: "ct-legend-error",
|
||||||
label: "label-default"
|
label: "label-default"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var campaign = {}
|
var campaign = {}
|
||||||
|
|
||||||
function dismiss(){
|
function dismiss() {
|
||||||
$("#modal\\.flashes").empty()
|
$("#modal\\.flashes").empty()
|
||||||
$("#modal").modal('hide')
|
$("#modal").modal('hide')
|
||||||
$("#resultsTable").dataTable().DataTable().clear().draw()
|
$("#resultsTable").dataTable().DataTable().clear().draw()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Deletes a campaign after prompting the user
|
// Deletes a campaign after prompting the user
|
||||||
function deleteCampaign(){
|
function deleteCampaign() {
|
||||||
if (confirm("Are you sure you want to delete: " + campaign.name + "?")){
|
if (confirm("Are you sure you want to delete: " + campaign.name + "?")) {
|
||||||
api.campaignId.delete(campaign.id)
|
api.campaignId.delete(campaign.id)
|
||||||
.success(function(msg){
|
.success(function(msg) {
|
||||||
console.log(msg)
|
location.href = '/campaigns'
|
||||||
})
|
})
|
||||||
.error(function(e){
|
.error(function(e) {
|
||||||
$("#modal\\.flashes").empty().append("<div style=\"text-align:center\" class=\"alert alert-danger\">\
|
$("#modal\\.flashes").empty().append("<div style=\"text-align:center\" class=\"alert alert-danger\">\
|
||||||
<i class=\"fa fa-exclamation-circle\"></i> " + data.responseJSON.message + "</div>")
|
<i class=\"fa fa-exclamation-circle\"></i> " + data.responseJSON.message + "</div>")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$(document).ready(function(){
|
// Exports campaign results as a CSV file
|
||||||
|
function exportAsCSV() {
|
||||||
|
exportHTML = $("#exportButton").html()
|
||||||
|
$("#exportButton").html('<i class="fa fa-spinner fa-spin"></i>')
|
||||||
|
var csvString = Papa.unparse(campaign.results, {})
|
||||||
|
var csvData = new Blob([csvString], {
|
||||||
|
type: 'text/csv;charset=utf-8;'
|
||||||
|
});
|
||||||
|
if (navigator.msSaveBlob) {
|
||||||
|
navigator.msSaveBlob(csvData, 'results.csv');
|
||||||
|
} else {
|
||||||
|
var csvURL = window.URL.createObjectURL(csvData);
|
||||||
|
var dlLink = document.createElement('a');
|
||||||
|
dlLink.href = csvURL;
|
||||||
|
dlLink.setAttribute('download', 'results.csv');
|
||||||
|
dlLink.click();
|
||||||
|
}
|
||||||
|
$("#exportButton").html(exportHTML)
|
||||||
|
}
|
||||||
|
|
||||||
|
$(document).ready(function() {
|
||||||
campaign.id = window.location.pathname.split('/').slice(-1)[0]
|
campaign.id = window.location.pathname.split('/').slice(-1)[0]
|
||||||
api.campaignId.get(campaign.id)
|
api.campaignId.get(campaign.id)
|
||||||
.success(function(c){
|
.success(function(c) {
|
||||||
campaign = c
|
campaign = c
|
||||||
if (campaign){
|
if (campaign) {
|
||||||
// Set the title
|
// Set the title
|
||||||
$("#page-title").text("Results for " + c.name)
|
$("#page-title").text("Results for " + c.name)
|
||||||
// Setup tooltips
|
// Setup tooltips
|
||||||
$('[data-toggle="tooltip"]').tooltip()
|
$('[data-toggle="tooltip"]').tooltip()
|
||||||
// Setup our graphs
|
// Setup our graphs
|
||||||
var timeline_data = {series:[{
|
var timeline_data = {
|
||||||
name: "Events",
|
series: [{
|
||||||
data: []
|
name: "Events",
|
||||||
}]}
|
data: []
|
||||||
var email_data = {series:[]}
|
}]
|
||||||
var email_legend = {}
|
|
||||||
var email_series_data = {}
|
|
||||||
var timeline_opts = {
|
|
||||||
axisX: {
|
|
||||||
showGrid: false,
|
|
||||||
type: Chartist.FixedScaleAxis,
|
|
||||||
divisor: 5,
|
|
||||||
labelInterpolationFnc: function(value){
|
|
||||||
return moment(value).format('MMMM Do YYYY h:mm')
|
|
||||||
}
|
|
||||||
},
|
|
||||||
axisY: {
|
|
||||||
type: Chartist.FixedScaleAxis,
|
|
||||||
ticks: [0, 1, 2],
|
|
||||||
low: 0,
|
|
||||||
showLabel: false
|
|
||||||
},
|
|
||||||
showArea: false,
|
|
||||||
plugins: []
|
|
||||||
}
|
|
||||||
var email_opts = {
|
|
||||||
donut : true,
|
|
||||||
donutWidth: 40,
|
|
||||||
chartPadding: 0,
|
|
||||||
showLabel: false
|
|
||||||
}
|
|
||||||
// Setup the results table
|
|
||||||
resultsTable = $("#resultsTable").DataTable();
|
|
||||||
$.each(campaign.results, function(i, result){
|
|
||||||
label = statuses[result.status].label || "label-default";
|
|
||||||
resultsTable.row.add([
|
|
||||||
result.first_name || "",
|
|
||||||
result.last_name || "",
|
|
||||||
result.email || "",
|
|
||||||
result.position || "",
|
|
||||||
"<span class=\"label " + label + "\">" + result.status + "</span>"
|
|
||||||
]).draw()
|
|
||||||
if (!email_series_data[result.status]){
|
|
||||||
email_series_data[result.status] = 1
|
|
||||||
} else {
|
|
||||||
email_series_data[result.status]++;
|
|
||||||
}
|
}
|
||||||
})
|
var email_data = {
|
||||||
// Setup the graphs
|
series: []
|
||||||
$.each(campaign.timeline, function(i, event){
|
|
||||||
timeline_data.series[0].data.push({meta : i, x: new Date(event.time), y:1})
|
|
||||||
})
|
|
||||||
$.each(email_series_data, function(status, count){
|
|
||||||
email_data.series.push({meta: status, value: count})
|
|
||||||
})
|
|
||||||
var timeline_chart = new Chartist.Line('#timeline_chart', timeline_data, timeline_opts)
|
|
||||||
// Setup the overview chart listeners
|
|
||||||
$chart = $("#timeline_chart")
|
|
||||||
var $toolTip = $chart
|
|
||||||
.append('<div class="chartist-tooltip"></div>')
|
|
||||||
.find('.chartist-tooltip')
|
|
||||||
.hide();
|
|
||||||
$chart.on('mouseenter', '.ct-point', function() {
|
|
||||||
var $point = $(this)
|
|
||||||
value = $point.attr('ct:value')
|
|
||||||
cidx = $point.attr('ct:meta')
|
|
||||||
html = "Event: " + campaign.timeline[cidx].message
|
|
||||||
if (campaign.timeline[cidx].email) {
|
|
||||||
html += '<br>' + "Email: " + campaign.timeline[cidx].email
|
|
||||||
}
|
}
|
||||||
$toolTip.html(html).show()
|
var email_legend = {}
|
||||||
});
|
var email_series_data = {}
|
||||||
$chart.on('mouseleave', '.ct-point', function() {
|
var timeline_opts = {
|
||||||
$toolTip.hide();
|
axisX: {
|
||||||
});
|
showGrid: false,
|
||||||
$chart.on('mousemove', function(event) {
|
type: Chartist.FixedScaleAxis,
|
||||||
$toolTip.css({
|
divisor: 5,
|
||||||
left: (event.offsetX || event.originalEvent.layerX) - $toolTip.width() / 2 - 10,
|
labelInterpolationFnc: function(value) {
|
||||||
top: (event.offsetY + 70 || event.originalEvent.layerY) - $toolTip.height() - 40
|
return moment(value).format('MMMM Do YYYY h:mm')
|
||||||
});
|
|
||||||
});
|
|
||||||
var email_chart = new Chartist.Pie("#email_chart", email_data, email_opts)
|
|
||||||
email_chart.on('draw', function(data){
|
|
||||||
// We don't want to create the legend twice
|
|
||||||
if (!email_legend[data.meta]) {
|
|
||||||
console.log(data.meta)
|
|
||||||
$("#email_chart_legend").append('<li><span class="' + statuses[data.meta].legend + '"></span>' + data.meta + '</li>')
|
|
||||||
email_legend[data.meta] = true
|
|
||||||
}
|
|
||||||
data.element.addClass(statuses[data.meta].slice)
|
|
||||||
})
|
|
||||||
// Setup the average chart listeners
|
|
||||||
$piechart = $("#email_chart")
|
|
||||||
var $pietoolTip = $piechart
|
|
||||||
.append('<div class="chartist-tooltip"></div>')
|
|
||||||
.find('.chartist-tooltip')
|
|
||||||
.hide();
|
|
||||||
|
|
||||||
$piechart.on('mouseenter', '.ct-slice-donut', function() {
|
|
||||||
var $point = $(this)
|
|
||||||
value = $point.attr('ct:value')
|
|
||||||
label = $point.attr('ct:meta')
|
|
||||||
$pietoolTip.html(label + ': ' + value.toString()).show();
|
|
||||||
});
|
|
||||||
|
|
||||||
$piechart.on('mouseleave', '.ct-slice-donut', function() {
|
|
||||||
$pietoolTip.hide();
|
|
||||||
});
|
|
||||||
$piechart.on('mousemove', function(event) {
|
|
||||||
$pietoolTip.css({
|
|
||||||
left: (event.offsetX || event.originalEvent.layerX) - $pietoolTip.width() / 2 - 10,
|
|
||||||
top: (event.offsetY + 40 || event.originalEvent.layerY) - $pietoolTip.height() - 80
|
|
||||||
});
|
|
||||||
});
|
|
||||||
$("#loading").hide()
|
|
||||||
$("#campaignResults").show()
|
|
||||||
map = new Datamap({
|
|
||||||
element: document.getElementById("resultsMap"),
|
|
||||||
responsive: true,
|
|
||||||
fills: {
|
|
||||||
defaultFill: "#ffffff",
|
|
||||||
point: "#283F50"
|
|
||||||
},
|
|
||||||
geographyConfig: {
|
|
||||||
highlightFillColor : "#1abc9c",
|
|
||||||
borderColor:"#283F50"
|
|
||||||
},
|
|
||||||
bubblesConfig: {
|
|
||||||
borderColor: "#283F50"
|
|
||||||
}
|
|
||||||
});
|
|
||||||
bubbles = []
|
|
||||||
$.each(campaign.results, function(i, result){
|
|
||||||
// Check that it wasn't an internal IP
|
|
||||||
if (result.latitude == 0 && result.longitude == 0) { return true; }
|
|
||||||
newIP = true
|
|
||||||
$.each(bubbles, function(i, bubble){
|
|
||||||
if (bubble.ip == result.ip){
|
|
||||||
bubbles[i].radius += 1
|
|
||||||
newIP = false
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
})
|
|
||||||
if (newIP){
|
|
||||||
console.log("Adding bubble at: ")
|
|
||||||
console.log({
|
|
||||||
latitude : result.latitude,
|
|
||||||
longitude: result.longitude,
|
|
||||||
name : result.ip,
|
|
||||||
fillKey: "point"
|
|
||||||
})
|
|
||||||
bubbles.push({
|
|
||||||
latitude : result.latitude,
|
|
||||||
longitude: result.longitude,
|
|
||||||
name : result.ip,
|
|
||||||
fillKey: "point",
|
|
||||||
radius: 2
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
map.bubbles(bubbles)
|
|
||||||
}
|
|
||||||
// Load up the map data (only once!)
|
|
||||||
$('a[data-toggle="tab"]').on('shown.bs.tab', function (e) {
|
|
||||||
if ($(e.target).attr('href') == "#overview"){
|
|
||||||
if (!map){
|
|
||||||
map = new Datamap({
|
|
||||||
element: document.getElementById("resultsMap"),
|
|
||||||
responsive: true,
|
|
||||||
fills: {
|
|
||||||
defaultFill: "#ffffff"
|
|
||||||
},
|
|
||||||
geographyConfig: {
|
|
||||||
highlightFillColor : "#1abc9c",
|
|
||||||
borderColor:"#283F50"
|
|
||||||
}
|
}
|
||||||
});
|
},
|
||||||
|
axisY: {
|
||||||
|
type: Chartist.FixedScaleAxis,
|
||||||
|
ticks: [0, 1, 2],
|
||||||
|
low: 0,
|
||||||
|
showLabel: false
|
||||||
|
},
|
||||||
|
showArea: false,
|
||||||
|
plugins: []
|
||||||
}
|
}
|
||||||
|
var email_opts = {
|
||||||
|
donut: true,
|
||||||
|
donutWidth: 40,
|
||||||
|
chartPadding: 0,
|
||||||
|
showLabel: false
|
||||||
|
}
|
||||||
|
// Setup the results table
|
||||||
|
resultsTable = $("#resultsTable").DataTable();
|
||||||
|
$.each(campaign.results, function(i, result) {
|
||||||
|
label = statuses[result.status].label || "label-default";
|
||||||
|
resultsTable.row.add([
|
||||||
|
result.first_name || "",
|
||||||
|
result.last_name || "",
|
||||||
|
result.email || "",
|
||||||
|
result.position || "",
|
||||||
|
"<span class=\"label " + label + "\">" + result.status + "</span>"
|
||||||
|
]).draw()
|
||||||
|
if (!email_series_data[result.status]) {
|
||||||
|
email_series_data[result.status] = 1
|
||||||
|
} else {
|
||||||
|
email_series_data[result.status]++;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// Setup the graphs
|
||||||
|
$.each(campaign.timeline, function(i, event) {
|
||||||
|
timeline_data.series[0].data.push({
|
||||||
|
meta: i,
|
||||||
|
x: new Date(event.time),
|
||||||
|
y: 1
|
||||||
|
})
|
||||||
|
})
|
||||||
|
$.each(email_series_data, function(status, count) {
|
||||||
|
email_data.series.push({
|
||||||
|
meta: status,
|
||||||
|
value: count
|
||||||
|
})
|
||||||
|
})
|
||||||
|
var timeline_chart = new Chartist.Line('#timeline_chart', timeline_data, timeline_opts)
|
||||||
|
// Setup the overview chart listeners
|
||||||
|
$chart = $("#timeline_chart")
|
||||||
|
var $toolTip = $chart
|
||||||
|
.append('<div class="chartist-tooltip"></div>')
|
||||||
|
.find('.chartist-tooltip')
|
||||||
|
.hide();
|
||||||
|
$chart.on('mouseenter', '.ct-point', function() {
|
||||||
|
var $point = $(this)
|
||||||
|
value = $point.attr('ct:value')
|
||||||
|
cidx = $point.attr('ct:meta')
|
||||||
|
html = "Event: " + campaign.timeline[cidx].message
|
||||||
|
if (campaign.timeline[cidx].email) {
|
||||||
|
html += '<br>' + "Email: " + campaign.timeline[cidx].email
|
||||||
|
}
|
||||||
|
$toolTip.html(html).show()
|
||||||
|
});
|
||||||
|
$chart.on('mouseleave', '.ct-point', function() {
|
||||||
|
$toolTip.hide();
|
||||||
|
});
|
||||||
|
$chart.on('mousemove', function(event) {
|
||||||
|
$toolTip.css({
|
||||||
|
left: (event.offsetX || event.originalEvent.layerX) - $toolTip.width() / 2 - 10,
|
||||||
|
top: (event.offsetY + 70 || event.originalEvent.layerY) - $toolTip.height() - 40
|
||||||
|
});
|
||||||
|
});
|
||||||
|
var email_chart = new Chartist.Pie("#email_chart", email_data, email_opts)
|
||||||
|
email_chart.on('draw', function(data) {
|
||||||
|
// We don't want to create the legend twice
|
||||||
|
if (!email_legend[data.meta]) {
|
||||||
|
console.log(data.meta)
|
||||||
|
$("#email_chart_legend").append('<li><span class="' + statuses[data.meta].legend + '"></span>' + data.meta + '</li>')
|
||||||
|
email_legend[data.meta] = true
|
||||||
|
}
|
||||||
|
data.element.addClass(statuses[data.meta].slice)
|
||||||
|
})
|
||||||
|
// Setup the average chart listeners
|
||||||
|
$piechart = $("#email_chart")
|
||||||
|
var $pietoolTip = $piechart
|
||||||
|
.append('<div class="chartist-tooltip"></div>')
|
||||||
|
.find('.chartist-tooltip')
|
||||||
|
.hide();
|
||||||
|
|
||||||
|
$piechart.on('mouseenter', '.ct-slice-donut', function() {
|
||||||
|
var $point = $(this)
|
||||||
|
value = $point.attr('ct:value')
|
||||||
|
label = $point.attr('ct:meta')
|
||||||
|
$pietoolTip.html(label + ': ' + value.toString()).show();
|
||||||
|
});
|
||||||
|
|
||||||
|
$piechart.on('mouseleave', '.ct-slice-donut', function() {
|
||||||
|
$pietoolTip.hide();
|
||||||
|
});
|
||||||
|
$piechart.on('mousemove', function(event) {
|
||||||
|
$pietoolTip.css({
|
||||||
|
left: (event.offsetX || event.originalEvent.layerX) - $pietoolTip.width() / 2 - 10,
|
||||||
|
top: (event.offsetY + 40 || event.originalEvent.layerY) - $pietoolTip.height() - 80
|
||||||
|
});
|
||||||
|
});
|
||||||
|
$("#loading").hide()
|
||||||
|
$("#campaignResults").show()
|
||||||
|
map = new Datamap({
|
||||||
|
element: document.getElementById("resultsMap"),
|
||||||
|
responsive: true,
|
||||||
|
fills: {
|
||||||
|
defaultFill: "#ffffff",
|
||||||
|
point: "#283F50"
|
||||||
|
},
|
||||||
|
geographyConfig: {
|
||||||
|
highlightFillColor: "#1abc9c",
|
||||||
|
borderColor: "#283F50"
|
||||||
|
},
|
||||||
|
bubblesConfig: {
|
||||||
|
borderColor: "#283F50"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
bubbles = []
|
||||||
|
$.each(campaign.results, function(i, result) {
|
||||||
|
// Check that it wasn't an internal IP
|
||||||
|
if (result.latitude == 0 && result.longitude == 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
newIP = true
|
||||||
|
$.each(bubbles, function(i, bubble) {
|
||||||
|
if (bubble.ip == result.ip) {
|
||||||
|
bubbles[i].radius += 1
|
||||||
|
newIP = false
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (newIP) {
|
||||||
|
console.log("Adding bubble at: ")
|
||||||
|
console.log({
|
||||||
|
latitude: result.latitude,
|
||||||
|
longitude: result.longitude,
|
||||||
|
name: result.ip,
|
||||||
|
fillKey: "point"
|
||||||
|
})
|
||||||
|
bubbles.push({
|
||||||
|
latitude: result.latitude,
|
||||||
|
longitude: result.longitude,
|
||||||
|
name: result.ip,
|
||||||
|
fillKey: "point",
|
||||||
|
radius: 2
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
map.bubbles(bubbles)
|
||||||
}
|
}
|
||||||
|
// Load up the map data (only once!)
|
||||||
|
$('a[data-toggle="tab"]').on('shown.bs.tab', function(e) {
|
||||||
|
if ($(e.target).attr('href') == "#overview") {
|
||||||
|
if (!map) {
|
||||||
|
map = new Datamap({
|
||||||
|
element: document.getElementById("resultsMap"),
|
||||||
|
responsive: true,
|
||||||
|
fills: {
|
||||||
|
defaultFill: "#ffffff"
|
||||||
|
},
|
||||||
|
geographyConfig: {
|
||||||
|
highlightFillColor: "#1abc9c",
|
||||||
|
borderColor: "#283F50"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.error(function() {
|
||||||
|
$("#loading").hide()
|
||||||
|
errorFlash(" Campaign not found!")
|
||||||
})
|
})
|
||||||
})
|
|
||||||
.error(function(){
|
|
||||||
$("#loading").hide()
|
|
||||||
errorFlash(" Campaign not found!")
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,212 +1,253 @@
|
||||||
// labels is a map of campaign statuses to
|
// labels is a map of campaign statuses to
|
||||||
// CSS classes
|
// CSS classes
|
||||||
var labels = {
|
var labels = {
|
||||||
"In progress" : "label-primary",
|
"In progress": "label-primary",
|
||||||
"Queued" : "label-info",
|
"Queued": "label-info",
|
||||||
"Completed" : "label-success",
|
"Completed": "label-success",
|
||||||
"Emails Sent" : "label-success",
|
"Emails Sent": "label-success",
|
||||||
"Error" : "label-danger"
|
"Error": "label-danger"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var campaigns = []
|
||||||
|
|
||||||
// Save attempts to POST to /campaigns/
|
// Save attempts to POST to /campaigns/
|
||||||
function save(){
|
function launch() {
|
||||||
groups = []
|
if (!confirm("This will launch the campaign. Are you sure?")) {
|
||||||
$.each($("#groupTable").DataTable().rows().data(), function(i, group){
|
return false;
|
||||||
groups.push({name: group[0]})
|
|
||||||
})
|
|
||||||
console.log(groups)
|
|
||||||
var campaign = {
|
|
||||||
name: $("#name").val(),
|
|
||||||
template:{
|
|
||||||
name: $("#template").val()
|
|
||||||
},
|
|
||||||
url: $("#url").val(),
|
|
||||||
page: {
|
|
||||||
name: $("#page").val()
|
|
||||||
},
|
|
||||||
smtp: {
|
|
||||||
from_address: $("input[name=from]").val(),
|
|
||||||
host: $("input[name=host]").val(),
|
|
||||||
username: $("input[name=username]").val(),
|
|
||||||
password: $("input[name=password]").val(),
|
|
||||||
},
|
|
||||||
groups: groups
|
|
||||||
}
|
}
|
||||||
// Submit the campaign
|
groups = []
|
||||||
|
$.each($("#groupTable").DataTable().rows().data(), function(i, group) {
|
||||||
|
groups.push({
|
||||||
|
name: group[0]
|
||||||
|
})
|
||||||
|
})
|
||||||
|
var campaign = {
|
||||||
|
name: $("#name").val(),
|
||||||
|
template: {
|
||||||
|
name: $("#template").val()
|
||||||
|
},
|
||||||
|
url: $("#url").val(),
|
||||||
|
page: {
|
||||||
|
name: $("#page").val()
|
||||||
|
},
|
||||||
|
smtp: {
|
||||||
|
from_address: $("input[name=from]").val(),
|
||||||
|
host: $("input[name=host]").val(),
|
||||||
|
username: $("input[name=username]").val(),
|
||||||
|
password: $("input[name=password]").val(),
|
||||||
|
},
|
||||||
|
groups: groups
|
||||||
|
}
|
||||||
|
// Submit the campaign
|
||||||
api.campaigns.post(campaign)
|
api.campaigns.post(campaign)
|
||||||
.success(function(data){
|
.success(function(data) {
|
||||||
successFlash("Campaign successfully launched!")
|
successFlash("Campaign successfully launched!")
|
||||||
window.location = "/campaigns/" + campaign.id.toString()
|
window.location = "/campaigns/" + data.id.toString()
|
||||||
})
|
})
|
||||||
.error(function(data){
|
.error(function(data) {
|
||||||
$("#modal\\.flashes").empty().append("<div style=\"text-align:center\" class=\"alert alert-danger\">\
|
$("#modal\\.flashes").empty().append("<div style=\"text-align:center\" class=\"alert alert-danger\">\
|
||||||
<i class=\"fa fa-exclamation-circle\"></i> " + data.responseJSON.message + "</div>")
|
<i class=\"fa fa-exclamation-circle\"></i> " + data.responseJSON.message + "</div>")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function dismiss(){
|
function dismiss() {
|
||||||
$("#modal\\.flashes").empty()
|
$("#modal\\.flashes").empty()
|
||||||
$("#modal").modal('hide')
|
$("#modal").modal('hide')
|
||||||
$("#groupTable").dataTable().DataTable().clear().draw()
|
$("#groupTable").dataTable().DataTable().clear().draw()
|
||||||
}
|
}
|
||||||
|
|
||||||
function edit(campaign){
|
function deleteCampaign(idx) {
|
||||||
|
if (confirm("Delete " + campaigns[idx].name + "?")) {
|
||||||
|
api.campaignId.delete(campaigns[idx].id)
|
||||||
|
.success(function(data) {
|
||||||
|
successFlash(data.message)
|
||||||
|
location.reload()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function edit(campaign) {
|
||||||
// Clear the bloodhound instance
|
// Clear the bloodhound instance
|
||||||
group_bh.clear();
|
group_bh.clear();
|
||||||
template_bh.clear();
|
template_bh.clear();
|
||||||
page_bh.clear();
|
page_bh.clear();
|
||||||
if (campaign == "new") {
|
if (campaign == "new") {
|
||||||
api.groups.get()
|
api.groups.get()
|
||||||
.success(function(groups){
|
.success(function(groups) {
|
||||||
if (groups.length == 0){
|
if (groups.length == 0) {
|
||||||
modalError("No groups found!")
|
modalError("No groups found!")
|
||||||
return false;
|
return false;
|
||||||
}
|
} else {
|
||||||
else {
|
group_bh.add(groups)
|
||||||
group_bh.add(groups)
|
}
|
||||||
}
|
})
|
||||||
})
|
api.templates.get()
|
||||||
api.templates.get()
|
.success(function(templates) {
|
||||||
.success(function(templates){
|
if (templates.length == 0) {
|
||||||
if (templates.length == 0){
|
modalError("No templates found!")
|
||||||
modalError("No templates found!")
|
return false
|
||||||
return false
|
} else {
|
||||||
}
|
template_bh.add(templates)
|
||||||
else {
|
}
|
||||||
template_bh.add(templates)
|
})
|
||||||
}
|
api.pages.get()
|
||||||
})
|
.success(function(pages) {
|
||||||
api.pages.get()
|
if (pages.length == 0) {
|
||||||
.success(function(pages){
|
modalError("No pages found!")
|
||||||
if (pages.length == 0){
|
return false
|
||||||
modalError("No pages found!")
|
} else {
|
||||||
return false
|
page_bh.add(pages)
|
||||||
}
|
}
|
||||||
else {
|
})
|
||||||
page_bh.add(pages)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$(document).ready(function(){
|
$(document).ready(function() {
|
||||||
api.campaigns.get()
|
api.campaigns.get()
|
||||||
.success(function(campaigns){
|
.success(function(cs) {
|
||||||
$("#loading").hide()
|
campaigns = cs
|
||||||
if (campaigns.length > 0){
|
campaignTable = $("#campaignTable").DataTable({
|
||||||
$("#campaignTable").show()
|
columnDefs: [{
|
||||||
campaignTable = $("#campaignTable").DataTable();
|
orderable: false,
|
||||||
$.each(campaigns, function(i, campaign){
|
targets: "no-sort"
|
||||||
label = labels[campaign.status] || "label-default";
|
}]
|
||||||
campaignTable.row.add([
|
});
|
||||||
campaign.name,
|
$("#loading").hide()
|
||||||
moment(campaign.created_date).format('MMMM Do YYYY, h:mm:ss a'),
|
if (campaigns.length > 0) {
|
||||||
"<span class=\"label " + label + "\">" + campaign.status + "</span>",
|
$("#campaignTable").show()
|
||||||
"<div class='pull-right'><a class='btn btn-primary' href='/campaigns/" + campaign.id + "'>\
|
campaignTable = $("#campaignTable").DataTable();
|
||||||
|
$.each(campaigns, function(i, campaign) {
|
||||||
|
label = labels[campaign.status] || "label-default";
|
||||||
|
campaignTable.row.add([
|
||||||
|
campaign.name,
|
||||||
|
moment(campaign.created_date).format('MMMM Do YYYY, h:mm:ss a'),
|
||||||
|
"<span class=\"label " + label + "\">" + campaign.status + "</span>",
|
||||||
|
"<div class='pull-right'><a class='btn btn-primary' href='/campaigns/" + campaign.id + "' data-toggle='tooltip' data-placement='left' title='View Results'>\
|
||||||
<i class='fa fa-bar-chart'></i>\
|
<i class='fa fa-bar-chart'></i>\
|
||||||
</a>\
|
</a>\
|
||||||
<button class='btn btn-danger' onclick='alert(\"test\")'>\
|
<button class='btn btn-danger' onclick='deleteCampaign(" + i + ")' data-toggle='tooltip' data-placement='left' title='Delete Campaign'>\
|
||||||
<i class='fa fa-trash-o'></i>\
|
<i class='fa fa-trash-o'></i>\
|
||||||
</button></div>"
|
</button></div>"
|
||||||
]).draw()
|
]).draw()
|
||||||
})
|
$('[data-toggle="tooltip"]').tooltip()
|
||||||
} else {
|
})
|
||||||
$("#emptyMessage").show()
|
} else {
|
||||||
}
|
$("#emptyMessage").show()
|
||||||
})
|
}
|
||||||
.error(function(){
|
|
||||||
$("#loading").hide()
|
|
||||||
errorFlash("Error fetching campaigns")
|
|
||||||
})
|
|
||||||
$("#groupForm").submit(function(){
|
|
||||||
groupTable.row.add([
|
|
||||||
$("#groupSelect").val(),
|
|
||||||
'<span style="cursor:pointer;"><i class="fa fa-trash-o"></i></span>'
|
|
||||||
]).draw()
|
|
||||||
$("#groupTable").on("click", "span>i.fa-trash-o", function(){
|
|
||||||
groupTable.row( $(this).parents('tr') )
|
|
||||||
.remove()
|
|
||||||
.draw();
|
|
||||||
})
|
})
|
||||||
return false;
|
.error(function() {
|
||||||
|
$("#loading").hide()
|
||||||
|
errorFlash("Error fetching campaigns")
|
||||||
|
})
|
||||||
|
$("#groupForm").submit(function() {
|
||||||
|
groupTable.row.add([
|
||||||
|
$("#groupSelect").val(),
|
||||||
|
'<span style="cursor:pointer;"><i class="fa fa-trash-o"></i></span>'
|
||||||
|
]).draw()
|
||||||
|
$("#groupTable").on("click", "span>i.fa-trash-o", function() {
|
||||||
|
groupTable.row($(this).parents('tr'))
|
||||||
|
.remove()
|
||||||
|
.draw();
|
||||||
|
})
|
||||||
|
return false;
|
||||||
|
})
|
||||||
|
// Create the group typeahead objects
|
||||||
|
groupTable = $("#groupTable").DataTable({
|
||||||
|
columnDefs: [{
|
||||||
|
orderable: false,
|
||||||
|
targets: "no-sort"
|
||||||
|
}]
|
||||||
})
|
})
|
||||||
// Create the group typeahead objects
|
|
||||||
groupTable = $("#groupTable").DataTable()
|
|
||||||
group_bh = new Bloodhound({
|
group_bh = new Bloodhound({
|
||||||
datumTokenizer: function(g) { return Bloodhound.tokenizers.whitespace(g.name) },
|
datumTokenizer: function(g) {
|
||||||
|
return Bloodhound.tokenizers.whitespace(g.name)
|
||||||
|
},
|
||||||
queryTokenizer: Bloodhound.tokenizers.whitespace,
|
queryTokenizer: Bloodhound.tokenizers.whitespace,
|
||||||
local: []
|
local: []
|
||||||
})
|
})
|
||||||
group_bh.initialize()
|
group_bh.initialize()
|
||||||
$("#groupSelect.typeahead.form-control").typeahead({
|
$("#groupSelect.typeahead.form-control").typeahead({
|
||||||
hint: true,
|
hint: true,
|
||||||
highlight: true,
|
highlight: true,
|
||||||
minLength: 1
|
minLength: 1
|
||||||
},
|
}, {
|
||||||
{
|
name: "groups",
|
||||||
name: "groups",
|
source: group_bh,
|
||||||
source: group_bh,
|
templates: {
|
||||||
templates: {
|
empty: function(data) {
|
||||||
empty: function(data) {return '<div class="tt-suggestion">No groups matched that query</div>' },
|
return '<div class="tt-suggestion">No groups matched that query</div>'
|
||||||
suggestion: function(data){ return '<div>' + data.name + '</div>' }
|
},
|
||||||
}
|
suggestion: function(data) {
|
||||||
})
|
return '<div>' + data.name + '</div>'
|
||||||
.bind('typeahead:select', function(ev, group){
|
}
|
||||||
$("#groupSelect").typeahead('val', group.name)
|
}
|
||||||
})
|
})
|
||||||
.bind('typeahead:autocomplete', function(ev, group){
|
.bind('typeahead:select', function(ev, group) {
|
||||||
$("#groupSelect").typeahead('val', group.name)
|
$("#groupSelect").typeahead('val', group.name)
|
||||||
});
|
})
|
||||||
|
.bind('typeahead:autocomplete', function(ev, group) {
|
||||||
|
$("#groupSelect").typeahead('val', group.name)
|
||||||
|
});
|
||||||
// Create the template typeahead objects
|
// Create the template typeahead objects
|
||||||
template_bh = new Bloodhound({
|
template_bh = new Bloodhound({
|
||||||
datumTokenizer: function(t) { return Bloodhound.tokenizers.whitespace(t.name) },
|
datumTokenizer: function(t) {
|
||||||
|
return Bloodhound.tokenizers.whitespace(t.name)
|
||||||
|
},
|
||||||
queryTokenizer: Bloodhound.tokenizers.whitespace,
|
queryTokenizer: Bloodhound.tokenizers.whitespace,
|
||||||
local: []
|
local: []
|
||||||
})
|
})
|
||||||
template_bh.initialize()
|
template_bh.initialize()
|
||||||
$("#template.typeahead.form-control").typeahead({
|
$("#template.typeahead.form-control").typeahead({
|
||||||
hint: true,
|
hint: true,
|
||||||
highlight: true,
|
highlight: true,
|
||||||
minLength: 1
|
minLength: 1
|
||||||
},
|
}, {
|
||||||
{
|
name: "templates",
|
||||||
name: "templates",
|
source: template_bh,
|
||||||
source: template_bh,
|
templates: {
|
||||||
templates: {
|
empty: function(data) {
|
||||||
empty: function(data) {return '<div class="tt-suggestion">No templates matched that query</div>' },
|
return '<div class="tt-suggestion">No templates matched that query</div>'
|
||||||
suggestion: function(data){ return '<div>' + data.name + '</div>' }
|
},
|
||||||
}
|
suggestion: function(data) {
|
||||||
})
|
return '<div>' + data.name + '</div>'
|
||||||
.bind('typeahead:select', function(ev, template){
|
}
|
||||||
$("#template").typeahead('val', template.name)
|
}
|
||||||
})
|
})
|
||||||
.bind('typeahead:autocomplete', function(ev, template){
|
.bind('typeahead:select', function(ev, template) {
|
||||||
$("#template").typeahead('val', template.name)
|
$("#template").typeahead('val', template.name)
|
||||||
});
|
})
|
||||||
|
.bind('typeahead:autocomplete', function(ev, template) {
|
||||||
|
$("#template").typeahead('val', template.name)
|
||||||
|
});
|
||||||
// Create the landing page typeahead objects
|
// Create the landing page typeahead objects
|
||||||
page_bh = new Bloodhound({
|
page_bh = new Bloodhound({
|
||||||
datumTokenizer: function(p) { return Bloodhound.tokenizers.whitespace(p.name) },
|
datumTokenizer: function(p) {
|
||||||
|
return Bloodhound.tokenizers.whitespace(p.name)
|
||||||
|
},
|
||||||
queryTokenizer: Bloodhound.tokenizers.whitespace,
|
queryTokenizer: Bloodhound.tokenizers.whitespace,
|
||||||
local: []
|
local: []
|
||||||
})
|
})
|
||||||
page_bh.initialize()
|
page_bh.initialize()
|
||||||
$("#page.typeahead.form-control").typeahead({
|
$("#page.typeahead.form-control").typeahead({
|
||||||
hint: true,
|
hint: true,
|
||||||
highlight: true,
|
highlight: true,
|
||||||
minLength: 1
|
minLength: 1
|
||||||
},
|
}, {
|
||||||
{
|
name: "pages",
|
||||||
name: "pages",
|
source: page_bh,
|
||||||
source: page_bh,
|
templates: {
|
||||||
templates: {
|
empty: function(data) {
|
||||||
empty: function(data) {return '<div class="tt-suggestion">No pages matched that query</div>' },
|
return '<div class="tt-suggestion">No pages matched that query</div>'
|
||||||
suggestion: function(data){ return '<div>' + data.name + '</div>' }
|
},
|
||||||
}
|
suggestion: function(data) {
|
||||||
})
|
return '<div>' + data.name + '</div>'
|
||||||
.bind('typeahead:select', function(ev, page){
|
}
|
||||||
$("#page").typeahead('val', page.name)
|
}
|
||||||
})
|
})
|
||||||
.bind('typeahead:autocomplete', function(ev, page){
|
.bind('typeahead:select', function(ev, page) {
|
||||||
$("#page").typeahead('val', page.name)
|
$("#page").typeahead('val', page.name)
|
||||||
});
|
})
|
||||||
|
.bind('typeahead:autocomplete', function(ev, page) {
|
||||||
|
$("#page").typeahead('val', page.name)
|
||||||
|
});
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,129 +1,159 @@
|
||||||
var campaigns = []
|
var campaigns = []
|
||||||
// labels is a map of campaign statuses to
|
// labels is a map of campaign statuses to
|
||||||
// CSS classes
|
// CSS classes
|
||||||
var labels = {
|
var labels = {
|
||||||
"In progress" : "label-primary",
|
"In progress": "label-primary",
|
||||||
"Queued" : "label-info",
|
"Queued": "label-info",
|
||||||
"Completed" : "label-success",
|
"Completed": "label-success",
|
||||||
"Emails Sent" : "label-success",
|
"Emails Sent": "label-success",
|
||||||
"Error" : "label-danger"
|
"Error": "label-danger"
|
||||||
}
|
}
|
||||||
|
|
||||||
$(document).ready(function(){
|
function deleteCampaign(idx) {
|
||||||
|
if (confirm("Delete " + campaigns[idx].name + "?")) {
|
||||||
|
api.campaignId.delete(campaigns[idx].id)
|
||||||
|
.success(function(data) {
|
||||||
|
successFlash(data.message)
|
||||||
|
location.reload()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$(document).ready(function() {
|
||||||
api.campaigns.get()
|
api.campaigns.get()
|
||||||
.success(function(cs){
|
.success(function(cs) {
|
||||||
$("#loading").hide()
|
$("#loading").hide()
|
||||||
campaigns = cs
|
campaigns = cs
|
||||||
if (campaigns.length > 0){
|
if (campaigns.length > 0) {
|
||||||
$("#dashboard").show()
|
$("#dashboard").show()
|
||||||
// Create the overview chart data
|
// Create the overview chart data
|
||||||
var overview_data = {labels:[],series:[[]]}
|
var overview_data = {
|
||||||
var average_data = {series:[]}
|
labels: [],
|
||||||
var overview_opts = {
|
series: [
|
||||||
axisX: {
|
[]
|
||||||
showGrid: false
|
]
|
||||||
},
|
}
|
||||||
showArea: true,
|
var average_data = {
|
||||||
plugins: []
|
series: []
|
||||||
}
|
}
|
||||||
var average_opts = {
|
var overview_opts = {
|
||||||
donut : true,
|
axisX: {
|
||||||
donutWidth: 40,
|
showGrid: false
|
||||||
chartPadding: 0,
|
},
|
||||||
showLabel: false
|
showArea: true,
|
||||||
}
|
plugins: []
|
||||||
var average = 0
|
}
|
||||||
campaignTable = $("#campaignTable").DataTable();
|
var average_opts = {
|
||||||
$.each(campaigns, function(i, campaign){
|
donut: true,
|
||||||
var campaign_date = moment(campaign.created_date).format('MMMM Do YYYY h:mm:ss a')
|
donutWidth: 40,
|
||||||
var label = labels[campaign.status] || "label-default";
|
chartPadding: 0,
|
||||||
// Add it to the table
|
showLabel: false
|
||||||
campaignTable.row.add([
|
}
|
||||||
campaign.name,
|
var average = 0
|
||||||
campaign_date,
|
campaignTable = $("#campaignTable").DataTable({
|
||||||
"<span class=\"label " + label + "\">" + campaign.status + "</span>",
|
columnDefs: [
|
||||||
"<div class='pull-right'><a class='btn btn-primary' href='/campaigns/" + campaign.id + "'>\
|
{ orderable: false, targets: "no-sort" }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
$.each(campaigns, function(i, campaign) {
|
||||||
|
var campaign_date = moment(campaign.created_date).format('MMMM Do YYYY h:mm:ss a')
|
||||||
|
var label = labels[campaign.status] || "label-default";
|
||||||
|
// Add it to the table
|
||||||
|
campaignTable.row.add([
|
||||||
|
campaign.name,
|
||||||
|
campaign_date,
|
||||||
|
"<span class=\"label " + label + "\">" + campaign.status + "</span>",
|
||||||
|
"<div class='pull-right'><a class='btn btn-primary' href='/campaigns/" + campaign.id + "' data-toggle='tooltip' data-placement='right' title='View Results'>\
|
||||||
<i class='fa fa-bar-chart'></i>\
|
<i class='fa fa-bar-chart'></i>\
|
||||||
</a>\
|
</a>\
|
||||||
<button class='btn btn-danger' onclick='deleteCampaign(" + i + ")'>\
|
<button class='btn btn-danger' onclick='deleteCampaign(" + i + ")' data-toggle='tooltip' data-placement='right' title='Delete Campaign'>\
|
||||||
<i class='fa fa-trash-o'></i>\
|
<i class='fa fa-trash-o'></i>\
|
||||||
</button></div>"
|
</button></div>"
|
||||||
]).draw()
|
]).draw()
|
||||||
// Add it to the chart data
|
// Add it to the chart data
|
||||||
campaign.y = 0
|
campaign.y = 0
|
||||||
$.each(campaign.results, function(j, result){
|
$.each(campaign.results, function(j, result) {
|
||||||
if (result.status == "Success"){
|
if (result.status == "Success") {
|
||||||
campaign.y++;
|
campaign.y++;
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
campaign.y = Math.floor((campaign.y / campaign.results.length) * 100)
|
||||||
|
average += campaign.y
|
||||||
|
// Add the data to the overview chart
|
||||||
|
overview_data.labels.push(campaign_date)
|
||||||
|
overview_data.series[0].push({
|
||||||
|
meta: i,
|
||||||
|
value: campaign.y
|
||||||
|
})
|
||||||
})
|
})
|
||||||
campaign.y = Math.floor((campaign.y / campaign.results.length) * 100)
|
average = Math.floor(average / campaigns.length);
|
||||||
average += campaign.y
|
average_data.series.push({
|
||||||
// Add the data to the overview chart
|
meta: "Unsuccessful Phishes",
|
||||||
overview_data.labels.push(campaign_date)
|
value: 100 - average
|
||||||
overview_data.series[0].push({meta : i, value: campaign.y})
|
})
|
||||||
})
|
average_data.series.push({
|
||||||
average = Math.floor(average / campaigns.length);
|
meta: "Successful Phishes",
|
||||||
average_data.series.push({meta: "Unsuccessful Phishes", value: 100 - average})
|
value: average
|
||||||
average_data.series.push({meta: "Successful Phishes", value: average})
|
})
|
||||||
// Build the charts
|
// Build the charts
|
||||||
var average_chart = new Chartist.Pie("#average_chart", average_data, average_opts)
|
var average_chart = new Chartist.Pie("#average_chart", average_data, average_opts)
|
||||||
var overview_chart = new Chartist.Line('#overview_chart', overview_data, overview_opts)
|
var overview_chart = new Chartist.Line('#overview_chart', overview_data, overview_opts)
|
||||||
// Setup the average chart listeners
|
// Setup the average chart listeners
|
||||||
$piechart = $("#average_chart")
|
$piechart = $("#average_chart")
|
||||||
var $pietoolTip = $piechart
|
var $pietoolTip = $piechart
|
||||||
.append('<div class="chartist-tooltip"></div>')
|
.append('<div class="chartist-tooltip"></div>')
|
||||||
.find('.chartist-tooltip')
|
.find('.chartist-tooltip')
|
||||||
.hide();
|
.hide();
|
||||||
|
|
||||||
$piechart.on('mouseenter', '.ct-slice-donut', function() {
|
$piechart.on('mouseenter', '.ct-slice-donut', function() {
|
||||||
var $point = $(this)
|
var $point = $(this)
|
||||||
value = $point.attr('ct:value')
|
value = $point.attr('ct:value')
|
||||||
label = $point.attr('ct:meta')
|
label = $point.attr('ct:meta')
|
||||||
$pietoolTip.html(label + ': ' + value.toString() + "%").show();
|
$pietoolTip.html(label + ': ' + value.toString() + "%").show();
|
||||||
});
|
|
||||||
|
|
||||||
$piechart.on('mouseleave', '.ct-slice-donut', function() {
|
|
||||||
$pietoolTip.hide();
|
|
||||||
});
|
|
||||||
$piechart.on('mousemove', function(event) {
|
|
||||||
$pietoolTip.css({
|
|
||||||
left: (event.offsetX || event.originalEvent.layerX) - $pietoolTip.width() / 2 - 10,
|
|
||||||
top: (event.offsetY + 40 || event.originalEvent.layerY) - $pietoolTip.height() - 80
|
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
// Setup the overview chart listeners
|
$piechart.on('mouseleave', '.ct-slice-donut', function() {
|
||||||
$chart = $("#overview_chart")
|
$pietoolTip.hide();
|
||||||
var $toolTip = $chart
|
|
||||||
.append('<div class="chartist-tooltip"></div>')
|
|
||||||
.find('.chartist-tooltip')
|
|
||||||
.hide();
|
|
||||||
|
|
||||||
$chart.on('mouseenter', '.ct-point', function() {
|
|
||||||
var $point = $(this)
|
|
||||||
value = $point.attr('ct:value') || 0
|
|
||||||
cidx = $point.attr('ct:meta')
|
|
||||||
$toolTip.html(campaigns[cidx].name + '<br>' + "Successes: " + value.toString() + "%").show();
|
|
||||||
});
|
|
||||||
|
|
||||||
$chart.on('mouseleave', '.ct-point', function() {
|
|
||||||
$toolTip.hide();
|
|
||||||
});
|
|
||||||
$chart.on('mousemove', function(event) {
|
|
||||||
$toolTip.css({
|
|
||||||
left: (event.offsetX || event.originalEvent.layerX) - $toolTip.width() / 2 - 10,
|
|
||||||
top: (event.offsetY + 40 || event.originalEvent.layerY) - $toolTip.height() - 40
|
|
||||||
});
|
});
|
||||||
});
|
$piechart.on('mousemove', function(event) {
|
||||||
$("#overview_chart").on("click", ".ct-point", function(e) {
|
$pietoolTip.css({
|
||||||
var $cidx = $(this).attr('ct:meta');
|
left: (event.offsetX || event.originalEvent.layerX) - $pietoolTip.width() / 2 - 10,
|
||||||
window.location.href = "/campaigns/" + campaigns[cidx].id
|
top: (event.offsetY + 40 || event.originalEvent.layerY) - $pietoolTip.height() - 80
|
||||||
});
|
});
|
||||||
} else {
|
});
|
||||||
$("#emptyMessage").show()
|
|
||||||
}
|
// Setup the overview chart listeners
|
||||||
})
|
$chart = $("#overview_chart")
|
||||||
.error(function(){
|
var $toolTip = $chart
|
||||||
errorFlash("Error fetching campaigns")
|
.append('<div class="chartist-tooltip"></div>')
|
||||||
})
|
.find('.chartist-tooltip')
|
||||||
|
.hide();
|
||||||
|
|
||||||
|
$chart.on('mouseenter', '.ct-point', function() {
|
||||||
|
var $point = $(this)
|
||||||
|
value = $point.attr('ct:value') || 0
|
||||||
|
cidx = $point.attr('ct:meta')
|
||||||
|
$toolTip.html(campaigns[cidx].name + '<br>' + "Successes: " + value.toString() + "%").show();
|
||||||
|
});
|
||||||
|
|
||||||
|
$chart.on('mouseleave', '.ct-point', function() {
|
||||||
|
$toolTip.hide();
|
||||||
|
});
|
||||||
|
$chart.on('mousemove', function(event) {
|
||||||
|
$toolTip.css({
|
||||||
|
left: (event.offsetX || event.originalEvent.layerX) - $toolTip.width() / 2 - 10,
|
||||||
|
top: (event.offsetY + 40 || event.originalEvent.layerY) - $toolTip.height() - 40
|
||||||
|
});
|
||||||
|
});
|
||||||
|
$("#overview_chart").on("click", ".ct-point", function(e) {
|
||||||
|
var $cidx = $(this).attr('ct:meta');
|
||||||
|
window.location.href = "/campaigns/" + campaigns[cidx].id
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
$("#emptyMessage").show()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.error(function() {
|
||||||
|
errorFlash("Error fetching campaigns")
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -4,72 +4,75 @@
|
||||||
Author: Jordan Wright <github.com/jordan-wright>
|
Author: Jordan Wright <github.com/jordan-wright>
|
||||||
*/
|
*/
|
||||||
var pages = []
|
var pages = []
|
||||||
|
|
||||||
// Save attempts to POST to /templates/
|
// Save attempts to POST to /templates/
|
||||||
function save(idx){
|
function save(idx) {
|
||||||
var page = {}
|
var page = {}
|
||||||
page.name = $("#name").val()
|
page.name = $("#name").val()
|
||||||
page.html = CKEDITOR.instances["html_editor"].getData();
|
page.html = CKEDITOR.instances["html_editor"].getData();
|
||||||
if (idx != -1){
|
if (idx != -1) {
|
||||||
page.id = pages[idx].id
|
page.id = pages[idx].id
|
||||||
api.pageId.put(page)
|
api.pageId.put(page)
|
||||||
.success(function(data){
|
.success(function(data) {
|
||||||
successFlash("Page edited successfully!")
|
successFlash("Page edited successfully!")
|
||||||
load()
|
load()
|
||||||
dismiss()
|
dismiss()
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
// Submit the page
|
// Submit the page
|
||||||
api.pages.post(page)
|
api.pages.post(page)
|
||||||
.success(function(data){
|
.success(function(data) {
|
||||||
successFlash("Page added successfully!")
|
successFlash("Page added successfully!")
|
||||||
load()
|
load()
|
||||||
dismiss()
|
dismiss()
|
||||||
})
|
})
|
||||||
.error(function(data){
|
.error(function(data) {
|
||||||
modalError(data.responseJSON.message)
|
modalError(data.responseJSON.message)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function dismiss(){
|
function dismiss() {
|
||||||
$("#modal\\.flashes").empty()
|
$("#modal\\.flashes").empty()
|
||||||
$("#name").val("")
|
$("#name").val("")
|
||||||
$("#html_editor").val("")
|
$("#html_editor").val("")
|
||||||
$("#newLandingPageModal").modal('hide')
|
$("#newLandingPageModal").modal('hide')
|
||||||
}
|
}
|
||||||
|
|
||||||
function deletePage(idx){
|
function deletePage(idx) {
|
||||||
if (confirm("Delete " + pages[idx].name + "?")){
|
if (confirm("Delete " + pages[idx].name + "?")) {
|
||||||
api.pageId.delete(pages[idx].id)
|
api.pageId.delete(pages[idx].id)
|
||||||
.success(function(data){
|
.success(function(data) {
|
||||||
successFlash(data.message)
|
successFlash(data.message)
|
||||||
load()
|
load()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function importSite(){
|
function importSite() {
|
||||||
url = $("#url").val()
|
url = $("#url").val()
|
||||||
if (!url){
|
if (!url) {
|
||||||
modalError("No URL Specified!")
|
modalError("No URL Specified!")
|
||||||
} else {
|
} else {
|
||||||
api.clone_site({
|
api.clone_site({
|
||||||
url: url,
|
url: url,
|
||||||
include_resources: false
|
include_resources: false
|
||||||
})
|
})
|
||||||
.success(function(data){
|
.success(function(data) {
|
||||||
console.log($("#html_editor"))
|
console.log($("#html_editor"))
|
||||||
$("#html_editor").val(data.html)
|
$("#html_editor").val(data.html)
|
||||||
$("#importSiteModal").modal("hide")
|
$("#importSiteModal").modal("hide")
|
||||||
})
|
})
|
||||||
.error(function(data){
|
.error(function(data) {
|
||||||
modalError(data.responseJSON.message)
|
modalError(data.responseJSON.message)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function edit(idx){
|
function edit(idx) {
|
||||||
$("#modalSubmit").unbind('click').click(function(){save(idx)})
|
$("#modalSubmit").unbind('click').click(function() {
|
||||||
|
save(idx)
|
||||||
|
})
|
||||||
$("#html_editor").ckeditor()
|
$("#html_editor").ckeditor()
|
||||||
var page = {}
|
var page = {}
|
||||||
if (idx != -1) {
|
if (idx != -1) {
|
||||||
|
@ -79,82 +82,86 @@ function edit(idx){
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function load(){
|
function load() {
|
||||||
/*
|
/*
|
||||||
load() - Loads the current pages using the API
|
load() - Loads the current pages using the API
|
||||||
*/
|
*/
|
||||||
$("#pagesTable").hide()
|
$("#pagesTable").hide()
|
||||||
$("#emptyMessage").hide()
|
$("#emptyMessage").hide()
|
||||||
$("#loading").show()
|
$("#loading").show()
|
||||||
api.pages.get()
|
api.pages.get()
|
||||||
.success(function(ps){
|
.success(function(ps) {
|
||||||
pages = ps
|
pages = ps
|
||||||
$("#loading").hide()
|
$("#loading").hide()
|
||||||
if (pages.length > 0){
|
if (pages.length > 0) {
|
||||||
$("#pagesTable").show()
|
$("#pagesTable").show()
|
||||||
pagesTable = $("#pagesTable").DataTable();
|
pagesTable = $("#pagesTable").DataTable({
|
||||||
pagesTable.clear()
|
destroy: true,
|
||||||
$.each(pages, function(i, page){
|
columnDefs: [{
|
||||||
pagesTable.row.add([
|
orderable: false,
|
||||||
page.name,
|
targets: "no-sort"
|
||||||
moment(page.modified_date).format('MMMM Do YYYY, h:mm:ss a'),
|
}]
|
||||||
"<div class='pull-right'><button class='btn btn-primary' data-toggle='modal' data-target='#newLandingPageModal' onclick='edit(" + i + ")'>\
|
});
|
||||||
|
pagesTable.clear()
|
||||||
|
$.each(pages, function(i, page) {
|
||||||
|
pagesTable.row.add([
|
||||||
|
page.name,
|
||||||
|
moment(page.modified_date).format('MMMM Do YYYY, h:mm:ss a'),
|
||||||
|
"<div class='pull-right'><button class='btn btn-primary' data-toggle='modal' data-target='#newLandingPageModal' onclick='edit(" + i + ")'>\
|
||||||
<i class='fa fa-pencil'></i>\
|
<i class='fa fa-pencil'></i>\
|
||||||
</button>\
|
</button>\
|
||||||
<button class='btn btn-danger' onclick='deletePage(" + i + ")'>\
|
<button class='btn btn-danger' onclick='deletePage(" + i + ")'>\
|
||||||
<i class='fa fa-trash-o'></i>\
|
<i class='fa fa-trash-o'></i>\
|
||||||
</button></div>"
|
</button></div>"
|
||||||
]).draw()
|
]).draw()
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
$("#emptyMessage").show()
|
$("#emptyMessage").show()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.error(function(){
|
.error(function() {
|
||||||
$("#loading").hide()
|
$("#loading").hide()
|
||||||
errorFlash("Error fetching pages")
|
errorFlash("Error fetching pages")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
$(document).ready(function(){
|
$(document).ready(function() {
|
||||||
// Setup multiple modals
|
// Setup multiple modals
|
||||||
// Code based on http://miles-by-motorcycle.com/static/bootstrap-modal/index.html
|
// Code based on http://miles-by-motorcycle.com/static/bootstrap-modal/index.html
|
||||||
$('.modal').on('hidden.bs.modal', function( event ) {
|
$('.modal').on('hidden.bs.modal', function(event) {
|
||||||
$(this).removeClass( 'fv-modal-stack' );
|
$(this).removeClass('fv-modal-stack');
|
||||||
$('body').data( 'fv_open_modals', $('body').data( 'fv_open_modals' ) - 1 );
|
$('body').data('fv_open_modals', $('body').data('fv_open_modals') - 1);
|
||||||
});
|
});
|
||||||
$( '.modal' ).on( 'shown.bs.modal', function ( event ) {
|
$('.modal').on('shown.bs.modal', function(event) {
|
||||||
// Keep track of the number of open modals
|
// Keep track of the number of open modals
|
||||||
if ( typeof( $('body').data( 'fv_open_modals' ) ) == 'undefined' )
|
if (typeof($('body').data('fv_open_modals')) == 'undefined') {
|
||||||
{
|
$('body').data('fv_open_modals', 0);
|
||||||
$('body').data( 'fv_open_modals', 0 );
|
|
||||||
}
|
}
|
||||||
// if the z-index of this modal has been set, ignore.
|
// if the z-index of this modal has been set, ignore.
|
||||||
if ( $(this).hasClass( 'fv-modal-stack' ) )
|
if ($(this).hasClass('fv-modal-stack')) {
|
||||||
{
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
$(this).addClass( 'fv-modal-stack' );
|
$(this).addClass('fv-modal-stack');
|
||||||
// Increment the number of open modals
|
// Increment the number of open modals
|
||||||
$('body').data( 'fv_open_modals', $('body').data( 'fv_open_modals' ) + 1 );
|
$('body').data('fv_open_modals', $('body').data('fv_open_modals') + 1);
|
||||||
// Setup the appropriate z-index
|
// Setup the appropriate z-index
|
||||||
$(this).css('z-index', 1040 + (10 * $('body').data( 'fv_open_modals' )));
|
$(this).css('z-index', 1040 + (10 * $('body').data('fv_open_modals')));
|
||||||
$( '.modal-backdrop' ).not( '.fv-modal-stack' ).css( 'z-index', 1039 + (10 * $('body').data( 'fv_open_modals' )));
|
$('.modal-backdrop').not('.fv-modal-stack').css('z-index', 1039 + (10 * $('body').data('fv_open_modals')));
|
||||||
$( '.modal-backdrop' ).not( 'fv-modal-stack' ).addClass( 'fv-modal-stack' );
|
$('.modal-backdrop').not('fv-modal-stack').addClass('fv-modal-stack');
|
||||||
});
|
});
|
||||||
$.fn.modal.Constructor.prototype.enforceFocus = function() {
|
$.fn.modal.Constructor.prototype.enforceFocus = function() {
|
||||||
$( document )
|
$(document)
|
||||||
.off( 'focusin.bs.modal' ) // guard against infinite focus loop
|
.off('focusin.bs.modal') // guard against infinite focus loop
|
||||||
.on( 'focusin.bs.modal', $.proxy( function( e ) {
|
.on('focusin.bs.modal', $.proxy(function(e) {
|
||||||
if (
|
if (
|
||||||
this.$element[ 0 ] !== e.target && !this.$element.has( e.target ).length
|
this.$element[0] !== e.target && !this.$element.has(e.target).length
|
||||||
// CKEditor compatibility fix start.
|
// CKEditor compatibility fix start.
|
||||||
&& !$( e.target ).closest( '.cke_dialog, .cke' ).length
|
&& !$(e.target).closest('.cke_dialog, .cke').length
|
||||||
// CKEditor compatibility fix end.
|
// CKEditor compatibility fix end.
|
||||||
) {
|
) {
|
||||||
this.$element.trigger( 'focus' );
|
this.$element.trigger('focus');
|
||||||
}
|
}
|
||||||
}, this ) );
|
}, this));
|
||||||
};
|
};
|
||||||
load()
|
load()
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,24 +1,24 @@
|
||||||
$(document).ready(function(){
|
$(document).ready(function() {
|
||||||
$("#apiResetForm").submit(function(e){
|
$("#apiResetForm").submit(function(e) {
|
||||||
$.post("/api/reset", $(this).serialize())
|
$.post("/api/reset", $(this).serialize())
|
||||||
.done(function(data){
|
.done(function(data) {
|
||||||
api_key = data.data
|
api_key = data.data
|
||||||
successFlash(data.message)
|
successFlash(data.message)
|
||||||
$("#api_key").val(api_key)
|
$("#api_key").val(api_key)
|
||||||
})
|
})
|
||||||
.fail(function(data){
|
.fail(function(data) {
|
||||||
errorFlash(data.message)
|
errorFlash(data.message)
|
||||||
})
|
})
|
||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
$("#settingsForm").submit(function(e){
|
$("#settingsForm").submit(function(e) {
|
||||||
$.post("/settings", $(this).serialize())
|
$.post("/settings", $(this).serialize())
|
||||||
.done(function(data){
|
.done(function(data) {
|
||||||
successFlash(data.message)
|
successFlash(data.message)
|
||||||
})
|
})
|
||||||
.fail(function(data){
|
.fail(function(data) {
|
||||||
errorFlash(data.responseJSON.message)
|
errorFlash(data.responseJSON.message)
|
||||||
})
|
})
|
||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,64 +1,66 @@
|
||||||
var templates = []
|
var templates = []
|
||||||
var icons = {
|
var icons = {
|
||||||
"application/vnd.ms-excel" : "fa-file-excel-o",
|
"application/vnd.ms-excel": "fa-file-excel-o",
|
||||||
"text/plain" : "fa-file-text-o",
|
"text/plain": "fa-file-text-o",
|
||||||
"image/gif" : "fa-file-image-o",
|
"image/gif": "fa-file-image-o",
|
||||||
"image/png" : "fa-file-image-o",
|
"image/png": "fa-file-image-o",
|
||||||
"application/pdf" : "fa-file-pdf-o",
|
"application/pdf": "fa-file-pdf-o",
|
||||||
"application/x-zip-compressed" : "fa-file-archive-o",
|
"application/x-zip-compressed": "fa-file-archive-o",
|
||||||
"application/x-gzip" : "fa-file-archive-o",
|
"application/x-gzip": "fa-file-archive-o",
|
||||||
"application/vnd.openxmlformats-officedocument.presentationml.presentation" : "fa-file-powerpoint-o",
|
"application/vnd.openxmlformats-officedocument.presentationml.presentation": "fa-file-powerpoint-o",
|
||||||
"application/vnd.openxmlformats-officedocument.wordprocessingml.document" : "fa-file-word-o",
|
"application/vnd.openxmlformats-officedocument.wordprocessingml.document": "fa-file-word-o",
|
||||||
"application/octet-stream" : "fa-file-o",
|
"application/octet-stream": "fa-file-o",
|
||||||
"application/x-msdownload" : "fa-file-o"
|
"application/x-msdownload": "fa-file-o"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save attempts to POST to /templates/
|
// Save attempts to POST to /templates/
|
||||||
function save(idx){
|
function save(idx) {
|
||||||
var template = {attachments:[]}
|
var template = {
|
||||||
|
attachments: []
|
||||||
|
}
|
||||||
template.name = $("#name").val()
|
template.name = $("#name").val()
|
||||||
template.subject = $("#subject").val()
|
template.subject = $("#subject").val()
|
||||||
template.html = CKEDITOR.instances["html_editor"].getData();
|
template.html = CKEDITOR.instances["html_editor"].getData();
|
||||||
// Fix the URL Scheme added by CKEditor (until we can remove it from the plugin)
|
// Fix the URL Scheme added by CKEditor (until we can remove it from the plugin)
|
||||||
template.html = template.html.replace(/https?:\/\/{{\.URL}}/gi, "{{.URL}}")
|
template.html = template.html.replace(/https?:\/\/{{\.URL}}/gi, "{{.URL}}")
|
||||||
// If the "Add Tracker Image" checkbox is checked, add the tracker
|
// If the "Add Tracker Image" checkbox is checked, add the tracker
|
||||||
if ($("#use_tracker_checkbox").prop("checked") &&
|
if ($("#use_tracker_checkbox").prop("checked") &&
|
||||||
template.html.indexOf("{{.Tracker}}") == -1 &&
|
template.html.indexOf("{{.Tracker}}") == -1 &&
|
||||||
template.html.indexOf("{{.TrackingUrl}}") == -1){
|
template.html.indexOf("{{.TrackingUrl}}") == -1) {
|
||||||
template.html = template.html.replace("</body>", "{{.Tracker}}</body>")
|
template.html = template.html.replace("</body>", "{{.Tracker}}</body>")
|
||||||
}
|
}
|
||||||
template.text = $("#text_editor").val()
|
template.text = $("#text_editor").val()
|
||||||
// Add the attachments
|
// Add the attachments
|
||||||
$.each($("#attachmentsTable").DataTable().rows().data(), function(i, target){
|
$.each($("#attachmentsTable").DataTable().rows().data(), function(i, target) {
|
||||||
template.attachments.push({
|
template.attachments.push({
|
||||||
name : target[1],
|
name: target[1],
|
||||||
content: target[3],
|
content: target[3],
|
||||||
type: target[4],
|
type: target[4],
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
if (idx != -1){
|
if (idx != -1) {
|
||||||
template.id = templates[idx].id
|
template.id = templates[idx].id
|
||||||
api.templateId.put(template)
|
api.templateId.put(template)
|
||||||
.success(function(data){
|
.success(function(data) {
|
||||||
successFlash("Template edited successfully!")
|
successFlash("Template edited successfully!")
|
||||||
load()
|
load()
|
||||||
dismiss()
|
dismiss()
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
// Submit the template
|
// Submit the template
|
||||||
api.templates.post(template)
|
api.templates.post(template)
|
||||||
.success(function(data){
|
.success(function(data) {
|
||||||
successFlash("Template added successfully!")
|
successFlash("Template added successfully!")
|
||||||
load()
|
load()
|
||||||
dismiss()
|
dismiss()
|
||||||
})
|
})
|
||||||
.error(function(data){
|
.error(function(data) {
|
||||||
modalError(data.responseJSON.message)
|
modalError(data.responseJSON.message)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function dismiss(){
|
function dismiss() {
|
||||||
$("#modal\\.flashes").empty()
|
$("#modal\\.flashes").empty()
|
||||||
$("#attachmentsTable").dataTable().DataTable().clear().draw()
|
$("#attachmentsTable").dataTable().DataTable().clear().draw()
|
||||||
$("#name").val("")
|
$("#name").val("")
|
||||||
|
@ -67,24 +69,36 @@ function dismiss(){
|
||||||
$("#modal").modal('hide')
|
$("#modal").modal('hide')
|
||||||
}
|
}
|
||||||
|
|
||||||
function deleteTemplate(idx){
|
function deleteTemplate(idx) {
|
||||||
if (confirm("Delete " + templates[idx].name + "?")){
|
if (confirm("Delete " + templates[idx].name + "?")) {
|
||||||
api.templateId.delete(templates[idx].id)
|
api.templateId.delete(templates[idx].id)
|
||||||
.success(function(data){
|
.success(function(data) {
|
||||||
successFlash(data.message)
|
successFlash(data.message)
|
||||||
load()
|
load()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function attach(files){
|
function attach(files) {
|
||||||
attachmentsTable = $("#attachmentsTable").DataTable();
|
attachmentsTable = $("#attachmentsTable").DataTable({
|
||||||
$.each(files, function(i, file){
|
destroy: true,
|
||||||
|
"order": [
|
||||||
|
[1, "asc"]
|
||||||
|
],
|
||||||
|
columnDefs: [{
|
||||||
|
orderable: false,
|
||||||
|
targets: "no-sort"
|
||||||
|
}, {
|
||||||
|
sClass: "datatable_hidden",
|
||||||
|
targets: [3, 4]
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
$.each(files, function(i, file) {
|
||||||
var reader = new FileReader();
|
var reader = new FileReader();
|
||||||
/* Make this a datatable */
|
/* Make this a datatable */
|
||||||
reader.onload = function(e){
|
reader.onload = function(e) {
|
||||||
var icon = icons[file.type] || "fa-file-o"
|
var icon = icons[file.type] || "fa-file-o"
|
||||||
// Add the record to the modal
|
// Add the record to the modal
|
||||||
attachmentsTable.row.add([
|
attachmentsTable.row.add([
|
||||||
'<i class="fa ' + icon + '"></i>',
|
'<i class="fa ' + icon + '"></i>',
|
||||||
file.name,
|
file.name,
|
||||||
|
@ -100,33 +114,40 @@ function attach(files){
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function edit(idx){
|
function edit(idx) {
|
||||||
$("#modalSubmit").unbind('click').click(function(){save(idx)})
|
$("#modalSubmit").unbind('click').click(function() {
|
||||||
$("#attachmentUpload").unbind('click').click(function(){this.value=null})
|
save(idx)
|
||||||
|
})
|
||||||
|
$("#attachmentUpload").unbind('click').click(function() {
|
||||||
|
this.value = null
|
||||||
|
})
|
||||||
$("#html_editor").ckeditor()
|
$("#html_editor").ckeditor()
|
||||||
$("#attachmentsTable").show()
|
$("#attachmentsTable").show()
|
||||||
attachmentsTable = null
|
attachmentsTable = $('#attachmentsTable').DataTable({
|
||||||
if ( $.fn.dataTable.isDataTable('#attachmentsTable') ) {
|
destroy: true,
|
||||||
attachmentsTable = $('#attachmentsTable').DataTable();
|
"order": [
|
||||||
|
[1, "asc"]
|
||||||
|
],
|
||||||
|
columnDefs: [{
|
||||||
|
orderable: false,
|
||||||
|
targets: "no-sort"
|
||||||
|
}, {
|
||||||
|
sClass: "datatable_hidden",
|
||||||
|
targets: [3, 4]
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
var template = {
|
||||||
|
attachments: []
|
||||||
}
|
}
|
||||||
else {
|
|
||||||
attachmentsTable = $("#attachmentsTable").DataTable({
|
|
||||||
"aoColumnDefs" : [{
|
|
||||||
"targets" : [3,4],
|
|
||||||
"sClass" : "datatable_hidden"
|
|
||||||
}]
|
|
||||||
});
|
|
||||||
}
|
|
||||||
var template = {attachments:[]}
|
|
||||||
if (idx != -1) {
|
if (idx != -1) {
|
||||||
template = templates[idx]
|
template = templates[idx]
|
||||||
$("#name").val(template.name)
|
$("#name").val(template.name)
|
||||||
$("#subject").val(template.subject)
|
$("#subject").val(template.subject)
|
||||||
$("#html_editor").val(template.html)
|
$("#html_editor").val(template.html)
|
||||||
$("#text_editor").val(template.text)
|
$("#text_editor").val(template.text)
|
||||||
$.each(template.attachments, function(i, file){
|
$.each(template.attachments, function(i, file) {
|
||||||
var icon = icons[file.type] || "fa-file-o"
|
var icon = icons[file.type] || "fa-file-o"
|
||||||
// Add the record to the modal
|
// Add the record to the modal
|
||||||
attachmentsTable.row.add([
|
attachmentsTable.row.add([
|
||||||
'<i class="fa ' + icon + '"></i>',
|
'<i class="fa ' + icon + '"></i>',
|
||||||
file.name,
|
file.name,
|
||||||
|
@ -137,110 +158,114 @@ function edit(idx){
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
// Handle Deletion
|
// Handle Deletion
|
||||||
$("#attachmentsTable").unbind('click').on("click", "span>i.fa-trash-o", function(){
|
$("#attachmentsTable").unbind('click').on("click", "span>i.fa-trash-o", function() {
|
||||||
attachmentsTable.row( $(this).parents('tr') )
|
attachmentsTable.row($(this).parents('tr'))
|
||||||
.remove()
|
.remove()
|
||||||
.draw();
|
.draw();
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function importEmail(){
|
function importEmail() {
|
||||||
raw = $("#email_content").val()
|
raw = $("#email_content").val()
|
||||||
if (!raw){
|
if (!raw) {
|
||||||
modalError("No Content Specified!")
|
modalError("No Content Specified!")
|
||||||
} else {
|
} else {
|
||||||
$.ajax({
|
$.ajax({
|
||||||
type: "POST",
|
type: "POST",
|
||||||
url: "/api/import/email",
|
url: "/api/import/email",
|
||||||
data: raw,
|
data: raw,
|
||||||
dataType: "json",
|
dataType: "json",
|
||||||
contentType: "text/plain"
|
contentType: "text/plain"
|
||||||
})
|
})
|
||||||
.success(function(data){
|
.success(function(data) {
|
||||||
$("#text_editor").val(data.text)
|
$("#text_editor").val(data.text)
|
||||||
$("#html_editor").val(data.html)
|
$("#html_editor").val(data.html)
|
||||||
$("#subject").val(data.subject)
|
$("#subject").val(data.subject)
|
||||||
$("#importEmailModal").modal("hide")
|
$("#importEmailModal").modal("hide")
|
||||||
})
|
})
|
||||||
.error(function(data){
|
.error(function(data) {
|
||||||
modalError(data.responseJSON.message)
|
modalError(data.responseJSON.message)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function load(){
|
function load() {
|
||||||
$("#templateTable").hide()
|
$("#templateTable").hide()
|
||||||
$("#emptyMessage").hide()
|
$("#emptyMessage").hide()
|
||||||
$("#loading").show()
|
$("#loading").show()
|
||||||
api.templates.get()
|
api.templates.get()
|
||||||
.success(function(ts){
|
.success(function(ts) {
|
||||||
templates = ts
|
templates = ts
|
||||||
$("#loading").hide()
|
$("#loading").hide()
|
||||||
if (templates.length > 0){
|
if (templates.length > 0) {
|
||||||
$("#templateTable").show()
|
$("#templateTable").show()
|
||||||
templateTable = $("#templateTable").DataTable();
|
templateTable = $("#templateTable").DataTable({
|
||||||
templateTable.clear()
|
destroy: true,
|
||||||
$.each(templates, function(i, template){
|
columnDefs: [{
|
||||||
templateTable.row.add([
|
orderable: false,
|
||||||
template.name,
|
targets: "no-sort"
|
||||||
moment(template.modified_date).format('MMMM Do YYYY, h:mm:ss a'),
|
}]
|
||||||
"<div class='pull-right'><button class='btn btn-primary' data-toggle='modal' data-target='#modal' onclick='edit(" + i + ")'>\
|
});
|
||||||
|
templateTable.clear()
|
||||||
|
$.each(templates, function(i, template) {
|
||||||
|
templateTable.row.add([
|
||||||
|
template.name,
|
||||||
|
moment(template.modified_date).format('MMMM Do YYYY, h:mm:ss a'),
|
||||||
|
"<div class='pull-right'><button class='btn btn-primary' data-toggle='modal' data-target='#modal' onclick='edit(" + i + ")'>\
|
||||||
<i class='fa fa-pencil'></i>\
|
<i class='fa fa-pencil'></i>\
|
||||||
</button>\
|
</button>\
|
||||||
<button class='btn btn-danger' onclick='deleteTemplate(" + i + ")'>\
|
<button class='btn btn-danger' onclick='deleteTemplate(" + i + ")'>\
|
||||||
<i class='fa fa-trash-o'></i>\
|
<i class='fa fa-trash-o'></i>\
|
||||||
</button></div>"
|
</button></div>"
|
||||||
]).draw()
|
]).draw()
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
$("#emptyMessage").show()
|
$("#emptyMessage").show()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.error(function(){
|
.error(function() {
|
||||||
$("#loading").hide()
|
$("#loading").hide()
|
||||||
errorFlash("Error fetching templates")
|
errorFlash("Error fetching templates")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
$(document).ready(function(){
|
$(document).ready(function() {
|
||||||
// Setup multiple modals
|
// Setup multiple modals
|
||||||
// Code based on http://miles-by-motorcycle.com/static/bootstrap-modal/index.html
|
// Code based on http://miles-by-motorcycle.com/static/bootstrap-modal/index.html
|
||||||
$('.modal').on('hidden.bs.modal', function( event ) {
|
$('.modal').on('hidden.bs.modal', function(event) {
|
||||||
$(this).removeClass( 'fv-modal-stack' );
|
$(this).removeClass('fv-modal-stack');
|
||||||
$('body').data( 'fv_open_modals', $('body').data( 'fv_open_modals' ) - 1 );
|
$('body').data('fv_open_modals', $('body').data('fv_open_modals') - 1);
|
||||||
});
|
});
|
||||||
$( '.modal' ).on( 'shown.bs.modal', function ( event ) {
|
$('.modal').on('shown.bs.modal', function(event) {
|
||||||
// Keep track of the number of open modals
|
// Keep track of the number of open modals
|
||||||
if ( typeof( $('body').data( 'fv_open_modals' ) ) == 'undefined' )
|
if (typeof($('body').data('fv_open_modals')) == 'undefined') {
|
||||||
{
|
$('body').data('fv_open_modals', 0);
|
||||||
$('body').data( 'fv_open_modals', 0 );
|
|
||||||
}
|
}
|
||||||
// if the z-index of this modal has been set, ignore.
|
// if the z-index of this modal has been set, ignore.
|
||||||
if ( $(this).hasClass( 'fv-modal-stack' ) )
|
if ($(this).hasClass('fv-modal-stack')) {
|
||||||
{
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
$(this).addClass( 'fv-modal-stack' );
|
$(this).addClass('fv-modal-stack');
|
||||||
// Increment the number of open modals
|
// Increment the number of open modals
|
||||||
$('body').data( 'fv_open_modals', $('body').data( 'fv_open_modals' ) + 1 );
|
$('body').data('fv_open_modals', $('body').data('fv_open_modals') + 1);
|
||||||
// Setup the appropriate z-index
|
// Setup the appropriate z-index
|
||||||
$(this).css('z-index', 1040 + (10 * $('body').data( 'fv_open_modals' )));
|
$(this).css('z-index', 1040 + (10 * $('body').data('fv_open_modals')));
|
||||||
$( '.modal-backdrop' ).not( '.fv-modal-stack' ).css( 'z-index', 1039 + (10 * $('body').data( 'fv_open_modals' )));
|
$('.modal-backdrop').not('.fv-modal-stack').css('z-index', 1039 + (10 * $('body').data('fv_open_modals')));
|
||||||
$( '.modal-backdrop' ).not( 'fv-modal-stack' ).addClass( 'fv-modal-stack' );
|
$('.modal-backdrop').not('fv-modal-stack').addClass('fv-modal-stack');
|
||||||
});
|
});
|
||||||
$.fn.modal.Constructor.prototype.enforceFocus = function() {
|
$.fn.modal.Constructor.prototype.enforceFocus = function() {
|
||||||
$( document )
|
$(document)
|
||||||
.off( 'focusin.bs.modal' ) // guard against infinite focus loop
|
.off('focusin.bs.modal') // guard against infinite focus loop
|
||||||
.on( 'focusin.bs.modal', $.proxy( function( e ) {
|
.on('focusin.bs.modal', $.proxy(function(e) {
|
||||||
if (
|
if (
|
||||||
this.$element[ 0 ] !== e.target && !this.$element.has( e.target ).length
|
this.$element[0] !== e.target && !this.$element.has(e.target).length
|
||||||
// CKEditor compatibility fix start.
|
// CKEditor compatibility fix start.
|
||||||
&& !$( e.target ).closest( '.cke_dialog, .cke' ).length
|
&& !$(e.target).closest('.cke_dialog, .cke').length
|
||||||
// CKEditor compatibility fix end.
|
// CKEditor compatibility fix end.
|
||||||
) {
|
) {
|
||||||
this.$element.trigger( 'focus' );
|
this.$element.trigger('focus');
|
||||||
}
|
}
|
||||||
}, this ) );
|
}, this));
|
||||||
};
|
};
|
||||||
load()
|
load()
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,59 +1,68 @@
|
||||||
var groups = []
|
var groups = []
|
||||||
|
|
||||||
// Save attempts to POST or PUT to /groups/
|
// Save attempts to POST or PUT to /groups/
|
||||||
function save(idx){
|
function save(idx) {
|
||||||
var targets = []
|
var targets = []
|
||||||
$.each($("#targetsTable").DataTable().rows().data(), function(i, target){
|
$.each($("#targetsTable").DataTable().rows().data(), function(i, target) {
|
||||||
targets.push({
|
targets.push({
|
||||||
first_name : target[0],
|
first_name: target[0],
|
||||||
last_name: target[1],
|
last_name: target[1],
|
||||||
email: target[2],
|
email: target[2],
|
||||||
position: target[3]
|
position: target[3]
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
var group = {
|
var group = {
|
||||||
name: $("#name").val(),
|
name: $("#name").val(),
|
||||||
targets: targets
|
targets: targets
|
||||||
}
|
}
|
||||||
// Submit the group
|
// Submit the group
|
||||||
if (idx != -1) {
|
if (idx != -1) {
|
||||||
// If we're just editing an existing group,
|
// If we're just editing an existing group,
|
||||||
// we need to PUT /groups/:id
|
// we need to PUT /groups/:id
|
||||||
group.id = groups[idx].id
|
group.id = groups[idx].id
|
||||||
api.groupId.put(group)
|
api.groupId.put(group)
|
||||||
.success(function(data){
|
.success(function(data) {
|
||||||
successFlash("Group updated successfully!")
|
successFlash("Group updated successfully!")
|
||||||
load()
|
load()
|
||||||
dismiss()
|
dismiss()
|
||||||
})
|
$("#modal").modal('hide')
|
||||||
.error(function(data){
|
})
|
||||||
modalError(data.responseJSON.message)
|
.error(function(data) {
|
||||||
})
|
modalError(data.responseJSON.message)
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
// Else, if this is a new group, POST it
|
// Else, if this is a new group, POST it
|
||||||
// to /groups
|
// to /groups
|
||||||
api.groups.post(group)
|
api.groups.post(group)
|
||||||
.success(function(data){
|
.success(function(data) {
|
||||||
successFlash("Group added successfully!")
|
successFlash("Group added successfully!")
|
||||||
load()
|
load()
|
||||||
dismiss()
|
dismiss()
|
||||||
})
|
$("#modal").modal('hide')
|
||||||
.error(function(data){
|
})
|
||||||
modalError(data.responseJSON.message)
|
.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()
|
||||||
$("#modal").modal('hide')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function edit(idx){
|
function edit(idx) {
|
||||||
targets = $("#targetsTable").dataTable()
|
targets = $("#targetsTable").dataTable({
|
||||||
$("#modalSubmit").unbind('click').click(function(){save(idx)})
|
destroy: true, // Destroy any other instantiated table - http://datatables.net/manual/tech-notes/3#destroy
|
||||||
|
columnDefs: [{
|
||||||
|
orderable: false,
|
||||||
|
targets: "no-sort"
|
||||||
|
}]
|
||||||
|
})
|
||||||
|
$("#modalSubmit").unbind('click').click(function() {
|
||||||
|
save(idx)
|
||||||
|
})
|
||||||
if (idx == -1) {
|
if (idx == -1) {
|
||||||
group = {}
|
group = {}
|
||||||
} else {
|
} else {
|
||||||
|
@ -61,31 +70,6 @@ function edit(idx){
|
||||||
$("#name").val(group.name)
|
$("#name").val(group.name)
|
||||||
$.each(group.targets, function(i, record) {
|
$.each(group.targets, function(i, record) {
|
||||||
targets.DataTable()
|
targets.DataTable()
|
||||||
.row.add([
|
|
||||||
record.first_name,
|
|
||||||
record.last_name,
|
|
||||||
record.email,
|
|
||||||
record.position,
|
|
||||||
'<span style="cursor:pointer;"><i class="fa fa-trash-o"></i></span>'
|
|
||||||
]).draw()
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// Handle file uploads
|
|
||||||
$("#csvupload").fileupload({
|
|
||||||
dataType:"json",
|
|
||||||
add: function(e, data){
|
|
||||||
$("#modal\\.flashes").empty()
|
|
||||||
var acceptFileTypes= /(csv|txt)$/i;
|
|
||||||
var filename = data.originalFiles[0]['name']
|
|
||||||
if (filename && !acceptFileTypes.test(filename.split(".").pop())) {
|
|
||||||
modalError("Unsupported file extension (use .csv or .txt)")
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
data.submit();
|
|
||||||
},
|
|
||||||
done: function(e, data){
|
|
||||||
$.each(data.result, function(i, record) {
|
|
||||||
targets.DataTable()
|
|
||||||
.row.add([
|
.row.add([
|
||||||
record.first_name,
|
record.first_name,
|
||||||
record.last_name,
|
record.last_name,
|
||||||
|
@ -93,87 +77,121 @@ function edit(idx){
|
||||||
record.position,
|
record.position,
|
||||||
'<span style="cursor:pointer;"><i class="fa fa-trash-o"></i></span>'
|
'<span style="cursor:pointer;"><i class="fa fa-trash-o"></i></span>'
|
||||||
]).draw()
|
]).draw()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Handle file uploads
|
||||||
|
$("#csvupload").fileupload({
|
||||||
|
dataType: "json",
|
||||||
|
add: function(e, data) {
|
||||||
|
$("#modal\\.flashes").empty()
|
||||||
|
var acceptFileTypes = /(csv|txt)$/i;
|
||||||
|
var filename = data.originalFiles[0]['name']
|
||||||
|
if (filename && !acceptFileTypes.test(filename.split(".").pop())) {
|
||||||
|
modalError("Unsupported file extension (use .csv or .txt)")
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
data.submit();
|
||||||
|
},
|
||||||
|
done: function(e, data) {
|
||||||
|
$.each(data.result, function(i, record) {
|
||||||
|
targets.DataTable()
|
||||||
|
.row.add([
|
||||||
|
record.first_name,
|
||||||
|
record.last_name,
|
||||||
|
record.email,
|
||||||
|
record.position,
|
||||||
|
'<span style="cursor:pointer;"><i class="fa fa-trash-o"></i></span>'
|
||||||
|
]).draw()
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function deleteGroup(idx){
|
function deleteGroup(idx) {
|
||||||
if (confirm("Delete " + groups[idx].name + "?")){
|
if (confirm("Delete " + groups[idx].name + "?")) {
|
||||||
api.groupId.delete(groups[idx].id)
|
api.groupId.delete(groups[idx].id)
|
||||||
.success(function(data){
|
.success(function(data) {
|
||||||
successFlash(data.message)
|
successFlash(data.message)
|
||||||
load()
|
load()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function load(){
|
function load() {
|
||||||
$("#groupTable").hide()
|
$("#groupTable").hide()
|
||||||
$("#emptyMessage").hide()
|
$("#emptyMessage").hide()
|
||||||
$("#loading").show()
|
$("#loading").show()
|
||||||
api.groups.get()
|
api.groups.get()
|
||||||
.success(function(gs){
|
.success(function(gs) {
|
||||||
$("#loading").hide()
|
$("#loading").hide()
|
||||||
if (gs.length > 0){
|
if (gs.length > 0) {
|
||||||
groups = gs
|
groups = gs
|
||||||
$("#emptyMessage").hide()
|
$("#emptyMessage").hide()
|
||||||
$("#groupTable").show()
|
$("#groupTable").show()
|
||||||
groupTable = $("#groupTable").DataTable();
|
groupTable = $("#groupTable").DataTable({
|
||||||
groupTable.clear();
|
destroy: true,
|
||||||
$.each(groups, function(i, group){
|
columnDefs: [{
|
||||||
var targets = ""
|
orderable: false,
|
||||||
$.each(group.targets, function(i, target){
|
targets: "no-sort"
|
||||||
targets += target.email + ", "
|
}]
|
||||||
if (targets.length > 50) {
|
});
|
||||||
targets = targets.slice(0,-3) + "..."
|
groupTable.clear();
|
||||||
return false;
|
$.each(groups, function(i, group) {
|
||||||
}
|
var targets = ""
|
||||||
})
|
$.each(group.targets, function(i, target) {
|
||||||
groupTable.row.add([
|
targets += target.email + ", "
|
||||||
group.name,
|
if (targets.length > 50) {
|
||||||
targets,
|
targets = targets.slice(0, -3) + "..."
|
||||||
moment(group.modified_date).format('MMMM Do YYYY, h:mm:ss a'),
|
return false;
|
||||||
"<div class='pull-right'><button class='btn btn-primary' data-toggle='modal' data-target='#modal' onclick='edit(" + i + ")'>\
|
}
|
||||||
|
})
|
||||||
|
groupTable.row.add([
|
||||||
|
group.name,
|
||||||
|
targets,
|
||||||
|
moment(group.modified_date).format('MMMM Do YYYY, h:mm:ss a'),
|
||||||
|
"<div class='pull-right'><button class='btn btn-primary' data-toggle='modal' data-target='#modal' onclick='edit(" + i + ")'>\
|
||||||
<i class='fa fa-pencil'></i>\
|
<i class='fa fa-pencil'></i>\
|
||||||
</button>\
|
</button>\
|
||||||
<button class='btn btn-danger' onclick='deleteGroup(" + i + ")'>\
|
<button class='btn btn-danger' onclick='deleteGroup(" + i + ")'>\
|
||||||
<i class='fa fa-trash-o'></i>\
|
<i class='fa fa-trash-o'></i>\
|
||||||
</button></div>"
|
</button></div>"
|
||||||
]).draw()
|
]).draw()
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
$("#emptyMessage").show()
|
$("#emptyMessage").show()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.error(function(){
|
.error(function() {
|
||||||
errorFlash("Error fetching groups")
|
errorFlash("Error fetching groups")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
$(document).ready(function(){
|
$(document).ready(function() {
|
||||||
load()
|
load()
|
||||||
// Setup the event listeners
|
// Setup the event listeners
|
||||||
// Handle manual additions
|
// Handle manual additions
|
||||||
$("#targetForm").submit(function(){
|
$("#targetForm").submit(function() {
|
||||||
|
targets.DataTable()
|
||||||
|
.row.add([
|
||||||
|
$("#firstName").val(),
|
||||||
|
$("#lastName").val(),
|
||||||
|
$("#email").val(),
|
||||||
|
$("#position").val(),
|
||||||
|
'<span style="cursor:pointer;"><i class="fa fa-trash-o"></i></span>'
|
||||||
|
])
|
||||||
|
.draw()
|
||||||
|
$("#targetForm>div>input").val('')
|
||||||
|
$("#firstName").focus()
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
// Handle Deletion
|
||||||
|
$("#targetsTable").on("click", "span>i.fa-trash-o", function() {
|
||||||
targets.DataTable()
|
targets.DataTable()
|
||||||
.row.add([
|
.row($(this).parents('tr'))
|
||||||
$("#firstName").val(),
|
.remove()
|
||||||
$("#lastName").val(),
|
.draw();
|
||||||
$("#email").val(),
|
|
||||||
$("#position").val(),
|
|
||||||
'<span style="cursor:pointer;"><i class="fa fa-trash-o"></i></span>'
|
|
||||||
])
|
|
||||||
.draw()
|
|
||||||
$("#targetForm>div>input").val('')
|
|
||||||
$("#firstName").focus()
|
|
||||||
return false
|
|
||||||
})
|
})
|
||||||
// Handle Deletion
|
$("#modal").on("hide.bs.modal", function() {
|
||||||
$("#targetsTable").on("click", "span>i.fa-trash-o", function(){
|
dismiss()
|
||||||
targets.DataTable()
|
|
||||||
.row( $(this).parents('tr') )
|
|
||||||
.remove()
|
|
||||||
.draw();
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,18 +1,16 @@
|
||||||
function errorFlash(message) {
|
function errorFlash(message) {
|
||||||
$("#flashes").empty()
|
$("#flashes").empty()
|
||||||
$("#flashes").append("<div style=\"text-align:center\" class=\"alert alert-danger\">\
|
$("#flashes").append("<div style=\"text-align:center\" class=\"alert alert-danger\">\
|
||||||
<i class=\"fa fa-exclamation-circle\"></i> " + message + "</div>"
|
<i class=\"fa fa-exclamation-circle\"></i> " + message + "</div>")
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function successFlash(message) {
|
function successFlash(message) {
|
||||||
$("#flashes").empty()
|
$("#flashes").empty()
|
||||||
$("#flashes").append("<div style=\"text-align:center\" class=\"alert alert-success\">\
|
$("#flashes").append("<div style=\"text-align:center\" class=\"alert alert-success\">\
|
||||||
<i class=\"fa fa-check-circle\"></i> " + message + "</div>"
|
<i class=\"fa fa-check-circle\"></i> " + message + "</div>")
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function modalError(message){
|
function modalError(message) {
|
||||||
$("#modal\\.flashes").empty().append("<div style=\"text-align:center\" class=\"alert alert-danger\">\
|
$("#modal\\.flashes").empty().append("<div style=\"text-align:center\" class=\"alert alert-danger\">\
|
||||||
<i class=\"fa fa-exclamation-circle\"></i> " + message + "</div>")
|
<i class=\"fa fa-exclamation-circle\"></i> " + message + "</div>")
|
||||||
}
|
}
|
||||||
|
@ -23,7 +21,7 @@ function query(endpoint, method, data) {
|
||||||
async: false,
|
async: false,
|
||||||
method: method,
|
method: method,
|
||||||
data: JSON.stringify(data),
|
data: JSON.stringify(data),
|
||||||
dataType:"json",
|
dataType: "json",
|
||||||
contentType: "application/json"
|
contentType: "application/json"
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -33,117 +31,117 @@ Define our API Endpoints
|
||||||
*/
|
*/
|
||||||
var api = {
|
var api = {
|
||||||
// campaigns contains the endpoints for /campaigns
|
// campaigns contains the endpoints for /campaigns
|
||||||
campaigns : {
|
campaigns: {
|
||||||
// get() - Queries the API for GET /campaigns
|
// get() - Queries the API for GET /campaigns
|
||||||
get: function(){
|
get: function() {
|
||||||
return query("/campaigns/", "GET", {})
|
return query("/campaigns/", "GET", {})
|
||||||
},
|
},
|
||||||
// post() - Posts a campaign to POST /campaigns
|
// post() - Posts a campaign to POST /campaigns
|
||||||
post: function(data){
|
post: function(data) {
|
||||||
return query("/campaigns/", "POST", data)
|
return query("/campaigns/", "POST", data)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// campaignId contains the endpoints for /campaigns/:id
|
// campaignId contains the endpoints for /campaigns/:id
|
||||||
campaignId : {
|
campaignId: {
|
||||||
// get() - Queries the API for GET /campaigns/:id
|
// get() - Queries the API for GET /campaigns/:id
|
||||||
get: function(id){
|
get: function(id) {
|
||||||
return query("/campaigns/" + id, "GET", {})
|
return query("/campaigns/" + id, "GET", {})
|
||||||
},
|
},
|
||||||
// delete() - Deletes a campaign at DELETE /campaigns/:id
|
// delete() - Deletes a campaign at DELETE /campaigns/:id
|
||||||
delete: function(id){
|
delete: function(id) {
|
||||||
return query("/campaigns/" + id, "DELETE", data)
|
return query("/campaigns/" + id, "DELETE", {})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// groups contains the endpoints for /groups
|
// groups contains the endpoints for /groups
|
||||||
groups : {
|
groups: {
|
||||||
// get() - Queries the API for GET /groups
|
// get() - Queries the API for GET /groups
|
||||||
get: function(){
|
get: function() {
|
||||||
return query("/groups/", "GET", {})
|
return query("/groups/", "GET", {})
|
||||||
},
|
},
|
||||||
// post() - Posts a campaign to POST /groups
|
// post() - Posts a campaign to POST /groups
|
||||||
post: function(group){
|
post: function(group) {
|
||||||
return query("/groups/", "POST", group)
|
return query("/groups/", "POST", group)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// groupId contains the endpoints for /groups/:id
|
// groupId contains the endpoints for /groups/:id
|
||||||
groupId : {
|
groupId: {
|
||||||
// get() - Queries the API for GET /groups/:id
|
// get() - Queries the API for GET /groups/:id
|
||||||
get: function(id){
|
get: function(id) {
|
||||||
return query("/groups/" + id, "GET", {})
|
return query("/groups/" + id, "GET", {})
|
||||||
},
|
},
|
||||||
// put() - Puts a campaign to PUT /groups/:id
|
// put() - Puts a campaign to PUT /groups/:id
|
||||||
put: function (group){
|
put: function(group) {
|
||||||
return query("/groups/" + group.id, "PUT", group)
|
return query("/groups/" + group.id, "PUT", group)
|
||||||
},
|
},
|
||||||
// delete() - Deletes a campaign at DELETE /groups/:id
|
// delete() - Deletes a campaign at DELETE /groups/:id
|
||||||
delete: function(id){
|
delete: function(id) {
|
||||||
return query("/groups/" + id, "DELETE", {})
|
return query("/groups/" + id, "DELETE", {})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// templates contains the endpoints for /templates
|
// templates contains the endpoints for /templates
|
||||||
templates : {
|
templates: {
|
||||||
// get() - Queries the API for GET /templates
|
// get() - Queries the API for GET /templates
|
||||||
get: function(){
|
get: function() {
|
||||||
return query("/templates/", "GET", {})
|
return query("/templates/", "GET", {})
|
||||||
},
|
},
|
||||||
// post() - Posts a campaign to POST /templates
|
// post() - Posts a campaign to POST /templates
|
||||||
post: function(template){
|
post: function(template) {
|
||||||
return query("/templates/", "POST", template)
|
return query("/templates/", "POST", template)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// templateId contains the endpoints for /templates/:id
|
// templateId contains the endpoints for /templates/:id
|
||||||
templateId : {
|
templateId: {
|
||||||
// get() - Queries the API for GET /templates/:id
|
// get() - Queries the API for GET /templates/:id
|
||||||
get: function(id){
|
get: function(id) {
|
||||||
return query("/templates/" + id, "GET", {})
|
return query("/templates/" + id, "GET", {})
|
||||||
},
|
},
|
||||||
// put() - Puts a campaign to PUT /templates/:id
|
// put() - Puts a campaign to PUT /templates/:id
|
||||||
put: function (template){
|
put: function(template) {
|
||||||
return query("/templates/" + template.id, "PUT", template)
|
return query("/templates/" + template.id, "PUT", template)
|
||||||
},
|
},
|
||||||
// delete() - Deletes a campaign at DELETE /templates/:id
|
// delete() - Deletes a campaign at DELETE /templates/:id
|
||||||
delete: function(id){
|
delete: function(id) {
|
||||||
return query("/templates/" + id, "DELETE", {})
|
return query("/templates/" + id, "DELETE", {})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// pages contains the endpoints for /pages
|
// pages contains the endpoints for /pages
|
||||||
pages : {
|
pages: {
|
||||||
// get() - Queries the API for GET /pages
|
// get() - Queries the API for GET /pages
|
||||||
get: function(){
|
get: function() {
|
||||||
return query("/pages/", "GET", {})
|
return query("/pages/", "GET", {})
|
||||||
},
|
},
|
||||||
// post() - Posts a campaign to POST /pages
|
// post() - Posts a campaign to POST /pages
|
||||||
post: function(page){
|
post: function(page) {
|
||||||
return query("/pages/", "POST", page)
|
return query("/pages/", "POST", page)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// templateId contains the endpoints for /templates/:id
|
// templateId contains the endpoints for /templates/:id
|
||||||
pageId : {
|
pageId: {
|
||||||
// get() - Queries the API for GET /templates/:id
|
// get() - Queries the API for GET /templates/:id
|
||||||
get: function(id){
|
get: function(id) {
|
||||||
return query("/pages/" + id, "GET", {})
|
return query("/pages/" + id, "GET", {})
|
||||||
},
|
},
|
||||||
// put() - Puts a campaign to PUT /templates/:id
|
// put() - Puts a campaign to PUT /templates/:id
|
||||||
put: function (page){
|
put: function(page) {
|
||||||
return query("/pages/" + page.id, "PUT", page)
|
return query("/pages/" + page.id, "PUT", page)
|
||||||
},
|
},
|
||||||
// delete() - Deletes a campaign at DELETE /templates/:id
|
// delete() - Deletes a campaign at DELETE /templates/:id
|
||||||
delete: function(id){
|
delete: function(id) {
|
||||||
return query("/pages/" + id, "DELETE", {})
|
return query("/pages/" + id, "DELETE", {})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// import handles all of the "import" functions in the api
|
// import handles all of the "import" functions in the api
|
||||||
import_email : function(raw) {
|
import_email: function(raw) {
|
||||||
return query("/import/email", "POST", {})
|
return query("/import/email", "POST", {})
|
||||||
},
|
},
|
||||||
clone_site : function(req){
|
clone_site: function(req) {
|
||||||
return query("/import/site", "POST", req)
|
return query("/import/site", "POST", req)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register our moment.js datatables listeners
|
// Register our moment.js datatables listeners
|
||||||
$(document).ready(function(){
|
$(document).ready(function() {
|
||||||
$.fn.dataTable.moment('MMMM Do YYYY, h:mm:ss a');
|
$.fn.dataTable.moment('MMMM Do YYYY, h:mm:ss a');
|
||||||
// Setup tooltips
|
// Setup tooltips
|
||||||
$('[data-toggle="tooltip"]').tooltip()
|
$('[data-toggle="tooltip"]').tooltip()
|
||||||
});
|
});
|
||||||
|
|
File diff suppressed because one or more lines are too long
|
@ -30,22 +30,11 @@
|
||||||
<h1 class="page-header" id="page-title">Results for campaign.name</h1>
|
<h1 class="page-header" id="page-title">Results for campaign.name</h1>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<!--
|
<button type="button" id="exportButton" class="btn btn-primary" onclick="exportAsCSV()">
|
||||||
<div class="btn-group">
|
<i class="fa fa-file-excel-o"></i> Export CSV
|
||||||
<button type="button" class="btn btn-primary dropdown-toggle" data-toggle="dropdown">
|
</button>
|
||||||
<i class="fa fa-cogs fa-lg"></i>
|
<button type="button" class="btn btn-danger" data-toggle="tooltip" onclick="deleteCampaign()">
|
||||||
<span class="caret"></span>
|
<i class="fa fa-trash-o fa-lg"></i> Delete
|
||||||
</button>
|
|
||||||
<ul class="dropdown-menu">
|
|
||||||
<li><a href="#">Export</a>
|
|
||||||
</li>
|
|
||||||
<li><a href="#">Relaunch</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
-->
|
|
||||||
<button type="button" class="btn btn-danger" data-toggle="tooltip" data-placement="right" title="Delete Campaign" onclick="deleteCampaign()">
|
|
||||||
<i class="fa fa-times fa-lg"></i>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<br />
|
<br />
|
||||||
|
@ -114,5 +103,6 @@
|
||||||
<script src="/js/d3.min.js"></script>
|
<script src="/js/d3.min.js"></script>
|
||||||
<script src="/js/topojson.min.js"></script>
|
<script src="/js/topojson.min.js"></script>
|
||||||
<script src="/js/datamaps.min.js"></script>
|
<script src="/js/datamaps.min.js"></script>
|
||||||
|
<script src="/js/papaparse.min.js"></script>
|
||||||
<script src="/js/app/campaign_results.js"></script>
|
<script src="/js/app/campaign_results.js"></script>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
|
@ -47,7 +47,7 @@
|
||||||
<th>Name</th>
|
<th>Name</th>
|
||||||
<th>Created Date</th>
|
<th>Created Date</th>
|
||||||
<th>Status</th>
|
<th>Status</th>
|
||||||
<th class="col-md-2"></th>
|
<th class="col-md-2 no-sort"></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
@ -113,7 +113,7 @@
|
||||||
<table id="groupTable" class="table table-hover table-striped table-condensed">
|
<table id="groupTable" class="table table-hover table-striped table-condensed">
|
||||||
<thead>
|
<thead>
|
||||||
<th>Group Name</th>
|
<th>Group Name</th>
|
||||||
<th></th>
|
<th class="no-sort"></th>
|
||||||
<tbody>
|
<tbody>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
@ -121,7 +121,7 @@
|
||||||
</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-default" data-dismiss="modal">Close</button>
|
||||||
<button type="button" class="btn btn-primary" onclick="save()">Save changes</button>
|
<button type="button" class="btn btn-primary" onclick="launch()"><i class="fa fa-envelope"></i> Launch Campaign</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -67,7 +67,7 @@
|
||||||
<th>Name</th>
|
<th>Name</th>
|
||||||
<th>Created Date</th>
|
<th>Created Date</th>
|
||||||
<th>Status</th>
|
<th>Status</th>
|
||||||
<th class="col-md-2 col-sm-2"></th>
|
<th class="col-md-2 col-sm-2 no-sort"></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
|
|
@ -44,7 +44,7 @@
|
||||||
<tr>
|
<tr>
|
||||||
<th>Name</th>
|
<th>Name</th>
|
||||||
<th>Last Modified Date</th>
|
<th>Last Modified Date</th>
|
||||||
<th class="col-md-2"></th>
|
<th class="col-md-2 no-sort"></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
|
|
@ -46,7 +46,7 @@
|
||||||
<tr>
|
<tr>
|
||||||
<th>Name</th>
|
<th>Name</th>
|
||||||
<th>Modified Date</th>
|
<th>Modified Date</th>
|
||||||
<th class="col-md-2"></th>
|
<th class="col-md-2 no-sort"></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
@ -102,11 +102,11 @@
|
||||||
<table id="attachmentsTable" class="table">
|
<table id="attachmentsTable" class="table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th class="col-md-1"></th>
|
<th class="col-md-1 no-sort"></th>
|
||||||
<th class="col-md-10">Name</th>
|
<th class="col-md-10">Name</th>
|
||||||
<th class="col-md-1"></th>
|
<th class="col-md-1 no-sort"></th>
|
||||||
<th class="datatable_hidden">Content</th>
|
<th class="datatable_hidden no-sort">Content</th>
|
||||||
<th class="datatable_hidden">Type</th>
|
<th class="datatable_hidden no-sort">Type</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
|
|
@ -47,7 +47,7 @@
|
||||||
<th>Name</th>
|
<th>Name</th>
|
||||||
<th>Members</th>
|
<th>Members</th>
|
||||||
<th>Modified Date</th>
|
<th>Modified Date</th>
|
||||||
<th class="col-md-2"></th>
|
<th class="col-md-2 no-sort"></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
@ -94,20 +94,20 @@
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<br />
|
<br />
|
||||||
<table id="targetsTable" class="table table-hover table-striped">
|
<table id="targetsTable" class="table table-hover table-striped table-condensed">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>First Name</th>
|
<th>First Name</th>
|
||||||
<th>Last Name</th>
|
<th>Last Name</th>
|
||||||
<th>Email</th>
|
<th>Email</th>
|
||||||
<th>Position</th>
|
<th>Position</th>
|
||||||
<th></th>
|
<th class="no-sort"></th>
|
||||||
<tbody>
|
<tbody>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button type="button" class="btn btn-default" onclick="dismiss()">Close</button>
|
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
|
||||||
<button type="button" class="btn btn-primary" id="modalSubmit">Save changes</button>
|
<button type="button" class="btn btn-primary" id="modalSubmit">Save changes</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -0,0 +1,28 @@
|
||||||
|
/*
|
||||||
|
gophish - Open-Source Phishing Framework
|
||||||
|
|
||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2013 Jordan Wright
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Package util provides misc. utility functions for gophish
|
||||||
|
package util
|
|
@ -0,0 +1,28 @@
|
||||||
|
/*
|
||||||
|
gophish - Open-Source Phishing Framework
|
||||||
|
|
||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2013 Jordan Wright
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Package worker contains the functionality for the background worker process.
|
||||||
|
package worker
|
|
@ -9,8 +9,8 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"text/template"
|
"text/template"
|
||||||
|
|
||||||
"github.com/jordan-wright/email"
|
|
||||||
"github.com/gophish/gophish/models"
|
"github.com/gophish/gophish/models"
|
||||||
|
"github.com/jordan-wright/email"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Logger is the logger for the worker
|
// Logger is the logger for the worker
|
||||||
|
@ -119,6 +119,10 @@ func processCampaign(c *models.Campaign) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
Logger.Println(err)
|
Logger.Println(err)
|
||||||
}
|
}
|
||||||
|
err = c.AddEvent(models.Event{Email: t.Email, Message: models.EVENT_SENT})
|
||||||
|
if err != nil {
|
||||||
|
Logger.Println(err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
err = c.UpdateStatus(models.CAMPAIGN_EMAILS_SENT)
|
err = c.UpdateStatus(models.CAMPAIGN_EMAILS_SENT)
|
||||||
|
|
Loading…
Reference in New Issue