From b75660ec1d885675d99b8d6712f43f9157a06405 Mon Sep 17 00:00:00 2001
From: ivan <ivan@opensupports.com>
Date: Tue, 10 Jan 2017 16:45:58 -0300
Subject: [PATCH 01/21] Max Red - Created first layout and functionality of
 ToggleList component [skip ci]

---
 client/src/app-components/toggle-list.js   | 52 ++++++++++++++++++++++
 client/src/app-components/toggle-list.scss | 20 +++++++++
 2 files changed, 72 insertions(+)
 create mode 100644 client/src/app-components/toggle-list.js
 create mode 100644 client/src/app-components/toggle-list.scss

diff --git a/client/src/app-components/toggle-list.js b/client/src/app-components/toggle-list.js
new file mode 100644
index 00000000..6df36dbe
--- /dev/null
+++ b/client/src/app-components/toggle-list.js
@@ -0,0 +1,52 @@
+import React from 'react';
+import _ from 'lodash';
+import classNames from 'classnames';
+
+class ToggleList extends React.Component {
+    static propTypes = {
+        items: React.PropTypes.arrayOf(React.PropTypes.shape({
+            content: React.PropTypes.node
+        }))
+    };
+
+    state = {
+        selected: [1, 3]
+    };
+    
+    render() {
+        return (
+            <div className="toggle-list">
+                {this.props.items.map(this.renderItem.bind(this))}
+            </div>
+        );
+    }
+
+    renderItem(obj, index) {
+
+        return (
+            <div className={this.getItemClass(index)} onClick={this.selectItem.bind(this, index)} key={index}>
+                {obj.content}
+            </div>
+        );
+    }
+
+    getItemClass(index) {
+        let classes = {
+            'toggle-list__item': true,
+            'toggle-list__first-item': (index === 0),
+            'toggle-list__selected': (_.includes(this.state.selected, index))
+        };
+
+        return classNames(classes);
+    }
+
+    selectItem(index) {
+        let actual = _.clone(this.state.selected);
+        (_.includes(this.state.selected, index)) ? (_.remove(actual, t => t == index)) : (actual.push(index));
+        this.setState({
+            selected: actual
+        });
+    }
+}
+
+export default ToggleList;
diff --git a/client/src/app-components/toggle-list.scss b/client/src/app-components/toggle-list.scss
new file mode 100644
index 00000000..b9d73082
--- /dev/null
+++ b/client/src/app-components/toggle-list.scss
@@ -0,0 +1,20 @@
+@import "../scss/vars";
+
+.toggle-list {
+
+    &__item {
+        border: 1px $light-grey solid;
+        border-left: none;
+        width: 120px;
+        height: 120px;
+        display: inline-block;
+    }
+
+    &__selected {
+        background-color: $light-grey;
+    }
+
+    &__first-item {
+        border: 1px $light-grey solid;
+    }
+}
\ No newline at end of file

From 6fc460d4b6c866fa909d3d4e023bb2bd118cfcc3 Mon Sep 17 00:00:00 2001
From: ivan <ivan@opensupports.com>
Date: Tue, 10 Jan 2017 18:27:53 -0300
Subject: [PATCH 02/21] Max Red - Modified some things, I don't know which
 [skip ci]

---
 client/src/app-components/toggle-list.js   | 16 ++++++++++++++--
 client/src/app-components/toggle-list.scss |  5 +++--
 2 files changed, 17 insertions(+), 4 deletions(-)

diff --git a/client/src/app-components/toggle-list.js b/client/src/app-components/toggle-list.js
index 6df36dbe..f90a26ae 100644
--- a/client/src/app-components/toggle-list.js
+++ b/client/src/app-components/toggle-list.js
@@ -6,7 +6,8 @@ class ToggleList extends React.Component {
     static propTypes = {
         items: React.PropTypes.arrayOf(React.PropTypes.shape({
             content: React.PropTypes.node
-        }))
+        })),
+        onChange: React.PropTypes.func
     };
 
     state = {
@@ -42,10 +43,21 @@ class ToggleList extends React.Component {
 
     selectItem(index) {
         let actual = _.clone(this.state.selected);
-        (_.includes(this.state.selected, index)) ? (_.remove(actual, t => t == index)) : (actual.push(index));
+
+        _.includes(this.state.selected, index) ? _.remove(actual, t => t == index) : actual.push(index);
+
+        console.log(actual);
         this.setState({
             selected: actual
         });
+
+        if (this.props.onChange) {
+            this.props.onChange({
+                target: {
+                    value: actual
+                }
+            });
+        }
     }
 }
 
diff --git a/client/src/app-components/toggle-list.scss b/client/src/app-components/toggle-list.scss
index b9d73082..7bf244cb 100644
--- a/client/src/app-components/toggle-list.scss
+++ b/client/src/app-components/toggle-list.scss
@@ -5,9 +5,10 @@
     &__item {
         border: 1px $light-grey solid;
         border-left: none;
-        width: 120px;
+        width: 180px;
         height: 120px;
         display: inline-block;
+        transition: background-color 0.4s ease;
     }
 
     &__selected {
@@ -17,4 +18,4 @@
     &__first-item {
         border: 1px $light-grey solid;
     }
-}
\ No newline at end of file
+}

From 845939f08981154810ee5bcea042530ea03ac94d Mon Sep 17 00:00:00 2001
From: AntonyAntonio <guillermo@opensupports.com>
Date: Wed, 11 Jan 2017 04:25:26 -0300
Subject: [PATCH 03/21] Guillermo -   stats architecture [skip ci]

---
 server/controllers/system.php               |  2 ++
 server/controllers/system/edit-settings.php |  1 -
 server/controllers/system/get-stats.php     | 34 +++++++++++++++++++++
 server/controllers/system/init-settings.php |  3 +-
 server/models/Log.php                       |  9 ++++--
 server/models/Staff.php                     |  3 +-
 server/models/Stat.php                      | 17 +++++++++++
 7 files changed, 63 insertions(+), 6 deletions(-)
 create mode 100644 server/controllers/system/get-stats.php
 create mode 100644 server/models/Stat.php

diff --git a/server/controllers/system.php b/server/controllers/system.php
index 2cde4fe2..534f556e 100644
--- a/server/controllers/system.php
+++ b/server/controllers/system.php
@@ -9,6 +9,7 @@ require_once 'system/get-logs.php';
 require_once 'system/get-mail-templates.php';
 require_once 'system/edit-mail-template.php';
 require_once 'system/recover-mail-template.php';
+require_once 'system/get-stats.php';
 
 $systemControllerGroup = new ControllerGroup();
 $systemControllerGroup->setGroupPath('/system');
@@ -23,5 +24,6 @@ $systemControllerGroup->addController(new GetLogsController);
 $systemControllerGroup->addController(new GetMailTemplatesController);
 $systemControllerGroup->addController(new EditMailTemplateController);
 $systemControllerGroup->addController(new RecoverMailTemplateController);
+$systemControllerGroup->addController(new GetStatsController);
 
 $systemControllerGroup->finalize();
\ No newline at end of file
diff --git a/server/controllers/system/edit-settings.php b/server/controllers/system/edit-settings.php
index c78f1f97..22454e62 100644
--- a/server/controllers/system/edit-settings.php
+++ b/server/controllers/system/edit-settings.php
@@ -1,5 +1,4 @@
 <?php
-use Respect\Validation\Validator as DataValidator;
 
 class EditSettingsController extends Controller {
     const PATH = '/edit-settings';
diff --git a/server/controllers/system/get-stats.php b/server/controllers/system/get-stats.php
new file mode 100644
index 00000000..d8349663
--- /dev/null
+++ b/server/controllers/system/get-stats.php
@@ -0,0 +1,34 @@
+<?php
+class GetStatsController extends Controller {
+    const PATH = '/get-stats';
+
+    public function validations() {
+        return [
+            'permission' => 'staff_1',
+            'requestData' => []
+        ];
+    }
+
+    public function handler() {
+        $begin = new DateTime(Setting::getSetting('last-stat-day')->value);
+        $end = new DateTime(Date::getCurrentDate());
+
+        $interval = new DateInterval('P1D');
+        $dateRange = new DatePeriod($begin, $interval ,$end);
+
+        foreach($dateRange as $date){
+            $numberOfTickets = Log::count('type=? AND date LIKE ?',['CREATE_TICKET', $date->format('Ymd') . '%']);
+            $stat = new Stat();
+
+            $stat->setProperties([
+                'date' => $date->format('Ymd'),
+                'type' => 'CREATE_TICKET',
+                'general' => 1,
+                'value' => $numberOfTickets,
+            ]);
+            $stat->store();
+        }
+
+        Response::respondSuccess();
+    }
+}
\ No newline at end of file
diff --git a/server/controllers/system/init-settings.php b/server/controllers/system/init-settings.php
index 98fce211..89842b5c 100644
--- a/server/controllers/system/init-settings.php
+++ b/server/controllers/system/init-settings.php
@@ -40,7 +40,8 @@ class InitSettingsController extends Controller {
             'allow-attachments' => 0,
             'max-size' => 0,
             'title' => 'Support Center',
-            'url' => 'http://www.opensupports.com/support'
+            'url' => 'http://www.opensupports.com/support',
+            'last-stat-day' => '20170101'//TODO: get current date
         ]);
     }
 
diff --git a/server/models/Log.php b/server/models/Log.php
index ebe24469..966f3b3a 100644
--- a/server/models/Log.php
+++ b/server/models/Log.php
@@ -9,7 +9,8 @@ class Log extends DataStore {
             'type',
             'authorUser',
             'authorStaff',
-            'to'
+            'to',
+            'date'
         ];
     }
 
@@ -22,7 +23,8 @@ class Log extends DataStore {
 
         $log->setProperties(array(
             'type' => $type,
-            'to' => $to
+            'to' => $to,
+            'date' => Date::getCurrentDate()
         ));
 
         if($author instanceof User) {
@@ -44,7 +46,8 @@ class Log extends DataStore {
                 'name' => $author->name,
                 'id' => $author->id,
                 'staff' => $author instanceof Staff
-            ]
+            ],
+            'date' => $this->date 
         ];
     }
 }
\ No newline at end of file
diff --git a/server/models/Staff.php b/server/models/Staff.php
index cca0bc4c..9ec69341 100644
--- a/server/models/Staff.php
+++ b/server/models/Staff.php
@@ -18,7 +18,8 @@ class Staff extends DataStore {
             'level',
             'sharedDepartmentList',
             'sharedTicketList',
-            'lastLogin'
+            'lastLogin',
+            'sharedStatList'
         ];
     }
 
diff --git a/server/models/Stat.php b/server/models/Stat.php
new file mode 100644
index 00000000..08e90572
--- /dev/null
+++ b/server/models/Stat.php
@@ -0,0 +1,17 @@
+<?php
+class Stat extends DataStore {
+    const TABLE = 'stat';
+
+    public static function getProps() {
+        return array (
+            'date',
+            'type',
+            'general',
+            'value'
+        );
+    }
+
+    public function getDefaultProps() {
+        return array();
+    }
+}
\ No newline at end of file

From 868d08ecb235622893bbf479af490a1817525e17 Mon Sep 17 00:00:00 2001
From: AntonyAntonio <guillermo@opensupports.com>
Date: Thu, 12 Jan 2017 02:50:45 -0300
Subject: [PATCH 04/21] Guillermo -   stats architecture [skip ci]

---
 server/controllers/system/get-stats.php | 138 +++++++++++++++++++++---
 server/data/ERRORS.php                  |   1 +
 server/models/Staff.php                 |   5 +-
 server/models/Stat.php                  |   8 ++
 4 files changed, 134 insertions(+), 18 deletions(-)

diff --git a/server/controllers/system/get-stats.php b/server/controllers/system/get-stats.php
index d8349663..5c83c8d7 100644
--- a/server/controllers/system/get-stats.php
+++ b/server/controllers/system/get-stats.php
@@ -1,34 +1,140 @@
 <?php
+use Respect\Validation\Validator as DataValidator;
+
 class GetStatsController extends Controller {
     const PATH = '/get-stats';
 
     public function validations() {
         return [
             'permission' => 'staff_1',
-            'requestData' => []
+            'requestData' => [
+                'period' => [
+                    'validation' => DataValidator::in(['week', 'month', 'quarter', 'year']),
+                    'error' => ERRORS::INVALID_PERIOD
+                ]
+            ]
         ];
     }
 
     public function handler() {
-        $begin = new DateTime(Setting::getSetting('last-stat-day')->value);
-        $end = new DateTime(Date::getCurrentDate());
+        $this->generationNewStats();
 
-        $interval = new DateInterval('P1D');
-        $dateRange = new DatePeriod($begin, $interval ,$end);
+        $staffId = Controller::request('staffId');
 
-        foreach($dateRange as $date){
-            $numberOfTickets = Log::count('type=? AND date LIKE ?',['CREATE_TICKET', $date->format('Ymd') . '%']);
-            $stat = new Stat();
+        if($staffId) {
+            if($staffId !== Controller::getLoggedUser()->id && !Controller::isStaffLogged(3)) {
+                Response::respondError(ERRORS::NO_PERMISSION);
+                return;
+            }
 
-            $stat->setProperties([
-                'date' => $date->format('Ymd'),
-                'type' => 'CREATE_TICKET',
-                'general' => 1,
-                'value' => $numberOfTickets,
-            ]);
-            $stat->store();
+            $this->getStaffStat();
+        } else {
+            $this->getGeneralStat();
+        }
+    }
+
+    public function generationNewStats() {
+        $lastStatDay = Setting::getSetting('last-stat-day');
+        $previousCurrentDate = floor(Date::getCurrentDate()/10000)-1;
+
+        if($lastStatDay->value !== $previousCurrentDate) {
+
+            $begin = new DateTime($lastStatDay->value);
+            $end = new DateTime($previousCurrentDate);
+
+            $interval = new DateInterval('P1D');
+            $dateRange = new DatePeriod($begin, $interval ,$end);
+
+            $staffList = Staff::getAll();
+
+            foreach($dateRange as $date) {
+                $this->generateGeneralStat('CREATE_TICKET', $date);
+                $this->generateGeneralStat('CLOSE', $date);
+                $this->generateGeneralStat('SIGNUP', $date);
+                $this->generateGeneralStat('COMMENT', $date);
+
+                foreach($staffList as $staff) {
+                    $assignments = Ticketevent::count('type=? AND author_staff_id=? AND date LIKE ?',['ASSIGN',$staff->id, $date->format('Ymd') . '%']);
+                    $closed = Ticketevent::count('type=? AND author_staff_id=? AND date LIKE ?',['CLOSE',$staff->id, $date->format('Ymd') . '%']);
+
+                    $statAssign = new Stat();
+                    $statAssign->setProperties([
+                        'date' => $date->format('Ymd'),
+                        'type' => 'ASSIGN',
+                        'general' => 0,
+                        'value' => $assignments,
+                    ]);
+
+                    $statClose = new Stat();
+                    $statClose->setProperties([
+                        'date' => $date->format('Ymd'),
+                        'type' => 'CLOSE',
+                        'general' => 0,
+                        'value' => $closed,
+                    ]);
+
+                    $staff->ownStatList->add($statAssign);
+                    $staff->ownStatList->add($statClose);
+
+                    $staff->store();
+                }
+            }
+
+            $lastStatDay->value = $previousCurrentDate;
+            $lastStatDay->store();
+        }
+    }
+
+    public function generateGeneralStat($type, $date) {
+        $value = Log::count('type=? AND date LIKE ?',[$type, $date->format('Ymd') . '%']);
+        $stat = new Stat();
+
+        $stat->setProperties([
+            'date' => $date->format('Ymd'),
+            'type' => $type,
+            'general' => 1,
+            'value' => $value,
+        ]);
+
+        $stat->store();
+    }
+
+    public  function getGeneralStat() {
+        $daysToRetrieve = $this->getDaysToRetrieve();
+
+        $statList = Stat::find('general=\'1\' ORDER BY id desc LIMIT ? ', [4 * $daysToRetrieve]);
+
+        Response::respondSuccess($statList->toArray());
+    }
+
+    public function getStaffStat() {
+        $staffId = Controller::request('staffId');
+        $daysToRetrieve = $this->getDaysToRetrieve();
+
+        $statList = Stat::find('general=\'0\' AND staff_id=? ORDER BY id desc LIMIT ? ', [$staffId, 4 * $daysToRetrieve]);
+
+        Response::respondSuccess($statList->toArray());
+    }
+
+    public function getDaysToRetrieve() {
+        $period = Controller::request('period');
+        $daysToRetrieve = 0;
+
+        switch ($period) {
+            case 'week':
+                $daysToRetrieve = 7;
+                break;
+            case 'month':
+                $daysToRetrieve = 30;
+                break;
+            case 'quarter':
+                $daysToRetrieve = 90;
+                break;
+            case 'year':
+                $daysToRetrieve = 365;
+                break;
         }
 
-        Response::respondSuccess();
+        return $daysToRetrieve;
     }
 }
\ No newline at end of file
diff --git a/server/data/ERRORS.php b/server/data/ERRORS.php
index e0052543..4b7507cf 100644
--- a/server/data/ERRORS.php
+++ b/server/data/ERRORS.php
@@ -35,4 +35,5 @@ class ERRORS {
     const INVALID_TEMPLATE = 'INVALID_TEMPLATE';
     const INVALID_SUBJECT = 'INVALID_SUBJECT';
     const INVALID_BODY = 'INVALID_BODY';
+    const INVALID_PERIOD = 'INVALID_PERIOD';
 }
diff --git a/server/models/Staff.php b/server/models/Staff.php
index 9ec69341..13c3d7a8 100644
--- a/server/models/Staff.php
+++ b/server/models/Staff.php
@@ -19,13 +19,14 @@ class Staff extends DataStore {
             'sharedDepartmentList',
             'sharedTicketList',
             'lastLogin',
-            'sharedStatList'
+            'ownStatList'
         ];
     }
 
     public function getDefaultProps() {
         return [
-            'level' => 1
+            'level' => 1,
+            'ownStatList' => new DataStoreList()
         ];
     }
 
diff --git a/server/models/Stat.php b/server/models/Stat.php
index 08e90572..97f85649 100644
--- a/server/models/Stat.php
+++ b/server/models/Stat.php
@@ -14,4 +14,12 @@ class Stat extends DataStore {
     public function getDefaultProps() {
         return array();
     }
+    public function toArray() {
+        return [
+            'date' => $this->date,
+            'type' => $this->type,
+            'general' => $this->general,
+            'value' => $this->value
+        ];
+    }
 }
\ No newline at end of file

From be4999009184e8c88f2d106ecf86d8d75e930ecd Mon Sep 17 00:00:00 2001
From: ivan <ivan@opensupports.com>
Date: Thu, 12 Jan 2017 03:02:56 -0300
Subject: [PATCH 05/21] Ivan - Add files folder [skip ci]

---
 server/files/.gitkeep | 0
 1 file changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 server/files/.gitkeep

diff --git a/server/files/.gitkeep b/server/files/.gitkeep
new file mode 100644
index 00000000..e69de29b

From b8d24b601dc2986dec74034a6d8cb59ccf0137b7 Mon Sep 17 00:00:00 2001
From: AntonyAntonio <guillermo@opensupports.com>
Date: Thu, 12 Jan 2017 16:30:58 -0300
Subject: [PATCH 06/21] Guillermo -   stats architecture [skip ci]

---
 server/controllers/system/get-stats.php |   7 +-
 server/libs/Date.php                    |   4 +
 tests/init.rb                           |   2 +
 tests/libs.rb                           |   4 +
 tests/system/get-stats.rb               | 250 ++++++++++++++++++++++++
 5 files changed, 264 insertions(+), 3 deletions(-)
 create mode 100644 tests/system/get-stats.rb

diff --git a/server/controllers/system/get-stats.php b/server/controllers/system/get-stats.php
index 5c83c8d7..ff1fdf3a 100644
--- a/server/controllers/system/get-stats.php
+++ b/server/controllers/system/get-stats.php
@@ -35,12 +35,13 @@ class GetStatsController extends Controller {
 
     public function generationNewStats() {
         $lastStatDay = Setting::getSetting('last-stat-day');
-        $previousCurrentDate = floor(Date::getCurrentDate()/10000)-1;
+        $previousCurrentDate = floor(Date::getPreviousDate() / 10000);
+        $currentDate = floor(Date::getCurrentDate() / 10000);
 
         if($lastStatDay->value !== $previousCurrentDate) {
 
             $begin = new DateTime($lastStatDay->value);
-            $end = new DateTime($previousCurrentDate);
+            $end = new DateTime($currentDate);
 
             $interval = new DateInterval('P1D');
             $dateRange = new DatePeriod($begin, $interval ,$end);
@@ -80,7 +81,7 @@ class GetStatsController extends Controller {
                 }
             }
 
-            $lastStatDay->value = $previousCurrentDate;
+            $lastStatDay->value = $currentDate;
             $lastStatDay->store();
         }
     }
diff --git a/server/libs/Date.php b/server/libs/Date.php
index c3e8ba0c..6921789f 100644
--- a/server/libs/Date.php
+++ b/server/libs/Date.php
@@ -3,4 +3,8 @@ class Date {
     public static function getCurrentDate() {
         return date('YmdHi');
     }
+
+    public static function getPreviousDate() {
+        return date('YmdHi', strtotime(' -1 day '));
+    }
 }
diff --git a/tests/init.rb b/tests/init.rb
index 3eb51877..b017ab09 100644
--- a/tests/init.rb
+++ b/tests/init.rb
@@ -5,6 +5,7 @@ require 'uri'
 require 'mysql'
 require 'json'
 require 'mechanize'
+require 'date'
 require './libs.rb'
 require './scripts.rb'
 
@@ -51,3 +52,4 @@ require './staff/last-events.rb'
 require './system/get-mail-templates.rb'
 require './system/edit-mail-template.rb'
 require './system/recover-mail-template.rb'
+require './system/get-stats.rb'
diff --git a/tests/libs.rb b/tests/libs.rb
index f5496d48..8b832429 100644
--- a/tests/libs.rb
+++ b/tests/libs.rb
@@ -29,6 +29,10 @@ class Database
 
         return queryResponse.fetch_hash
     end
+
+    def query(query_string)
+        return @connection.query(query_string);
+    end
 end
 
 $database = Database.new
diff --git a/tests/system/get-stats.rb b/tests/system/get-stats.rb
new file mode 100644
index 00000000..28f79541
--- /dev/null
+++ b/tests/system/get-stats.rb
@@ -0,0 +1,250 @@
+describe'/system/get-stats' do
+    request('/user/logout')
+    Scripts.login($staff[:email], $staff[:password], true)
+
+    it 'should get stats' do
+
+        d = Date.today.prev_day
+        yesterday = d.strftime("%Y%m%d%H%M")
+        d = Date.today.prev_day.prev_day
+        yesterday2 = d.strftime("%Y%m%d%H%M")
+        d = Date.today.prev_day.prev_day.prev_day
+        yesterday3 = d.strftime("%Y%m%d%H%M")
+
+        #day 1
+        for i in 0..5
+            $database.query("INSERT INTO log VALUES('', 'SIGNUP', NULL, " + yesterday3 + ", NULL, NULL);")
+        end
+        for i in 0..0
+            $database.query("INSERT INTO log VALUES('', 'CREATE_TICKET', NULL, " + yesterday3 + ", NULL, NULL);")
+        end
+        for i in 0..1
+            $database.query("INSERT INTO log VALUES('', 'CLOSE', NULL, " + yesterday3 + ", NULL, NULL);")
+        end
+        for i in 0..2
+            $database.query("INSERT INTO log VALUES('', 'COMMENT', NULL, " + yesterday3 + ", NULL, NULL);")
+        end
+        for i in 0..8
+            $database.query("INSERT INTO ticketevent VALUES('', 'CLOSE', NULL, " + yesterday3 + ", NULL, NULL, 1);")
+        end
+        for i in 0..4
+            $database.query("INSERT INTO ticketevent VALUES('', 'ASSIGN', NULL, " + yesterday3 + ", NULL, NULL, 1);")
+        end
+
+        #day 2
+        for i in 0..7
+            $database.query("INSERT INTO log VALUES('', 'SIGNUP', NULL, " + yesterday2 + ", NULL, NULL);")
+        end
+        for i in 0..2
+            $database.query("INSERT INTO log VALUES('', 'CREATE_TICKET', NULL, " + yesterday2 + ", NULL, NULL);")
+        end
+        for i in 0..9
+            $database.query("INSERT INTO log VALUES('', 'CLOSE', NULL, " + yesterday2 + ", NULL, NULL);")
+        end
+        for i in 0..2
+            $database.query("INSERT INTO log VALUES('', 'COMMENT', NULL, " + yesterday2 + ", NULL, NULL);")
+        end
+        for i in 0..10
+            $database.query("INSERT INTO ticketevent VALUES('', 'CLOSE', NULL, " + yesterday2 + ", NULL, NULL, 1);")
+        end
+        for i in 0..2
+            $database.query("INSERT INTO ticketevent VALUES('', 'ASSIGN', NULL, " + yesterday2 + ", NULL, NULL, 1);")
+        end
+
+        #day 3
+        for i in 0..0
+            $database.query("INSERT INTO log VALUES('', 'SIGNUP', NULL, " + yesterday + ", NULL, NULL);")
+        end
+        for i in 0..1
+            $database.query("INSERT INTO log VALUES('', 'CREATE_TICKET', NULL, " + yesterday + ", NULL, NULL);")
+        end
+        for i in 0..4
+            $database.query("INSERT INTO log VALUES('', 'CLOSE', NULL, " + yesterday + ", NULL, NULL);")
+        end
+        for i in 0..7
+            $database.query("INSERT INTO log VALUES('', 'COMMENT', NULL, " + yesterday + ", NULL, NULL);")
+        end
+        for i in 0..3
+            $database.query("INSERT INTO ticketevent VALUES('', 'CLOSE', NULL, " + yesterday + ", NULL, NULL, 1);")
+        end
+        for i in 0..7
+            $database.query("INSERT INTO ticketevent VALUES('', 'ASSIGN', NULL, " + yesterday + ", NULL, NULL, 1);")
+        end
+
+        result= request('/system/get-stats', {
+            csrf_userid: $csrf_userid,
+            csrf_token: $csrf_token,
+            period: 'week'
+        })
+
+        (result['status']).should.equal('success')
+
+        row = $database.getRow('stat', 65, 'id')
+
+        (row['date']).should.equal('20170109')
+        (row['type']).should.equal('CREATE_TICKET')
+        (row['general']).should.equal('1')
+        (row['value']).should.equal('1')
+
+        row = $database.getRow('stat', 66, 'id')
+
+        (row['date']).should.equal('20170109')
+        (row['type']).should.equal('CLOSE')
+        (row['general']).should.equal('1')
+        (row['value']).should.equal('2')
+
+        row = $database.getRow('stat', 67, 'id')
+
+        (row['date']).should.equal('20170109')
+        (row['type']).should.equal('SIGNUP')
+        (row['general']).should.equal('1')
+        (row['value']).should.equal('6')
+
+        row = $database.getRow('stat', 68, 'id')
+
+        (row['date']).should.equal('20170109')
+        (row['type']).should.equal('COMMENT')
+        (row['general']).should.equal('1')
+        (row['value']).should.equal('3')
+
+        row = $database.getRow('stat', 69, 'id')
+
+        (row['date']).should.equal('20170109')
+        (row['type']).should.equal('ASSIGN')
+        (row['general']).should.equal('0')
+        (row['value']).should.equal('5')
+
+        row = $database.getRow('stat', 70, 'id')
+
+        (row['date']).should.equal('20170109')
+        (row['type']).should.equal('CLOSE')
+        (row['general']).should.equal('0')
+        (row['value']).should.equal('9')
+
+        row = $database.getRow('stat', 71, 'id')
+
+        (row['date']).should.equal('20170109')
+        (row['type']).should.equal('ASSIGN')
+        (row['general']).should.equal('0')
+        (row['value']).should.equal('0')
+
+        row = $database.getRow('stat', 72, 'id')
+
+        (row['date']).should.equal('20170109')
+        (row['type']).should.equal('CLOSE')
+        (row['general']).should.equal('0')
+        (row['value']).should.equal('0')
+
+        row = $database.getRow('stat', 73, 'id')
+
+        (row['date']).should.equal('20170110')
+        (row['type']).should.equal('CREATE_TICKET')
+        (row['general']).should.equal('1')
+        (row['value']).should.equal('3')
+
+        row = $database.getRow('stat', 74, 'id')
+
+        (row['date']).should.equal('20170110')
+        (row['type']).should.equal('CLOSE')
+        (row['general']).should.equal('1')
+        (row['value']).should.equal('10')
+
+        row = $database.getRow('stat', 75, 'id')
+
+        (row['date']).should.equal('20170110')
+        (row['type']).should.equal('SIGNUP')
+        (row['general']).should.equal('1')
+        (row['value']).should.equal('8')
+
+        row = $database.getRow('stat', 76, 'id')
+
+        (row['date']).should.equal('20170110')
+        (row['type']).should.equal('COMMENT')
+        (row['general']).should.equal('1')
+        (row['value']).should.equal('3')
+
+        row = $database.getRow('stat', 77, 'id')
+
+        (row['date']).should.equal('20170110')
+        (row['type']).should.equal('ASSIGN')
+        (row['general']).should.equal('0')
+        (row['value']).should.equal('3')
+
+        row = $database.getRow('stat', 78, 'id')
+
+        (row['date']).should.equal('20170110')
+        (row['type']).should.equal('CLOSE')
+        (row['general']).should.equal('0')
+        (row['value']).should.equal('11')
+
+        row = $database.getRow('stat', 79, 'id')
+
+        (row['date']).should.equal('20170110')
+        (row['type']).should.equal('ASSIGN')
+        (row['general']).should.equal('0')
+        (row['value']).should.equal('0')
+
+        row = $database.getRow('stat', 80, 'id')
+
+        (row['date']).should.equal('20170110')
+        (row['type']).should.equal('CLOSE')
+        (row['general']).should.equal('0')
+        (row['value']).should.equal('0')
+
+        row = $database.getRow('stat', 81, 'id')
+
+        (row['date']).should.equal('20170111')
+        (row['type']).should.equal('CREATE_TICKET')
+        (row['general']).should.equal('1')
+        (row['value']).should.equal('2')
+
+        row = $database.getRow('stat', 82, 'id')
+
+        (row['date']).should.equal('20170111')
+        (row['type']).should.equal('CLOSE')
+        (row['general']).should.equal('1')
+        (row['value']).should.equal('5')
+
+        row = $database.getRow('stat', 83, 'id')
+
+        (row['date']).should.equal('20170111')
+        (row['type']).should.equal('SIGNUP')
+        (row['general']).should.equal('1')
+        (row['value']).should.equal('1')
+
+        row = $database.getRow('stat', 84, 'id')
+
+        (row['date']).should.equal('20170111')
+        (row['type']).should.equal('COMMENT')
+        (row['general']).should.equal('1')
+        (row['value']).should.equal('8')
+
+        row = $database.getRow('stat', 85, 'id')
+
+        (row['date']).should.equal('20170111')
+        (row['type']).should.equal('ASSIGN')
+        (row['general']).should.equal('0')
+        (row['value']).should.equal('8')
+
+        row = $database.getRow('stat', 86, 'id')
+
+        (row['date']).should.equal('20170111')
+        (row['type']).should.equal('CLOSE')
+        (row['general']).should.equal('0')
+        (row['value']).should.equal('4')
+
+        row = $database.getRow('stat', 87, 'id')
+
+        (row['date']).should.equal('20170111')
+        (row['type']).should.equal('ASSIGN')
+        (row['general']).should.equal('0')
+        (row['value']).should.equal('0')
+
+        row = $database.getRow('stat', 88, 'id')
+
+        (row['date']).should.equal('20170111')
+        (row['type']).should.equal('CLOSE')
+        (row['general']).should.equal('0')
+        (row['value']).should.equal('0')
+    end
+end

From 134540fed957efa2e6e03fb216fbfcb48691f1f0 Mon Sep 17 00:00:00 2001
From: ivan <ivan@opensupports.com>
Date: Thu, 12 Jan 2017 16:45:01 -0300
Subject: [PATCH 07/21] Ivan - Add Database Backup, FileDownloader,
 FileUploader and LinearCongruentialGenerator classes [skip ci]

---
 server/composer.json                          |  3 +-
 server/controllers/system.php                 |  4 ++
 server/controllers/system/backup-database.php | 30 +++++++++++++
 server/controllers/system/download.php        | 24 ++++++++++
 server/controllers/system/init-settings.php   |  5 ++-
 server/data/ERRORS.php                        |  1 +
 server/index.php                              |  4 ++
 server/libs/FileDownloader.php                | 40 +++++++++++++++++
 server/libs/FileManager.php                   | 26 +++++++++++
 server/libs/FileUploader.php                  | 44 +++++++++++++++++--
 server/libs/Hashing.php                       | 30 ++++++++++++-
 server/libs/LinearCongruentialGenerator.php   | 30 +++++++++++++
 server/models/Ticket.php                      | 15 +++----
 tests/ticket/create.rb                        |  8 ++--
 14 files changed, 246 insertions(+), 18 deletions(-)
 create mode 100644 server/controllers/system/backup-database.php
 create mode 100644 server/controllers/system/download.php
 create mode 100644 server/libs/FileDownloader.php
 create mode 100644 server/libs/FileManager.php
 create mode 100644 server/libs/LinearCongruentialGenerator.php

diff --git a/server/composer.json b/server/composer.json
index 71e09ba2..a506cdee 100644
--- a/server/composer.json
+++ b/server/composer.json
@@ -4,7 +4,8 @@
         "respect/validation": "^1.1",
         "phpmailer/phpmailer": "^5.2",
         "google/recaptcha": "~1.1",
-        "gabordemooij/redbean": "^4.3"
+        "gabordemooij/redbean": "^4.3",
+        "ifsnop/mysqldump-php": "2.*"
     },
     "require-dev": {
       "phpunit/phpunit": "5.0.*"
diff --git a/server/controllers/system.php b/server/controllers/system.php
index 2cde4fe2..e66ecc5d 100644
--- a/server/controllers/system.php
+++ b/server/controllers/system.php
@@ -9,6 +9,8 @@ require_once 'system/get-logs.php';
 require_once 'system/get-mail-templates.php';
 require_once 'system/edit-mail-template.php';
 require_once 'system/recover-mail-template.php';
+require_once 'system/backup-database.php';
+require_once 'system/download.php';
 
 $systemControllerGroup = new ControllerGroup();
 $systemControllerGroup->setGroupPath('/system');
@@ -23,5 +25,7 @@ $systemControllerGroup->addController(new GetLogsController);
 $systemControllerGroup->addController(new GetMailTemplatesController);
 $systemControllerGroup->addController(new EditMailTemplateController);
 $systemControllerGroup->addController(new RecoverMailTemplateController);
+$systemControllerGroup->addController(new BackupDatabaseController);
+$systemControllerGroup->addController(new DownloadController);
 
 $systemControllerGroup->finalize();
\ No newline at end of file
diff --git a/server/controllers/system/backup-database.php b/server/controllers/system/backup-database.php
new file mode 100644
index 00000000..82b1fccf
--- /dev/null
+++ b/server/controllers/system/backup-database.php
@@ -0,0 +1,30 @@
+<?php
+use Ifsnop\Mysqldump as IMysqldump;
+
+class BackupDatabaseController extends Controller {
+    const PATH = '/backup-database';
+
+    public function validations() {
+        return [
+            'permission' => 'staff_3',
+            'requestData' => []
+        ];
+    }
+
+    public function handler() {
+        global $mysql_host;
+        global $mysql_database;
+        global $mysql_user;
+        global $mysql_password;
+
+        $fileDownloader = FileDownloader::getInstance();
+        $fileDownloader->setFileName('backup.sql');
+
+        $mysqlDump = new IMysqldump\Mysqldump('mysql:host='. $mysql_host .';dbname=' . $mysql_database, $mysql_user, $mysql_password);
+        $mysqlDump->start($fileDownloader->getFullFilePath());
+
+        if($fileDownloader->download()) {
+            $fileDownloader->eraseFile();
+        }
+    }
+}
\ No newline at end of file
diff --git a/server/controllers/system/download.php b/server/controllers/system/download.php
new file mode 100644
index 00000000..0b481bf8
--- /dev/null
+++ b/server/controllers/system/download.php
@@ -0,0 +1,24 @@
+<?php
+use Ifsnop\Mysqldump as IMysqldump;
+use Respect\Validation\Validator as DataValidator;
+
+class DownloadController extends Controller {
+    const PATH = '/download';
+
+    public function validations() {
+        return [
+            'permission' => 'staff_1',
+            'requestData' => [
+                'file' => [
+                    'validation' => DataValidator::alnum('_.')->noWhitespace()
+                ]
+            ]
+        ];
+    }
+
+    public function handler() {
+        $fileDownloader = FileDownloader::getInstance();
+        $fileDownloader->setFileName(Controller::request('file'));
+        $fileDownloader->download();
+    }
+}
\ No newline at end of file
diff --git a/server/controllers/system/init-settings.php b/server/controllers/system/init-settings.php
index 98fce211..6613b17a 100644
--- a/server/controllers/system/init-settings.php
+++ b/server/controllers/system/init-settings.php
@@ -40,7 +40,10 @@ class InitSettingsController extends Controller {
             'allow-attachments' => 0,
             'max-size' => 0,
             'title' => 'Support Center',
-            'url' => 'http://www.opensupports.com/support'
+            'url' => 'http://www.opensupports.com/support',
+            'ticket-gap' => Hashing::generateRandomPrime(100000, 999999),
+            'file-gap' => Hashing::generateRandomPrime(100000, 999999),
+            'file-first-number' => Hashing::generateRandomNumber(100000, 999999),
         ]);
     }
 
diff --git a/server/data/ERRORS.php b/server/data/ERRORS.php
index e0052543..bdd16d22 100644
--- a/server/data/ERRORS.php
+++ b/server/data/ERRORS.php
@@ -35,4 +35,5 @@ class ERRORS {
     const INVALID_TEMPLATE = 'INVALID_TEMPLATE';
     const INVALID_SUBJECT = 'INVALID_SUBJECT';
     const INVALID_BODY = 'INVALID_BODY';
+    const INVALID_FILE = 'INVALID_FILE';
 }
diff --git a/server/index.php b/server/index.php
index a9056e1e..d8508ba3 100644
--- a/server/index.php
+++ b/server/index.php
@@ -18,6 +18,10 @@ include_once 'libs/Hashing.php';
 include_once 'libs/MailSender.php';
 include_once 'libs/Date.php';
 include_once 'libs/DataStoreList.php';
+include_once 'libs/LinearCongruentialGenerator.php';
+include_once 'libs/FileManager.php';
+include_once 'libs/FileDownloader.php';
+include_once 'libs/FileUploader.php';
 
 // LOAD DATA
 spl_autoload_register(function ($class) {
diff --git a/server/libs/FileDownloader.php b/server/libs/FileDownloader.php
new file mode 100644
index 00000000..6f49ca64
--- /dev/null
+++ b/server/libs/FileDownloader.php
@@ -0,0 +1,40 @@
+<?php
+
+class FileDownloader extends FileManager {
+
+    private static $instance = null;
+
+    public static function getInstance() {
+        if (self::$instance === null) {
+            self::$instance = new FileDownloader();
+        }
+
+        return self::$instance;
+    }
+
+    private function __construct() {}
+
+    public function download() {
+        $fullFilePath = $this->getFullFilePath();
+
+        if(file_exists($fullFilePath) && is_file($fullFilePath)) {
+            header('Cache-control: private');
+            header('Content-Type: application/octet-stream');
+            header('Content-Length: '.filesize($fullFilePath));
+            header('Content-Disposition: filename='. $this->getFileName());
+
+            flush();
+            $file = fopen($fullFilePath, 'r');
+            print fread($file, filesize($fullFilePath));
+            fclose($file);
+
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    public function eraseFile() {
+        unlink($this->getLocalPath() . $this->getFileName());
+    }
+}
\ No newline at end of file
diff --git a/server/libs/FileManager.php b/server/libs/FileManager.php
new file mode 100644
index 00000000..fd05bb13
--- /dev/null
+++ b/server/libs/FileManager.php
@@ -0,0 +1,26 @@
+<?php
+
+abstract class FileManager {
+    private $fileName;
+    private $localPath = 'files/';
+    
+    public function setLocalPath($localPath) {
+        $this->localPath = $localPath;
+    }
+
+    public function setFileName($fileName) {
+        $this->fileName = $fileName;
+    }
+
+    public function getLocalPath() {
+        return $this->localPath;
+    }
+
+    public function getFileName() {
+        return $this->fileName;
+    }
+    
+    public function getFullFilePath() {
+        return $this->getLocalPath() . $this->getFileName();
+    }
+}
\ No newline at end of file
diff --git a/server/libs/FileUploader.php b/server/libs/FileUploader.php
index 55eb9bbc..dfc9c09b 100644
--- a/server/libs/FileUploader.php
+++ b/server/libs/FileUploader.php
@@ -1,5 +1,10 @@
 <?php
-class FileUploader {
+
+class FileUploader extends FileManager {
+    private $maxSize = 1024;
+    private $linearCongruentialGenerator;
+    private $linearCongruentialGeneratorOffset;
+
     private static $instance = null;
 
     public static function getInstance() {
@@ -12,7 +17,40 @@ class FileUploader {
 
     private function __construct() {}
 
-    public function upload() {
-        // TODO: Implement file upload features
+    public function upload($file) {
+        $newFileName = $this->generateNewName($file['name']);
+
+        if($file['size'] > (1024 * $this->maxSize)) {
+            return false;
+        }
+        
+        move_uploaded_file($file['tmp_name'], $this->getLocalPath() . $newFileName);
+
+        return true;
     }
+
+    private function generateNewName($fileName) {
+        $newName = $fileName;
+        $newName = strtolower($newName);
+        $newName = preg_replace('/\s+/', '_', $newName);
+
+        if ($this->linearCongruentialGenerator instanceof LinearCongruentialGenerator) {
+            $newName = $this->linearCongruentialGenerator->generate($this->linearCongruentialGeneratorOffset) . '_' . $newName;
+        }
+
+        return $newName;
+    }
+
+    public function setGeneratorValues($gap, $first, $offset) {
+        $this->linearCongruentialGenerator = new LinearCongruentialGenerator();
+        $this->linearCongruentialGeneratorOffset = $offset;
+
+        $this->linearCongruentialGenerator->setGap($gap);
+        $this->linearCongruentialGenerator->setFirst($first);
+    }
+    
+    public function setMaxSize($maxSize) {
+        $this->maxSize = $maxSize;
+    }
+
 }
\ No newline at end of file
diff --git a/server/libs/Hashing.php b/server/libs/Hashing.php
index aa402cf5..da076181 100644
--- a/server/libs/Hashing.php
+++ b/server/libs/Hashing.php
@@ -7,10 +7,36 @@ class Hashing {
     public static function verifyPassword($password, $hash) {
         return password_verify($password, $hash);
     }
+
     public static function generateRandomToken() {
         return md5(uniqid(rand()));
     }
-    public static function getRandomTicketNumber($min,$max) {
-        return rand($min,$max);
+
+    public static function generateRandomNumber($min, $max) {
+        return rand($min, $max);
+    }
+
+    public static function generateRandomPrime($min, $max) {
+        $number = Hashing::generateRandomNumber($min, $max);
+
+        while(!Hashing::isPrime($number)) {
+            $number = Hashing::generateRandomNumber($min, $max);
+        }
+
+        return $number;
+    }
+
+    public static function isPrime($number) {
+        $sqrt = sqrt($number);
+        $prime = true;
+
+        for($i = 0; $i < $sqrt; $i++) {
+            if($sqrt % 2 === 0) {
+                $prime = false;
+                break;
+            }
+        }
+
+        return $prime;
     }
 }
\ No newline at end of file
diff --git a/server/libs/LinearCongruentialGenerator.php b/server/libs/LinearCongruentialGenerator.php
new file mode 100644
index 00000000..aafddb16
--- /dev/null
+++ b/server/libs/LinearCongruentialGenerator.php
@@ -0,0 +1,30 @@
+<?php
+class LinearCongruentialGenerator {
+    private $gap;
+    private $first;
+    private $min = 100000;
+    private $max = 999999;
+    
+    public function setRange($min, $max) {
+        $this->min = $min;
+        $this->max = $max;
+    }
+
+    public function setGap($gap) {
+        if(!Hashing::isPrime($gap)) throw new Exception('LinearCongruentialGenerator: gap must be prime');
+        
+        $this->gap = $gap;
+    }
+
+    public function setFirst($first) {
+        $this->first = $first;
+    }
+    
+    public function generate($offset) {
+        return ($this->first - $this->min + $offset * $this->gap) % ($this->max - $this->min + 1) + $this->min;
+    }
+    
+    public function generateFirst() {
+        return Hashing::generateRandomNumber($this->min, $this->max);
+    }
+}
\ No newline at end of file
diff --git a/server/models/Ticket.php b/server/models/Ticket.php
index 315434e8..88351ea6 100644
--- a/server/models/Ticket.php
+++ b/server/models/Ticket.php
@@ -46,17 +46,16 @@ class Ticket extends DataStore {
     }
 
     public function generateUniqueTicketNumber() {
+        $linearCongruentialGenerator = new LinearCongruentialGenerator();
         $ticketQuantity = Ticket::count();
-        $minValue = 100000;
-        $maxValue = 999999;
-
+        
         if ($ticketQuantity === 0) {
-            $ticketNumber = Hashing::getRandomTicketNumber($minValue, $maxValue);
+            $ticketNumber = $linearCongruentialGenerator->generateFirst();
         } else {
-            $firstTicketNumber = Ticket::getTicket(1)->ticketNumber;
-            $gap = 176611; //TODO: USE RANDOM PRIME INSTEAD
-
-            $ticketNumber = ($firstTicketNumber - $minValue + $ticketQuantity * $gap) % ($maxValue - $minValue + 1) + $minValue;
+            $linearCongruentialGenerator->setGap(Setting::getSetting('ticket-gap')->value);
+            $linearCongruentialGenerator->setFirst(Ticket::getTicket(1)->ticketNumber);
+            
+            $ticketNumber = $linearCongruentialGenerator->generate($ticketQuantity);
         }
 
         return $ticketNumber;
diff --git a/tests/ticket/create.rb b/tests/ticket/create.rb
index 481e0b50..fbc6be44 100644
--- a/tests/ticket/create.rb
+++ b/tests/ticket/create.rb
@@ -147,13 +147,15 @@ describe '/ticket/create' do
             csrf_token: $csrf_token
         })
 
+        ticket_number_gap = $database.getRow('setting', 'ticket-gap', 'name')['value'].to_i
+
         ticket0 = $database.getRow('ticket','Winter is coming','title')['ticket_number'].to_i
         ticket1 = $database.getRow('ticket','Winter is coming1','title')['ticket_number'].to_i
         ticket2 = $database.getRow('ticket','Winter is coming2','title')['ticket_number'].to_i
         ticket3 = $database.getRow('ticket','Winter is coming3','title')['ticket_number'].to_i
 
-        (ticket1).should.equal((ticket0 - 100000 + 1 * 176611) % 900000 + 100000)
-        (ticket2).should.equal((ticket0 - 100000 + 2 * 176611) % 900000 + 100000)
-        (ticket3).should.equal((ticket0 - 100000 + 3 * 176611) % 900000 + 100000)
+        (ticket1).should.equal((ticket0 - 100000 + 1 * ticket_number_gap) % 900000 + 100000)
+        (ticket2).should.equal((ticket0 - 100000 + 2 * ticket_number_gap) % 900000 + 100000)
+        (ticket3).should.equal((ticket0 - 100000 + 3 * ticket_number_gap) % 900000 + 100000)
     end
 end

From 1d514dda580be4efe78983c4ca0629347f00330c Mon Sep 17 00:00:00 2001
From: AntonyAntonio <guillermo@opensupports.com>
Date: Thu, 12 Jan 2017 17:06:41 -0300
Subject: [PATCH 08/21] Guillermo -   path-delete-all-users [skip ci]

---
 server/controllers/system.php                 |  2 ++
 .../controllers/system/delete-all-users.php   | 32 +++++++++++++++++++
 2 files changed, 34 insertions(+)
 create mode 100644 server/controllers/system/delete-all-users.php

diff --git a/server/controllers/system.php b/server/controllers/system.php
index e5423a5a..4a908bc1 100644
--- a/server/controllers/system.php
+++ b/server/controllers/system.php
@@ -11,6 +11,7 @@ require_once 'system/edit-mail-template.php';
 require_once 'system/recover-mail-template.php';
 require_once 'system/disable-registration.php';
 require_once 'system/enable-registration.php';
+require_once 'system/delete-all-users.php';
 
 $systemControllerGroup = new ControllerGroup();
 $systemControllerGroup->setGroupPath('/system');
@@ -27,5 +28,6 @@ $systemControllerGroup->addController(new EditMailTemplateController);
 $systemControllerGroup->addController(new RecoverMailTemplateController);
 $systemControllerGroup->addController(new DisableRegistrationController);
 $systemControllerGroup->addController(new EnableRegistrationController);
+$systemControllerGroup->addController(new DeleteAllUsersController);
 
 $systemControllerGroup->finalize();
\ No newline at end of file
diff --git a/server/controllers/system/delete-all-users.php b/server/controllers/system/delete-all-users.php
new file mode 100644
index 00000000..63749e59
--- /dev/null
+++ b/server/controllers/system/delete-all-users.php
@@ -0,0 +1,32 @@
+<?php
+use RedBeanPHP\Facade as RedBean;
+
+class DeleteAllUsersController extends Controller {
+    const PATH = '/delete-all-users';
+
+    public function validations() {
+        return [
+            'permission' => 'staff_3',
+            'requestData' => []
+        ];
+    }
+
+    public function handler() {
+        $password = Controller::request('password');
+
+        if(!Hashing::verifyPassword($password, Controller::getLoggedUser()->password)) {
+            Response::respondError(ERRORS::INVALID_PASSWORD);
+            return;
+        }
+
+        Redbean::exec('SET FOREIGN_KEY_CHECKS = 0;');
+        RedBean::wipe(SessionCookie::TABLE);
+        RedBean::wipe(User::TABLE);
+        RedBean::wipe(Ticket::TABLE);
+        RedBean::wipe(Ticketevent::TABLE);
+        RedBean::wipe('ticket_user');
+        Redbean::exec('SET FOREIGN_KEY_CHECKS = 1;');
+
+        Response::respondSuccess();
+    }
+}
\ No newline at end of file

From 5f5a2be76beae00f7d93b2b3b26648be9cbc3228 Mon Sep 17 00:00:00 2001
From: AntonyAntonio <guillermo@opensupports.com>
Date: Thu, 12 Jan 2017 18:26:06 -0300
Subject: [PATCH 09/21] Guillermo -   stast architecture [skip ci]

---
 tests/system/get-stats.rb | 205 +++++++++-----------------------------
 1 file changed, 46 insertions(+), 159 deletions(-)

diff --git a/tests/system/get-stats.rb b/tests/system/get-stats.rb
index 28f79541..a86531f0 100644
--- a/tests/system/get-stats.rb
+++ b/tests/system/get-stats.rb
@@ -71,180 +71,67 @@ describe'/system/get-stats' do
             $database.query("INSERT INTO ticketevent VALUES('', 'ASSIGN', NULL, " + yesterday + ", NULL, NULL, 1);")
         end
 
-        result= request('/system/get-stats', {
+        @result = request('/system/get-stats', {
             csrf_userid: $csrf_userid,
             csrf_token: $csrf_token,
             period: 'week'
         })
 
-        (result['status']).should.equal('success')
+        def assertData(position, date, type, value)
+            (@result['data'][position]['date']).should.equal(date)
+            (@result['data'][position]['type']).should.equal(type)
+            (@result['data'][position]['value']).should.equal(value)
+        end
 
-        row = $database.getRow('stat', 65, 'id')
+        assertData(11, '20170109', 'CREATE_TICKET', '1')
+        assertData(10, '20170109', 'CLOSE', '2')
+        assertData(9, '20170109', 'SIGNUP', '6')
+        assertData(8, '20170109', 'COMMENT', '3')
 
-        (row['date']).should.equal('20170109')
-        (row['type']).should.equal('CREATE_TICKET')
-        (row['general']).should.equal('1')
-        (row['value']).should.equal('1')
 
-        row = $database.getRow('stat', 66, 'id')
+        assertData(7, '20170110', 'CREATE_TICKET', '3')
+        assertData(6, '20170110', 'CLOSE', '10')
+        assertData(5, '20170110', 'SIGNUP', '8')
+        assertData(4, '20170110', 'COMMENT', '3')
 
-        (row['date']).should.equal('20170109')
-        (row['type']).should.equal('CLOSE')
-        (row['general']).should.equal('1')
-        (row['value']).should.equal('2')
+        assertData(3, '20170111', 'CREATE_TICKET', '2')
+        assertData(2, '20170111', 'CLOSE', '5')
+        assertData(1, '20170111', 'SIGNUP', '1')
+        assertData(0, '20170111', 'COMMENT', '8')
 
-        row = $database.getRow('stat', 67, 'id')
 
-        (row['date']).should.equal('20170109')
-        (row['type']).should.equal('SIGNUP')
-        (row['general']).should.equal('1')
-        (row['value']).should.equal('6')
+        @result = request('/system/get-stats', {
+            csrf_userid: $csrf_userid,
+            csrf_token: $csrf_token,
+            period: 'week',
+            staffId: '1'
+        })
+        assertData(0, '20170111', 'CLOSE', '4')
+        assertData(1, '20170111', 'ASSIGN', '8')
+        assertData(2, '20170110', 'CLOSE', '11')
+        assertData(3, '20170110', 'ASSIGN', '3')
 
-        row = $database.getRow('stat', 68, 'id')
+        assertData(4, '20170109', 'CLOSE', '9')
+        assertData(5, '20170109', 'ASSIGN', '5')
+        assertData(6, '20170108', 'CLOSE', '0')
+        assertData(7, '20170108', 'ASSIGN', '0')
 
-        (row['date']).should.equal('20170109')
-        (row['type']).should.equal('COMMENT')
-        (row['general']).should.equal('1')
-        (row['value']).should.equal('3')
+        assertData(8, '20170107', 'CLOSE', '0')
+        assertData(9, '20170107', 'ASSIGN', '0')
+        assertData(10, '20170106', 'CLOSE', '0')
+        assertData(11, '20170106', 'ASSIGN', '0')
 
-        row = $database.getRow('stat', 69, 'id')
+        assertData(12, '20170105', 'CLOSE', '0')
+        assertData(13, '20170105', 'ASSIGN', '0')
+        assertData(14, '20170104', 'CLOSE', '0')
+        assertData(15, '20170104', 'ASSIGN', '0')
 
-        (row['date']).should.equal('20170109')
-        (row['type']).should.equal('ASSIGN')
-        (row['general']).should.equal('0')
-        (row['value']).should.equal('5')
+        assertData(16, '20170103', 'CLOSE', '0')
+        assertData(17, '20170103', 'ASSIGN', '0')
+        assertData(18, '20170102', 'CLOSE', '0')
+        assertData(19, '20170102', 'ASSIGN', '0')
 
-        row = $database.getRow('stat', 70, 'id')
-
-        (row['date']).should.equal('20170109')
-        (row['type']).should.equal('CLOSE')
-        (row['general']).should.equal('0')
-        (row['value']).should.equal('9')
-
-        row = $database.getRow('stat', 71, 'id')
-
-        (row['date']).should.equal('20170109')
-        (row['type']).should.equal('ASSIGN')
-        (row['general']).should.equal('0')
-        (row['value']).should.equal('0')
-
-        row = $database.getRow('stat', 72, 'id')
-
-        (row['date']).should.equal('20170109')
-        (row['type']).should.equal('CLOSE')
-        (row['general']).should.equal('0')
-        (row['value']).should.equal('0')
-
-        row = $database.getRow('stat', 73, 'id')
-
-        (row['date']).should.equal('20170110')
-        (row['type']).should.equal('CREATE_TICKET')
-        (row['general']).should.equal('1')
-        (row['value']).should.equal('3')
-
-        row = $database.getRow('stat', 74, 'id')
-
-        (row['date']).should.equal('20170110')
-        (row['type']).should.equal('CLOSE')
-        (row['general']).should.equal('1')
-        (row['value']).should.equal('10')
-
-        row = $database.getRow('stat', 75, 'id')
-
-        (row['date']).should.equal('20170110')
-        (row['type']).should.equal('SIGNUP')
-        (row['general']).should.equal('1')
-        (row['value']).should.equal('8')
-
-        row = $database.getRow('stat', 76, 'id')
-
-        (row['date']).should.equal('20170110')
-        (row['type']).should.equal('COMMENT')
-        (row['general']).should.equal('1')
-        (row['value']).should.equal('3')
-
-        row = $database.getRow('stat', 77, 'id')
-
-        (row['date']).should.equal('20170110')
-        (row['type']).should.equal('ASSIGN')
-        (row['general']).should.equal('0')
-        (row['value']).should.equal('3')
-
-        row = $database.getRow('stat', 78, 'id')
-
-        (row['date']).should.equal('20170110')
-        (row['type']).should.equal('CLOSE')
-        (row['general']).should.equal('0')
-        (row['value']).should.equal('11')
-
-        row = $database.getRow('stat', 79, 'id')
-
-        (row['date']).should.equal('20170110')
-        (row['type']).should.equal('ASSIGN')
-        (row['general']).should.equal('0')
-        (row['value']).should.equal('0')
-
-        row = $database.getRow('stat', 80, 'id')
-
-        (row['date']).should.equal('20170110')
-        (row['type']).should.equal('CLOSE')
-        (row['general']).should.equal('0')
-        (row['value']).should.equal('0')
-
-        row = $database.getRow('stat', 81, 'id')
-
-        (row['date']).should.equal('20170111')
-        (row['type']).should.equal('CREATE_TICKET')
-        (row['general']).should.equal('1')
-        (row['value']).should.equal('2')
-
-        row = $database.getRow('stat', 82, 'id')
-
-        (row['date']).should.equal('20170111')
-        (row['type']).should.equal('CLOSE')
-        (row['general']).should.equal('1')
-        (row['value']).should.equal('5')
-
-        row = $database.getRow('stat', 83, 'id')
-
-        (row['date']).should.equal('20170111')
-        (row['type']).should.equal('SIGNUP')
-        (row['general']).should.equal('1')
-        (row['value']).should.equal('1')
-
-        row = $database.getRow('stat', 84, 'id')
-
-        (row['date']).should.equal('20170111')
-        (row['type']).should.equal('COMMENT')
-        (row['general']).should.equal('1')
-        (row['value']).should.equal('8')
-
-        row = $database.getRow('stat', 85, 'id')
-
-        (row['date']).should.equal('20170111')
-        (row['type']).should.equal('ASSIGN')
-        (row['general']).should.equal('0')
-        (row['value']).should.equal('8')
-
-        row = $database.getRow('stat', 86, 'id')
-
-        (row['date']).should.equal('20170111')
-        (row['type']).should.equal('CLOSE')
-        (row['general']).should.equal('0')
-        (row['value']).should.equal('4')
-
-        row = $database.getRow('stat', 87, 'id')
-
-        (row['date']).should.equal('20170111')
-        (row['type']).should.equal('ASSIGN')
-        (row['general']).should.equal('0')
-        (row['value']).should.equal('0')
-
-        row = $database.getRow('stat', 88, 'id')
-
-        (row['date']).should.equal('20170111')
-        (row['type']).should.equal('CLOSE')
-        (row['general']).should.equal('0')
-        (row['value']).should.equal('0')
+        assertData(20, '20170101', 'CLOSE', '0')
+        assertData(21, '20170101', 'ASSIGN', '0')
     end
 end

From f2401dcec718b09c44f02d8c644357c8b8c33aa6 Mon Sep 17 00:00:00 2001
From: ivan <ivan@opensupports.com>
Date: Thu, 12 Jan 2017 18:29:50 -0300
Subject: [PATCH 10/21] Ivan - Fix file uploader issues, add file upload in
 ticket creation [skip ci]

---
 server/controllers/system/init-settings.php |  3 ++-
 server/controllers/ticket/create.php        | 24 ++++++++++++++++++++-
 server/libs/FileUploader.php                | 13 +++++++----
 server/libs/LinearCongruentialGenerator.php |  3 ++-
 tests/system/edit-settings.rb               |  4 ++--
 5 files changed, 38 insertions(+), 9 deletions(-)

diff --git a/server/controllers/system/init-settings.php b/server/controllers/system/init-settings.php
index 6613b17a..a78a6c51 100644
--- a/server/controllers/system/init-settings.php
+++ b/server/controllers/system/init-settings.php
@@ -38,12 +38,13 @@ class InitSettingsController extends Controller {
             'maintenance-mode' => 0,
             'layout' => 'boxed',
             'allow-attachments' => 0,
-            'max-size' => 0,
+            'max-size' => 1024,
             'title' => 'Support Center',
             'url' => 'http://www.opensupports.com/support',
             'ticket-gap' => Hashing::generateRandomPrime(100000, 999999),
             'file-gap' => Hashing::generateRandomPrime(100000, 999999),
             'file-first-number' => Hashing::generateRandomNumber(100000, 999999),
+            'file-quantity' => 0
         ]);
     }
 
diff --git a/server/controllers/ticket/create.php b/server/controllers/ticket/create.php
index ac9f4c53..b9409361 100644
--- a/server/controllers/ticket/create.php
+++ b/server/controllers/ticket/create.php
@@ -60,7 +60,7 @@ class CreateController extends Controller {
             'language' => $this->language,
             'author' => $author,
             'department' => $department,
-            'file' => '',
+            'file' => $this->uploadFile(),
             'date' => Date::getCurrentDate(),
             'unread' => false,
             'unreadStaff' => true,
@@ -75,4 +75,26 @@ class CreateController extends Controller {
         
         $this->ticketNumber = $ticket->ticketNumber;
     }
+    
+    private function uploadFile() {
+        if(!isset($_FILES['file'])) return '';
+        
+        $maxSize = Setting::getSetting('max-size')->getValue();
+        $fileGap = Setting::getSetting('file-gap')->getValue();
+        $fileFirst = Setting::getSetting('file-first-number')->getValue();
+        $fileQuantity = Setting::getSetting('file-quantity');
+        
+        $fileUploader = FileUploader::getInstance();
+        $fileUploader->setMaxSize($maxSize);
+        $fileUploader->setGeneratorValues($fileGap, $fileFirst, $fileQuantity->getValue());
+
+        if($fileUploader->upload($_FILES['file'])) {
+            $fileQuantity->value++;
+            $fileQuantity->store();
+
+            return $fileUploader->getFileName();
+        } else {
+            throw new Exception(ERRORS::INVALID_FILE);
+        }
+    }
 }
diff --git a/server/libs/FileUploader.php b/server/libs/FileUploader.php
index dfc9c09b..8e01e089 100644
--- a/server/libs/FileUploader.php
+++ b/server/libs/FileUploader.php
@@ -4,6 +4,7 @@ class FileUploader extends FileManager {
     private $maxSize = 1024;
     private $linearCongruentialGenerator;
     private $linearCongruentialGeneratorOffset;
+    private $fileName;
 
     private static $instance = null;
 
@@ -18,18 +19,18 @@ class FileUploader extends FileManager {
     private function __construct() {}
 
     public function upload($file) {
-        $newFileName = $this->generateNewName($file['name']);
+        $this->setNewName($file['name']);
 
         if($file['size'] > (1024 * $this->maxSize)) {
             return false;
         }
         
-        move_uploaded_file($file['tmp_name'], $this->getLocalPath() . $newFileName);
+        move_uploaded_file($file['tmp_name'], $this->getLocalPath() . $this->getFileName());
 
         return true;
     }
 
-    private function generateNewName($fileName) {
+    private function setNewName($fileName) {
         $newName = $fileName;
         $newName = strtolower($newName);
         $newName = preg_replace('/\s+/', '_', $newName);
@@ -38,7 +39,7 @@ class FileUploader extends FileManager {
             $newName = $this->linearCongruentialGenerator->generate($this->linearCongruentialGeneratorOffset) . '_' . $newName;
         }
 
-        return $newName;
+        $this->fileName = $newName;
     }
 
     public function setGeneratorValues($gap, $first, $offset) {
@@ -52,5 +53,9 @@ class FileUploader extends FileManager {
     public function setMaxSize($maxSize) {
         $this->maxSize = $maxSize;
     }
+    
+    public function getFileName() {
+        return $this->fileName;
+    }
 
 }
\ No newline at end of file
diff --git a/server/libs/LinearCongruentialGenerator.php b/server/libs/LinearCongruentialGenerator.php
index aafddb16..88df4141 100644
--- a/server/libs/LinearCongruentialGenerator.php
+++ b/server/libs/LinearCongruentialGenerator.php
@@ -21,7 +21,8 @@ class LinearCongruentialGenerator {
     }
     
     public function generate($offset) {
-        return ($this->first - $this->min + $offset * $this->gap) % ($this->max - $this->min + 1) + $this->min;
+        if($offset) return ($this->first - $this->min + $offset * $this->gap) % ($this->max - $this->min + 1) + $this->min;
+        else return $this->generateFirst();
     }
     
     public function generateFirst() {
diff --git a/tests/system/edit-settings.rb b/tests/system/edit-settings.rb
index 0b2b77aa..69223599 100644
--- a/tests/system/edit-settings.rb
+++ b/tests/system/edit-settings.rb
@@ -10,7 +10,7 @@ describe'system/edit-settings' do
             "time-zone" => -3,
             "layout" => 'full-width',
             "allow-attachments" => 1,
-            "max-size" => 2,
+            "max-size" => 2048,
             "language" => 'en',
             "no-reply-email" => 'testemail@hotmail.com'
         })
@@ -27,7 +27,7 @@ describe'system/edit-settings' do
         (row['value']).should.equal('full-width')
 
         row = $database.getRow('setting', 'max-size', 'name')
-        (row['value']).should.equal('2')
+        (row['value']).should.equal('2048')
 
         row = $database.getRow('setting', 'language', 'name')
         (row['value']).should.equal('en')

From 71984384ccddee35723f2966e7057dbd27b673bf Mon Sep 17 00:00:00 2001
From: ivan <ivan@opensupports.com>
Date: Thu, 12 Jan 2017 20:30:44 -0300
Subject: [PATCH 11/21] Ivan - Add validations before download and upload on
 comment [skip ci]

---
 server/controllers/system/download.php | 36 +++++++++++++++++++++++---
 server/controllers/ticket/comment.php  |  1 +
 server/controllers/ticket/create.php   | 22 ----------------
 server/libs/Controller.php             | 22 ++++++++++++++++
 4 files changed, 56 insertions(+), 25 deletions(-)

diff --git a/server/controllers/system/download.php b/server/controllers/system/download.php
index 0b481bf8..8c6699b3 100644
--- a/server/controllers/system/download.php
+++ b/server/controllers/system/download.php
@@ -7,18 +7,48 @@ class DownloadController extends Controller {
 
     public function validations() {
         return [
-            'permission' => 'staff_1',
+            'permission' => 'user',
             'requestData' => [
                 'file' => [
-                    'validation' => DataValidator::alnum('_.')->noWhitespace()
+                    'validation' => DataValidator::alnum('_.')->noWhitespace(),
+                    'error' => ERRORS::INVALID_FILE
                 ]
             ]
         ];
     }
 
     public function handler() {
+        $fileName = Controller::request('file');
+
+        $loggedUser = Controller::getLoggedUser();
+        $ticket = Ticket::getTicket($fileName, 'file');
+
+        if($ticket->isNull() || ($this->isNotAuthor($ticket, $loggedUser) && $this->isNotOwner($ticket, $loggedUser))) {
+            $ticketEvent = Ticketevent::getDataStore($fileName, 'file');
+
+            if($ticketEvent->isNull()) {
+                print '';
+                return;
+            }
+
+            $ticket = $ticketEvent->ticket;
+
+            if($this->isNotAuthor($ticket, $loggedUser) && $this->isNotOwner($ticket, $loggedUser)) {
+                print '';
+                return;
+            }
+        }
+
         $fileDownloader = FileDownloader::getInstance();
-        $fileDownloader->setFileName(Controller::request('file'));
+        $fileDownloader->setFileName($fileName);
         $fileDownloader->download();
     }
+
+    private function isNotAuthor($ticket, $loggedUser) {
+        return Controller::isStaffLogged() || $ticket->author->id !== $loggedUser->id;
+    }
+
+    private function isNotOwner($ticket, $loggedUser) {
+        return !Controller::isStaffLogged() || !$ticket->owner || $ticket->owner->id !== $loggedUser->id;
+    }
 }
\ No newline at end of file
diff --git a/server/controllers/ticket/comment.php b/server/controllers/ticket/comment.php
index b51414c2..6a043757 100644
--- a/server/controllers/ticket/comment.php
+++ b/server/controllers/ticket/comment.php
@@ -50,6 +50,7 @@ class CommentController extends Controller {
         $comment = Ticketevent::getEvent(Ticketevent::COMMENT);
         $comment->setProperties(array(
             'content' => $this->content,
+            'file' => $this->uploadFile(),
             'date' => Date::getCurrentDate()
         ));
 
diff --git a/server/controllers/ticket/create.php b/server/controllers/ticket/create.php
index b9409361..fbcd9203 100644
--- a/server/controllers/ticket/create.php
+++ b/server/controllers/ticket/create.php
@@ -75,26 +75,4 @@ class CreateController extends Controller {
         
         $this->ticketNumber = $ticket->ticketNumber;
     }
-    
-    private function uploadFile() {
-        if(!isset($_FILES['file'])) return '';
-        
-        $maxSize = Setting::getSetting('max-size')->getValue();
-        $fileGap = Setting::getSetting('file-gap')->getValue();
-        $fileFirst = Setting::getSetting('file-first-number')->getValue();
-        $fileQuantity = Setting::getSetting('file-quantity');
-        
-        $fileUploader = FileUploader::getInstance();
-        $fileUploader->setMaxSize($maxSize);
-        $fileUploader->setGeneratorValues($fileGap, $fileFirst, $fileQuantity->getValue());
-
-        if($fileUploader->upload($_FILES['file'])) {
-            $fileQuantity->value++;
-            $fileQuantity->store();
-
-            return $fileUploader->getFileName();
-        } else {
-            throw new Exception(ERRORS::INVALID_FILE);
-        }
-    }
 }
diff --git a/server/libs/Controller.php b/server/libs/Controller.php
index 3c8a3ca9..b82bdb0f 100644
--- a/server/libs/Controller.php
+++ b/server/libs/Controller.php
@@ -60,4 +60,26 @@ abstract class Controller {
     public static function getAppInstance() {
         return \Slim\Slim::getInstance();
     }
+    
+    public function uploadFile() {
+        if(!isset($_FILES['file'])) return '';
+
+        $maxSize = Setting::getSetting('max-size')->getValue();
+        $fileGap = Setting::getSetting('file-gap')->getValue();
+        $fileFirst = Setting::getSetting('file-first-number')->getValue();
+        $fileQuantity = Setting::getSetting('file-quantity');
+
+        $fileUploader = FileUploader::getInstance();
+        $fileUploader->setMaxSize($maxSize);
+        $fileUploader->setGeneratorValues($fileGap, $fileFirst, $fileQuantity->getValue());
+
+        if($fileUploader->upload($_FILES['file'])) {
+            $fileQuantity->value++;
+            $fileQuantity->store();
+
+            return $fileUploader->getFileName();
+        } else {
+            throw new Exception(ERRORS::INVALID_FILE);
+        }
+    }
 }
\ No newline at end of file

From 139a474693bc855a84fdfeba0814030380648657 Mon Sep 17 00:00:00 2001
From: ivan <ivan@opensupports.com>
Date: Fri, 13 Jan 2017 10:33:17 -0300
Subject: [PATCH 12/21] Max Red - beautify code and set default selected
 indices to an empty array [skip ci]

---
 .../toggle-list.js                                  | 13 ++++++-------
 .../toggle-list.scss                                |  0
 2 files changed, 6 insertions(+), 7 deletions(-)
 rename client/src/{app-components => core-components}/toggle-list.js (77%)
 rename client/src/{app-components => core-components}/toggle-list.scss (100%)

diff --git a/client/src/app-components/toggle-list.js b/client/src/core-components/toggle-list.js
similarity index 77%
rename from client/src/app-components/toggle-list.js
rename to client/src/core-components/toggle-list.js
index f90a26ae..8dba39a7 100644
--- a/client/src/app-components/toggle-list.js
+++ b/client/src/core-components/toggle-list.js
@@ -11,7 +11,7 @@ class ToggleList extends React.Component {
     };
 
     state = {
-        selected: [1, 3]
+        selected: []
     };
     
     render() {
@@ -35,26 +35,25 @@ class ToggleList extends React.Component {
         let classes = {
             'toggle-list__item': true,
             'toggle-list__first-item': (index === 0),
-            'toggle-list__selected': (_.includes(this.state.selected, index))
+            'toggle-list__selected': _.includes(this.state.selected, index)
         };
 
         return classNames(classes);
     }
 
     selectItem(index) {
-        let actual = _.clone(this.state.selected);
+        let newSelected = _.clone(this.state.selected);
 
-        _.includes(this.state.selected, index) ? _.remove(actual, t => t == index) : actual.push(index);
+        _.includes(this.state.selected, index) ? _.remove(newSelected, _index => _index == index) : newSelected.push(index);
 
-        console.log(actual);
         this.setState({
-            selected: actual
+            selected: newSelected
         });
 
         if (this.props.onChange) {
             this.props.onChange({
                 target: {
-                    value: actual
+                    value: newSelected
                 }
             });
         }
diff --git a/client/src/app-components/toggle-list.scss b/client/src/core-components/toggle-list.scss
similarity index 100%
rename from client/src/app-components/toggle-list.scss
rename to client/src/core-components/toggle-list.scss

From 087a24c75c30ff4d75a9d9cd0e9db2ae5c55181d Mon Sep 17 00:00:00 2001
From: AntonyAntonio <guillermo@opensupports.com>
Date: Fri, 13 Jan 2017 13:46:57 -0300
Subject: [PATCH 13/21] Guillermo -   stast architecture [skip ci]

---
 tests/system/get-stats.rb | 91 ++++++++++++++++++++++++---------------
 1 file changed, 57 insertions(+), 34 deletions(-)

diff --git a/tests/system/get-stats.rb b/tests/system/get-stats.rb
index a86531f0..406c4516 100644
--- a/tests/system/get-stats.rb
+++ b/tests/system/get-stats.rb
@@ -83,21 +83,44 @@ describe'/system/get-stats' do
             (@result['data'][position]['value']).should.equal(value)
         end
 
-        assertData(11, '20170109', 'CREATE_TICKET', '1')
-        assertData(10, '20170109', 'CLOSE', '2')
-        assertData(9, '20170109', 'SIGNUP', '6')
-        assertData(8, '20170109', 'COMMENT', '3')
+        d = Date.today.prev_day
+        yesterday = d.strftime("%Y%m%d")
+        d = Date.today.prev_day.prev_day
+        yesterday2 = d.strftime("%Y%m%d")
+        d = Date.today.prev_day.prev_day.prev_day
+        yesterday3 = d.strftime("%Y%m%d")
+        d = Date.today.prev_day.prev_day.prev_day.prev_day
+        yesterday4 = d.strftime("%Y%m%d")
+        d = Date.today.prev_day.prev_day.prev_day.prev_day.prev_day
+        yesterday5 = d.strftime("%Y%m%d")
+        d = Date.today.prev_day.prev_day.prev_day.prev_day.prev_day.prev_day
+        yesterday6 = d.strftime("%Y%m%d")
+        d = Date.today.prev_day.prev_day.prev_day.prev_day.prev_day.prev_day.prev_day
+        yesterday7 = d.strftime("%Y%m%d")
+        d = Date.today.prev_day.prev_day.prev_day.prev_day.prev_day.prev_day.prev_day.prev_day
+        yesterday8 = d.strftime("%Y%m%d")
+        d = Date.today.prev_day.prev_day.prev_day.prev_day.prev_day.prev_day.prev_day.prev_day.prev_day
+        yesterday9 = d.strftime("%Y%m%d")
+        d = Date.today.prev_day.prev_day.prev_day.prev_day.prev_day.prev_day.prev_day.prev_day.prev_day.prev_day
+        yesterday10 = d.strftime("%Y%m%d")
+        d = Date.today.prev_day.prev_day.prev_day.prev_day.prev_day.prev_day.prev_day.prev_day.prev_day.prev_day.prev_day
+        yesterday11 = d.strftime("%Y%m%d")
+
+        assertData(11, yesterday3, 'CREATE_TICKET', '1')
+        assertData(10, yesterday3, 'CLOSE', '2')
+        assertData(9, yesterday3, 'SIGNUP', '6')
+        assertData(8, yesterday3, 'COMMENT', '3')
 
 
-        assertData(7, '20170110', 'CREATE_TICKET', '3')
-        assertData(6, '20170110', 'CLOSE', '10')
-        assertData(5, '20170110', 'SIGNUP', '8')
-        assertData(4, '20170110', 'COMMENT', '3')
+        assertData(7, yesterday2, 'CREATE_TICKET', '3')
+        assertData(6, yesterday2, 'CLOSE', '10')
+        assertData(5, yesterday2, 'SIGNUP', '8')
+        assertData(4, yesterday2, 'COMMENT', '3')
 
-        assertData(3, '20170111', 'CREATE_TICKET', '2')
-        assertData(2, '20170111', 'CLOSE', '5')
-        assertData(1, '20170111', 'SIGNUP', '1')
-        assertData(0, '20170111', 'COMMENT', '8')
+        assertData(3, yesterday, 'CREATE_TICKET', '2')
+        assertData(2, yesterday, 'CLOSE', '5')
+        assertData(1, yesterday, 'SIGNUP', '1')
+        assertData(0, yesterday, 'COMMENT', '8')
 
 
         @result = request('/system/get-stats', {
@@ -106,32 +129,32 @@ describe'/system/get-stats' do
             period: 'week',
             staffId: '1'
         })
-        assertData(0, '20170111', 'CLOSE', '4')
-        assertData(1, '20170111', 'ASSIGN', '8')
-        assertData(2, '20170110', 'CLOSE', '11')
-        assertData(3, '20170110', 'ASSIGN', '3')
+        assertData(0, yesterday, 'CLOSE', '4')
+        assertData(1, yesterday, 'ASSIGN', '8')
+        assertData(2, yesterday2, 'CLOSE', '11')
+        assertData(3, yesterday2, 'ASSIGN', '3')
 
-        assertData(4, '20170109', 'CLOSE', '9')
-        assertData(5, '20170109', 'ASSIGN', '5')
-        assertData(6, '20170108', 'CLOSE', '0')
-        assertData(7, '20170108', 'ASSIGN', '0')
+        assertData(4, yesterday3, 'CLOSE', '9')
+        assertData(5, yesterday3, 'ASSIGN', '5')
+        assertData(6, yesterday4, 'CLOSE', '0')
+        assertData(7, yesterday4, 'ASSIGN', '0')
 
-        assertData(8, '20170107', 'CLOSE', '0')
-        assertData(9, '20170107', 'ASSIGN', '0')
-        assertData(10, '20170106', 'CLOSE', '0')
-        assertData(11, '20170106', 'ASSIGN', '0')
+        assertData(8, yesterday5, 'CLOSE', '0')
+        assertData(9, yesterday5, 'ASSIGN', '0')
+        assertData(10, yesterday6, 'CLOSE', '0')
+        assertData(11, yesterday6, 'ASSIGN', '0')
 
-        assertData(12, '20170105', 'CLOSE', '0')
-        assertData(13, '20170105', 'ASSIGN', '0')
-        assertData(14, '20170104', 'CLOSE', '0')
-        assertData(15, '20170104', 'ASSIGN', '0')
+        assertData(12, yesterday7, 'CLOSE', '0')
+        assertData(13, yesterday7, 'ASSIGN', '0')
+        assertData(14, yesterday8, 'CLOSE', '0')
+        assertData(15, yesterday8, 'ASSIGN', '0')
 
-        assertData(16, '20170103', 'CLOSE', '0')
-        assertData(17, '20170103', 'ASSIGN', '0')
-        assertData(18, '20170102', 'CLOSE', '0')
-        assertData(19, '20170102', 'ASSIGN', '0')
+        assertData(16, yesterday9, 'CLOSE', '0')
+        assertData(17, yesterday9, 'ASSIGN', '0')
+        assertData(18, yesterday10, 'CLOSE', '0')
+        assertData(19, yesterday10, 'ASSIGN', '0')
 
-        assertData(20, '20170101', 'CLOSE', '0')
-        assertData(21, '20170101', 'ASSIGN', '0')
+        assertData(20, yesterday11, 'CLOSE', '0')
+        assertData(21, yesterday11, 'ASSIGN', '0')
     end
 end

From db63948db700e5fd6a257d3afc663b5617cee49c Mon Sep 17 00:00:00 2001
From: Ivan Diaz <ivan@opensupports.com>
Date: Fri, 13 Jan 2017 14:40:21 -0300
Subject: [PATCH 14/21] Ivan - Add file upload test [skip ci]

---
 tests/system/fiile-upload-download.rb | 24 ++++++++++++++++++++++++
 1 file changed, 24 insertions(+)
 create mode 100644 tests/system/fiile-upload-download.rb

diff --git a/tests/system/fiile-upload-download.rb b/tests/system/fiile-upload-download.rb
new file mode 100644
index 00000000..ebaa8345
--- /dev/null
+++ b/tests/system/fiile-upload-download.rb
@@ -0,0 +1,24 @@
+describe 'File Upload and Download' do
+    request('/user/logout')
+    Scripts.login("creator@os4.com", "creator")
+
+    it 'should upload file when creating ticket' do
+        file = File.new('upload.txt', 'w')
+        file.puts('file content')
+
+        result = request('/ticket/create', {
+            'csrf_userid' => $csrf_userid,
+            'csrf_token' => $csrf_token,
+            'title' => 'Ticket with file',
+            'content' => 'this is a ticket that contains a file',
+            'language' => 'en',
+            'file' => file
+        })
+        (result['status']).should.equal('success');
+
+        ticket = $database.getLastRow('ticket');
+
+        (ticket['file'].include? 'upload.txt').should.equal(true)
+        (File.exist?('../server/files' + ticket['file'])).should.equal(true)
+    end
+end

From f79913de975f2418dca3be8330e6c8785ad60330 Mon Sep 17 00:00:00 2001
From: AntonyAntonio <guillermo@opensupports.com>
Date: Fri, 13 Jan 2017 15:50:35 -0300
Subject: [PATCH 15/21] Guillermo -   registration api keys [skip ci]

---
 server/controllers/system.php                |  6 +++
 server/controllers/system/add-api-key.php    | 41 ++++++++++++++++++++
 server/controllers/system/delete-api-key.php | 32 +++++++++++++++
 server/controllers/system/get-all-keys.php   | 19 +++++++++
 server/data/ERRORS.php                       |  1 +
 server/models/APIKey.php                     | 18 +++++++++
 6 files changed, 117 insertions(+)
 create mode 100644 server/controllers/system/add-api-key.php
 create mode 100644 server/controllers/system/delete-api-key.php
 create mode 100644 server/controllers/system/get-all-keys.php
 create mode 100644 server/models/APIKey.php

diff --git a/server/controllers/system.php b/server/controllers/system.php
index 8bc393cb..1d8fa889 100644
--- a/server/controllers/system.php
+++ b/server/controllers/system.php
@@ -12,6 +12,9 @@ require_once 'system/recover-mail-template.php';
 require_once 'system/get-stats.php';
 require_once 'system/disable-registration.php';
 require_once 'system/enable-registration.php';
+require_once 'system/add-api-key.php';
+require_once 'system/delete-api-key.php';
+require_once 'system/get-all-keys.php';
 
 $systemControllerGroup = new ControllerGroup();
 $systemControllerGroup->setGroupPath('/system');
@@ -29,5 +32,8 @@ $systemControllerGroup->addController(new RecoverMailTemplateController);
 $systemControllerGroup->addController(new DisableRegistrationController);
 $systemControllerGroup->addController(new EnableRegistrationController);
 $systemControllerGroup->addController(new GetStatsController);
+$systemControllerGroup->addController(new AddAPIKeyController);
+$systemControllerGroup->addController(new DeleteAPIKeyController);
+$systemControllerGroup->addController(new GetAllKeyController);
 
 $systemControllerGroup->finalize();
\ No newline at end of file
diff --git a/server/controllers/system/add-api-key.php b/server/controllers/system/add-api-key.php
new file mode 100644
index 00000000..8bbb4b78
--- /dev/null
+++ b/server/controllers/system/add-api-key.php
@@ -0,0 +1,41 @@
+<?php
+use Respect\Validation\Validator as DataValidator;
+
+class AddAPIKeyController extends Controller {
+    const PATH = '/add-api-key';
+
+    public function validations() {
+        return [
+            'permission' => 'staff_3',
+            'requestData' => [
+                'name' => [
+                    'validation' => DataValidator::length(2, 55)->alpha(),
+                    'error' => ERRORS::INVALID_NAME
+                ]
+            ]
+        ];
+    }
+
+    public function handler() {
+        $apiInstance = new APIKey();
+
+        $name = Controller::request('name');
+
+        $keyInstance = APIKey::getDataStore($name, 'name');
+
+        if($keyInstance->isNull()){
+            $token = Hashing::generateRandomToken();
+
+            $apiInstance->setProperties([
+                'name' => $name,
+                'key' => $token
+            ]);
+
+            $apiInstance->store();
+            Response::respondSuccess($token);
+        } else {
+            Response::respondError(ERRORS::NAME_ALREADY_USED);
+        }
+
+    }
+}
\ No newline at end of file
diff --git a/server/controllers/system/delete-api-key.php b/server/controllers/system/delete-api-key.php
new file mode 100644
index 00000000..8ee03152
--- /dev/null
+++ b/server/controllers/system/delete-api-key.php
@@ -0,0 +1,32 @@
+<?php
+use Respect\Validation\Validator as DataValidator;
+
+class DeleteAPIKeyController extends Controller {
+    const PATH = '/delete-api-key';
+
+    public function validations() {
+        return [
+            'permission' => 'staff_3',
+            'requestData' => [
+                'name' => [
+                    'validation' => DataValidator::length(2, 55)->alpha(),
+                    'error' => ERRORS::INVALID_NAME
+                ]
+            ]
+        ];
+    }
+
+    public function handler() {
+        $name = Controller::request('name');
+
+        $keyInstance = APIKey::getDataStore($name, 'name');
+        
+        if($keyInstance->isNull()) {
+            Response::respondError(ERRORS::INVALID_NAME);
+            return;
+        }
+
+        $keyInstance->delete();
+        Response::respondSuccess();
+    }
+}
\ No newline at end of file
diff --git a/server/controllers/system/get-all-keys.php b/server/controllers/system/get-all-keys.php
new file mode 100644
index 00000000..35206374
--- /dev/null
+++ b/server/controllers/system/get-all-keys.php
@@ -0,0 +1,19 @@
+<?php
+use Respect\Validation\Validator as DataValidator;
+
+class GetAllKeyController extends Controller {
+    const PATH = '/get-all-keys';
+
+    public function validations() {
+        return [
+            'permission' => 'staff_3',
+            'requestData' => []
+        ];
+    }
+
+    public function handler() {
+        $apiList = APIKey::getAll();
+        
+        Response::respondSuccess($apiList->toArray());
+    }
+}
\ No newline at end of file
diff --git a/server/data/ERRORS.php b/server/data/ERRORS.php
index 4b7507cf..06211649 100644
--- a/server/data/ERRORS.php
+++ b/server/data/ERRORS.php
@@ -36,4 +36,5 @@ class ERRORS {
     const INVALID_SUBJECT = 'INVALID_SUBJECT';
     const INVALID_BODY = 'INVALID_BODY';
     const INVALID_PERIOD = 'INVALID_PERIOD';
+    const NAME_ALREADY_USED = 'NAME_ALREADY_USED';
 }
diff --git a/server/models/APIKey.php b/server/models/APIKey.php
new file mode 100644
index 00000000..c0b751fa
--- /dev/null
+++ b/server/models/APIKey.php
@@ -0,0 +1,18 @@
+<?php
+
+class APIKey extends DataStore {
+    const TABLE  = 'apikey';
+
+    public static function getProps() {
+        return [
+            'name',
+            'key'
+        ];
+    }
+    public function toArray() {
+        return [
+            'name' => $this->name,
+            'key' => $this->key
+        ];
+    }
+}
\ No newline at end of file

From 0108414a707904732efe9a9f86967a7fd5504b6d Mon Sep 17 00:00:00 2001
From: AntonyAntonio <guillermo@opensupports.com>
Date: Fri, 13 Jan 2017 17:06:49 -0300
Subject: [PATCH 16/21] Guillermo -   registration api keys [skip ci]

---
 server/controllers/system/add-api-key.php |  4 +--
 server/controllers/user/signup.php        |  3 ++-
 server/libs/validations/captcha.php       |  3 ++-
 server/models/APIKey.php                  |  4 +--
 tests/init.rb                             |  3 +++
 tests/scripts.rb                          |  8 ++++++
 tests/system/add-api-key.rb               | 30 +++++++++++++++++++++++
 tests/system/delete-api-key.rb            | 30 +++++++++++++++++++++++
 tests/system/get-all-keys.rb              | 26 ++++++++++++++++++++
 9 files changed, 105 insertions(+), 6 deletions(-)
 create mode 100644 tests/system/add-api-key.rb
 create mode 100644 tests/system/delete-api-key.rb
 create mode 100644 tests/system/get-all-keys.rb

diff --git a/server/controllers/system/add-api-key.php b/server/controllers/system/add-api-key.php
index 8bbb4b78..2f32d66d 100644
--- a/server/controllers/system/add-api-key.php
+++ b/server/controllers/system/add-api-key.php
@@ -9,7 +9,7 @@ class AddAPIKeyController extends Controller {
             'permission' => 'staff_3',
             'requestData' => [
                 'name' => [
-                    'validation' => DataValidator::length(2, 55)->alpha(),
+                    'validation' => DataValidator::length(2, 55)->alnum(),
                     'error' => ERRORS::INVALID_NAME
                 ]
             ]
@@ -28,7 +28,7 @@ class AddAPIKeyController extends Controller {
 
             $apiInstance->setProperties([
                 'name' => $name,
-                'key' => $token
+                'token' => $token
             ]);
 
             $apiInstance->store();
diff --git a/server/controllers/user/signup.php b/server/controllers/user/signup.php
index 48e6e1c6..cee72f1b 100644
--- a/server/controllers/user/signup.php
+++ b/server/controllers/user/signup.php
@@ -37,6 +37,7 @@ class SignUpController extends Controller {
 
     public function handler() {
         $this->storeRequestData();
+        $apiKey = APIKey::getDataStore(Controller::request('apiKey'), 'token');
 
         $existentUser = User::getUser($this->userEmail, 'email');
 
@@ -51,7 +52,7 @@ class SignUpController extends Controller {
             return;
         }
 
-        if (!Setting::getSetting('registration')->value) {
+        if (!Setting::getSetting('registration')->value && $apiKey->isNull() ) {
             Response::respondError(ERRORS::NO_PERMISSION);
             return;
         }
diff --git a/server/libs/validations/captcha.php b/server/libs/validations/captcha.php
index 437d92d8..7ac805c2 100644
--- a/server/libs/validations/captcha.php
+++ b/server/libs/validations/captcha.php
@@ -8,8 +8,9 @@ class Captcha extends AbstractRule {
 
     public function validate($reCaptchaResponse) {
         $reCaptchaPrivateKey = \Setting::getSetting('recaptcha-private')->getValue();
+        $apiKey = \APIKey::getDataStore(\Controller::request('apiKey'), 'token');
 
-        if (!$reCaptchaPrivateKey) return true;
+        if (!$reCaptchaPrivateKey || !$apiKey->isNull()) return true;
 
         $reCaptcha = new \ReCaptcha\ReCaptcha($reCaptchaPrivateKey);
         $reCaptchaValidation = $reCaptcha->verify($reCaptchaResponse, $_SERVER['REMOTE_ADDR']);
diff --git a/server/models/APIKey.php b/server/models/APIKey.php
index c0b751fa..2cfc3783 100644
--- a/server/models/APIKey.php
+++ b/server/models/APIKey.php
@@ -6,13 +6,13 @@ class APIKey extends DataStore {
     public static function getProps() {
         return [
             'name',
-            'key'
+            'token'
         ];
     }
     public function toArray() {
         return [
             'name' => $this->name,
-            'key' => $this->key
+            'token' => $this->token
         ];
     }
 }
\ No newline at end of file
diff --git a/tests/init.rb b/tests/init.rb
index cae53d63..aaf24e28 100644
--- a/tests/init.rb
+++ b/tests/init.rb
@@ -55,3 +55,6 @@ require './system/recover-mail-template.rb'
 require './system/disable-registration.rb'
 require './system/enable-registration.rb'
 require './system/get-stats.rb'
+require './system/add-api-key.rb'
+require './system/delete-api-key.rb'
+require './system/get-all-keys.rb'
diff --git a/tests/scripts.rb b/tests/scripts.rb
index 4cf57fcf..9b5eb892 100644
--- a/tests/scripts.rb
+++ b/tests/scripts.rb
@@ -44,4 +44,12 @@ class Scripts
 
         result['data']
     end
+
+    def self.createAPIKey(name)
+        result = request('/system/add-api-key', {
+            csrf_userid: $csrf_userid,
+            csrf_token: $csrf_token,
+            name: name
+        })
+    end
 end
diff --git a/tests/system/add-api-key.rb b/tests/system/add-api-key.rb
new file mode 100644
index 00000000..cf8c86d5
--- /dev/null
+++ b/tests/system/add-api-key.rb
@@ -0,0 +1,30 @@
+describe'system/add-api-key' do
+        request('/user/logout')
+        Scripts.login($staff[:email], $staff[:password], true)
+
+        it 'should add API key' do
+            result= request('/system/add-api-key', {
+                csrf_userid: $csrf_userid,
+                csrf_token: $csrf_token,
+                name: 'new API'
+            })
+
+            (result['status']).should.equal('success')
+
+            row = $database.getRow('apikey', 1, 'id')
+
+            (row['name']).should.equal('new API')
+            (result['data']).should.equal(row['token'])
+
+        end
+        it 'should not add API key' do
+            result= request('/system/add-api-key', {
+                csrf_userid: $csrf_userid,
+                csrf_token: $csrf_token,
+                name: 'new API'
+            })
+
+            (result['status']).should.equal('fail')
+            (result['message']).should.equal('NAME_ALREADY_USED')
+        end
+end
diff --git a/tests/system/delete-api-key.rb b/tests/system/delete-api-key.rb
new file mode 100644
index 00000000..21553cfb
--- /dev/null
+++ b/tests/system/delete-api-key.rb
@@ -0,0 +1,30 @@
+describe'system/delete-api-key' do
+        request('/user/logout')
+        Scripts.login($staff[:email], $staff[:password], true)
+
+        it 'should not delete API key' do
+            result= request('/system/delete-api-key', {
+                csrf_userid: $csrf_userid,
+                csrf_token: $csrf_token,
+                name: 'new PIA'
+            })
+
+            (result['status']).should.equal('fail')
+            (result['message']).should.equal('INVALID_NAME')
+        end
+
+        it 'should delete API key' do
+            result= request('/system/delete-api-key', {
+                csrf_userid: $csrf_userid,
+                csrf_token: $csrf_token,
+                name: 'new API'
+            })
+
+            (result['status']).should.equal('success')
+
+            row = $database.getRow('apikey', 1, 'id')
+
+            (row).should.equal(nil)
+        end
+
+end
diff --git a/tests/system/get-all-keys.rb b/tests/system/get-all-keys.rb
new file mode 100644
index 00000000..a604af3d
--- /dev/null
+++ b/tests/system/get-all-keys.rb
@@ -0,0 +1,26 @@
+describe'system/get-all-keys' do
+        request('/user/logout')
+        Scripts.login($staff[:email], $staff[:password], true)
+
+        it 'should get all  API keys' do
+            Scripts.createAPIKey('namekey1')
+            Scripts.createAPIKey('namekey2')
+            Scripts.createAPIKey('namekey3')
+            Scripts.createAPIKey('namekey4')
+            Scripts.createAPIKey('namekey5')
+            
+            result= request('/system/get-all-keys', {
+                csrf_userid: $csrf_userid,
+                csrf_token: $csrf_token,
+            })
+
+            (result['status']).should.equal('success')
+            (result['data'][0]['name']).should.equal('namekey1')
+            (result['data'][1]['name']).should.equal('namekey2')
+            (result['data'][2]['name']).should.equal('namekey3')
+            (result['data'][3]['name']).should.equal('namekey4')
+            (result['data'][4]['name']).should.equal('namekey5')
+
+        end
+
+end

From dffe4a87a0b0062085b4fdeee5b209998688f333 Mon Sep 17 00:00:00 2001
From: Ivan Diaz <ivan@opensupports.com>
Date: Fri, 13 Jan 2017 21:58:59 -0300
Subject: [PATCH 17/21] Ivan - Add file upload  and download test [skip ci]

---
 server/controllers/system/init-settings.php |  2 +-
 tests/init.rb                               |  1 +
 tests/libs.rb                               |  7 ++
 tests/system/fiile-upload-download.rb       | 24 -------
 tests/system/file-upload-download.rb        | 74 +++++++++++++++++++++
 tests/system/get-stats.rb                   | 12 ++--
 6 files changed, 89 insertions(+), 31 deletions(-)
 delete mode 100644 tests/system/fiile-upload-download.rb
 create mode 100644 tests/system/file-upload-download.rb

diff --git a/server/controllers/system/init-settings.php b/server/controllers/system/init-settings.php
index 31536cba..b5d7c6d3 100644
--- a/server/controllers/system/init-settings.php
+++ b/server/controllers/system/init-settings.php
@@ -42,7 +42,7 @@ class InitSettingsController extends Controller {
             'title' => 'Support Center',
             'url' => 'http://www.opensupports.com/support',
             'registration' => true,
-            'last-stat-day' => '20170101' //TODO: get current date
+            'last-stat-day' => '20170101', //TODO: get current date
             'ticket-gap' => Hashing::generateRandomPrime(100000, 999999),
             'file-gap' => Hashing::generateRandomPrime(100000, 999999),
             'file-first-number' => Hashing::generateRandomNumber(100000, 999999),
diff --git a/tests/init.rb b/tests/init.rb
index cae53d63..f98e9cda 100644
--- a/tests/init.rb
+++ b/tests/init.rb
@@ -55,3 +55,4 @@ require './system/recover-mail-template.rb'
 require './system/disable-registration.rb'
 require './system/enable-registration.rb'
 require './system/get-stats.rb'
+require './system/file-upload-download.rb'
diff --git a/tests/libs.rb b/tests/libs.rb
index 8b832429..fef8e646 100644
--- a/tests/libs.rb
+++ b/tests/libs.rb
@@ -1,5 +1,12 @@
 $agent = Mechanize.new
 
+def plainRequest(path, data = {})
+    uri = 'http://localhost:8080' + path
+    response = $agent.post(uri, data)
+
+    return response
+end
+
 def request(path, data = {})
     uri = 'http://localhost:8080' + path
     response = $agent.post(uri, data)
diff --git a/tests/system/fiile-upload-download.rb b/tests/system/fiile-upload-download.rb
deleted file mode 100644
index ebaa8345..00000000
--- a/tests/system/fiile-upload-download.rb
+++ /dev/null
@@ -1,24 +0,0 @@
-describe 'File Upload and Download' do
-    request('/user/logout')
-    Scripts.login("creator@os4.com", "creator")
-
-    it 'should upload file when creating ticket' do
-        file = File.new('upload.txt', 'w')
-        file.puts('file content')
-
-        result = request('/ticket/create', {
-            'csrf_userid' => $csrf_userid,
-            'csrf_token' => $csrf_token,
-            'title' => 'Ticket with file',
-            'content' => 'this is a ticket that contains a file',
-            'language' => 'en',
-            'file' => file
-        })
-        (result['status']).should.equal('success');
-
-        ticket = $database.getLastRow('ticket');
-
-        (ticket['file'].include? 'upload.txt').should.equal(true)
-        (File.exist?('../server/files' + ticket['file'])).should.equal(true)
-    end
-end
diff --git a/tests/system/file-upload-download.rb b/tests/system/file-upload-download.rb
new file mode 100644
index 00000000..44bc3c37
--- /dev/null
+++ b/tests/system/file-upload-download.rb
@@ -0,0 +1,74 @@
+describe 'File Upload and Download' do
+    request('/user/logout')
+    Scripts.login('creator@os4.com', 'creator')
+
+    it 'should upload file when creating ticket' do
+        file = File.new('../server/files/upload.txt', 'w+')
+        file.puts('file content')
+        file.close
+
+        result = request('/ticket/create', {
+            'csrf_userid' => $csrf_userid,
+            'csrf_token' => $csrf_token,
+            'title' => 'Ticket with file',
+            'content' => 'this is a ticket that contains a file',
+            'language' => 'en',
+            'departmentId' => 1,
+            'file' => File.open( "../server/files/upload.txt")
+        })
+        (result['status']).should.equal('success')
+
+        ticket = $database.getLastRow('ticket')
+
+        (ticket['file'].include? 'upload.txt').should.equal(true)
+        (File.exist? ('../server/files/' + ticket['file'])).should.equal(true)
+    end
+
+    it 'should download file if author is logged' do
+        ticket = $database.getLastRow('ticket')
+        file = File.open("../server/files/" + ticket['file'])
+
+        result = plainRequest('/system/download', {
+            'csrf_userid' => $csrf_userid,
+            'csrf_token' => $csrf_token,
+            'file' => ticket['file']
+        })
+
+        (result.body).should.equal(file.read)
+    end
+
+    it 'should not download if author is not logged' do
+        request('/user/logout')
+        Scripts.login('staff@opensupports.com', 'staff', true)
+
+        ticket = $database.getLastRow('ticket')
+
+        result = plainRequest('/system/download', {
+            'csrf_userid' => $csrf_userid,
+            'csrf_token' => $csrf_token,
+            'file' => ticket['file']
+        })
+
+        (result.body).should.equal('')
+    end
+
+    it 'should download if owner is logged' do
+        ticket = $database.getLastRow('ticket')
+        file = File.open("../server/files/" + ticket['file'])
+
+        request('/staff/assign-ticket', {
+            'csrf_userid' => $csrf_userid,
+            'csrf_token' => $csrf_token,
+            'ticketNumber' => ticket['ticket_number']
+        })
+
+        result = plainRequest('/system/download', {
+            'csrf_userid' => $csrf_userid,
+            'csrf_token' => $csrf_token,
+            'file' => ticket['file']
+        })
+
+        (result.body).should.equal(file.read)
+    end
+
+end
diff --git a/tests/system/get-stats.rb b/tests/system/get-stats.rb
index 406c4516..04d31e83 100644
--- a/tests/system/get-stats.rb
+++ b/tests/system/get-stats.rb
@@ -25,10 +25,10 @@ describe'/system/get-stats' do
             $database.query("INSERT INTO log VALUES('', 'COMMENT', NULL, " + yesterday3 + ", NULL, NULL);")
         end
         for i in 0..8
-            $database.query("INSERT INTO ticketevent VALUES('', 'CLOSE', NULL, " + yesterday3 + ", NULL, NULL, 1);")
+            $database.query("INSERT INTO ticketevent VALUES('', 'CLOSE', NULL, NULL, " + yesterday3 + ", NULL, NULL, 1);")
         end
         for i in 0..4
-            $database.query("INSERT INTO ticketevent VALUES('', 'ASSIGN', NULL, " + yesterday3 + ", NULL, NULL, 1);")
+            $database.query("INSERT INTO ticketevent VALUES('', 'ASSIGN', NULL, NULL, " + yesterday3 + ", NULL, NULL, 1);")
         end
 
         #day 2
@@ -45,10 +45,10 @@ describe'/system/get-stats' do
             $database.query("INSERT INTO log VALUES('', 'COMMENT', NULL, " + yesterday2 + ", NULL, NULL);")
         end
         for i in 0..10
-            $database.query("INSERT INTO ticketevent VALUES('', 'CLOSE', NULL, " + yesterday2 + ", NULL, NULL, 1);")
+            $database.query("INSERT INTO ticketevent VALUES('', 'CLOSE', NULL, NULL, " + yesterday2 + ", NULL, NULL, 1);")
         end
         for i in 0..2
-            $database.query("INSERT INTO ticketevent VALUES('', 'ASSIGN', NULL, " + yesterday2 + ", NULL, NULL, 1);")
+            $database.query("INSERT INTO ticketevent VALUES('', 'ASSIGN', NULL, NULL, " + yesterday2 + ", NULL, NULL, 1);")
         end
 
         #day 3
@@ -65,10 +65,10 @@ describe'/system/get-stats' do
             $database.query("INSERT INTO log VALUES('', 'COMMENT', NULL, " + yesterday + ", NULL, NULL);")
         end
         for i in 0..3
-            $database.query("INSERT INTO ticketevent VALUES('', 'CLOSE', NULL, " + yesterday + ", NULL, NULL, 1);")
+            $database.query("INSERT INTO ticketevent VALUES('', 'CLOSE', NULL, NULL, " + yesterday + ", NULL, NULL, 1);")
         end
         for i in 0..7
-            $database.query("INSERT INTO ticketevent VALUES('', 'ASSIGN', NULL, " + yesterday + ", NULL, NULL, 1);")
+            $database.query("INSERT INTO ticketevent VALUES('', 'ASSIGN', NULL, NULL, " + yesterday + ", NULL, NULL, 1);")
         end
 
         @result = request('/system/get-stats', {

From d575882bf2b2009ea3cde94067cf0fae4616bd52 Mon Sep 17 00:00:00 2001
From: Ivan Diaz <ivan@opensupports.com>
Date: Sat, 14 Jan 2017 16:39:22 -0300
Subject: [PATCH 18/21] Ivan - Add file uploader component [skip ci]

---
 .../create-ticket-form.js                     |  3 ++
 .../create-ticket-form.scss                   |  4 ++
 client/src/core-components/file-uploader.js   | 40 +++++++++++++++++++
 client/src/core-components/file-uploader.scss | 31 ++++++++++++++
 client/src/core-components/form-field.js      |  9 +++--
 client/src/lib-core/APIUtils.js               |  3 +-
 6 files changed, 86 insertions(+), 4 deletions(-)
 create mode 100644 client/src/core-components/file-uploader.js
 create mode 100644 client/src/core-components/file-uploader.scss

diff --git a/client/src/app/main/dashboard/dashboard-create-ticket/create-ticket-form.js b/client/src/app/main/dashboard/dashboard-create-ticket/create-ticket-form.js
index 83b012bc..f8898b8f 100644
--- a/client/src/app/main/dashboard/dashboard-create-ticket/create-ticket-form.js
+++ b/client/src/app/main/dashboard/dashboard-create-ticket/create-ticket-form.js
@@ -56,6 +56,9 @@ class CreateTicketForm extends React.Component {
                         }}/>
                     </div>
                     <FormField label={i18n('CONTENT')} name="content" validation="TEXT_AREA" required field="textarea" />
+                    <div className="create-ticket-form__file">
+                        <FormField name="file" field="file" />
+                    </div>
                     {(!this.props.userLogged) ? this.renderCaptcha() : null}
                     <SubmitButton>Create Ticket</SubmitButton>
                 </Form>
diff --git a/client/src/app/main/dashboard/dashboard-create-ticket/create-ticket-form.scss b/client/src/app/main/dashboard/dashboard-create-ticket/create-ticket-form.scss
index 045c290c..bc7d906e 100644
--- a/client/src/app/main/dashboard/dashboard-create-ticket/create-ticket-form.scss
+++ b/client/src/app/main/dashboard/dashboard-create-ticket/create-ticket-form.scss
@@ -1,5 +1,9 @@
 .create-ticket-form {
 
+    &__file {
+        text-align: left;
+    }
+
     &__message {
         margin-top: 20px;
     }
diff --git a/client/src/core-components/file-uploader.js b/client/src/core-components/file-uploader.js
new file mode 100644
index 00000000..c18771ad
--- /dev/null
+++ b/client/src/core-components/file-uploader.js
@@ -0,0 +1,40 @@
+import React from 'react';
+
+import Button from 'core-components/button';
+import Icon from 'core-components/icon';
+
+class FileUploader extends React.Component {
+    static propTypes = {
+        text: React.PropTypes.string,
+        value: React.PropTypes.object,
+        onChange: React.PropTypes.func
+    };
+
+    static defaultProps = {
+        text: 'Upload file'
+    };
+
+    render() {
+        return (
+            <label className="file-uploader">
+                <input className="file-uploader__input" type="file" multiple={false} onChange={this.onChange.bind(this)}/>
+                <span className="file-uploader__custom" tabIndex="0">
+                    <Icon className="file-uploader__icon" name="upload" /> {this.props.text}
+                </span>
+                <span className="file-uploader__value">{this.props.value && this.props.value.name}</span>
+            </label>
+        );
+    }
+
+    onChange(event) {
+        if(this.props.onChange) {
+            this.props.onChange({
+                target: {
+                    value: event.target.files[0]
+                }
+            });
+        }
+    }
+}
+
+export default FileUploader;
\ No newline at end of file
diff --git a/client/src/core-components/file-uploader.scss b/client/src/core-components/file-uploader.scss
new file mode 100644
index 00000000..b6c766e7
--- /dev/null
+++ b/client/src/core-components/file-uploader.scss
@@ -0,0 +1,31 @@
+@import "../scss/vars";
+
+.file-uploader {
+
+    &__input {
+        width: 0.1px;
+        height: 0.1px;
+        opacity: 0;
+        overflow: hidden;
+        position: absolute;
+        z-index: -1;
+    }
+
+    &__custom {
+        display: inline-block;
+        cursor: pointer;
+        color: white;
+        background-color: $primary-red;
+        line-height: 35px;
+        padding: 0 15px;
+    }
+
+    &__icon {
+        margin-right: 15px;
+    }
+
+    &__value {
+        margin-left: 10px;
+        color: $dark-grey;
+    }
+}
\ No newline at end of file
diff --git a/client/src/core-components/form-field.js b/client/src/core-components/form-field.js
index c913e959..d12dbe47 100644
--- a/client/src/core-components/form-field.js
+++ b/client/src/core-components/form-field.js
@@ -9,6 +9,7 @@ import Checkbox from 'core-components/checkbox';
 import CheckboxGroup from 'core-components/checkbox-group';
 import TextEditor from 'core-components/text-editor';
 import InfoTooltip from 'core-components/info-tooltip';
+import FileUploader from 'core-components/file-uploader';
 
 class FormField extends React.Component {
     static contextTypes = {
@@ -24,7 +25,7 @@ class FormField extends React.Component {
         error: React.PropTypes.string,
         infoMessage: React.PropTypes.node,
         value: React.PropTypes.any,
-        field: React.PropTypes.oneOf(['input', 'textarea', 'select', 'checkbox', 'checkbox-group']),
+        field: React.PropTypes.oneOf(['input', 'textarea', 'select', 'checkbox', 'checkbox-group', 'file']),
         fieldProps: React.PropTypes.object
     };
     
@@ -84,7 +85,8 @@ class FormField extends React.Component {
             'textarea': TextEditor,
             'select': DropDown,
             'checkbox': Checkbox,
-            'checkbox-group': CheckboxGroup
+            'checkbox-group': CheckboxGroup,
+            'file': FileUploader
         }[this.props.field];
 
         if(this.props.decorator) {
@@ -142,7 +144,8 @@ class FormField extends React.Component {
     getDivTypes() {
         return [
             'textarea',
-            'checkbox-group'
+            'checkbox-group',
+            'file'
         ];
     }
 
diff --git a/client/src/lib-core/APIUtils.js b/client/src/lib-core/APIUtils.js
index 2ba05eea..65b52ae6 100644
--- a/client/src/lib-core/APIUtils.js
+++ b/client/src/lib-core/APIUtils.js
@@ -9,7 +9,8 @@ const APIUtils = {
                 url: path,
                 method: method,
                 data: data,
-                dataType: 'json'
+                processData: false,
+                contentType: false
             })
             .done(resolve)
             .fail((jqXHR, textStatus) => {

From 35d9a166cfbe28c212e9873f97cb6f356686d4d9 Mon Sep 17 00:00:00 2001
From: Ivan Diaz <ivan@opensupports.com>
Date: Sat, 14 Jan 2017 21:44:20 -0300
Subject: [PATCH 19/21] Ivan - Add file download component [skip ci]

---
 client/src/app-components/ticket-event.js    | 22 ++++++++++++++++++--
 client/src/app-components/ticket-event.scss  |  8 +++++++
 client/src/app-components/ticket-viewer.js   |  5 ++++-
 client/src/app-components/ticket-viewer.scss |  7 -------
 client/src/data/fixtures/system-fixtures.js  |  8 +++++++
 client/src/data/fixtures/user-fixtures.js    |  8 +++----
 client/src/lib-app/api-call.js               |  4 ++--
 client/src/lib-app/fixtures-loader.js        |  2 +-
 server/controllers/system/download.php       |  2 +-
 9 files changed, 48 insertions(+), 18 deletions(-)

diff --git a/client/src/app-components/ticket-event.js b/client/src/app-components/ticket-event.js
index bdcd9b2e..719f417d 100644
--- a/client/src/app-components/ticket-event.js
+++ b/client/src/app-components/ticket-event.js
@@ -2,6 +2,7 @@ import React from 'react';
 import classNames from 'classnames';
 
 import i18n from 'lib-app/i18n';
+import API from 'lib-app/api-call';
 
 import DateTransformer from 'lib-core/date-transformer';
 import Icon from 'core-components/icon';
@@ -161,7 +162,7 @@ class TicketEvent extends React.Component {
         }
 
         return (
-            <div className="ticket-viewer__file">
+            <div className="ticket-event__file">
                 {node}
             </div>
         )
@@ -222,9 +223,26 @@ class TicketEvent extends React.Component {
         const fileName = filePath.replace(/^.*[\\\/]/, '');
 
         return (
-            <a href={filePath} target="_blank">{fileName}</a>
+            <span onClick={this.onFileClick.bind(this, filePath)}>{fileName}</span>
         )
     }
+
+    onFileClick(filePath) {
+        API.call({
+            path: '/system/download',
+            plain: true,
+            data: {
+                file: filePath
+            }
+        }).then((result) => {
+            let contentType = 'application/octet-stream';
+            let link = document.createElement('a');
+            let blob = new Blob([result], {'type': contentType});
+            link.href = window.URL.createObjectURL(blob);
+            link.download = filePath;
+            link.click();
+        });
+    }
 }
 
 export default TicketEvent;
diff --git a/client/src/app-components/ticket-event.scss b/client/src/app-components/ticket-event.scss
index 50105bbe..328d16e1 100644
--- a/client/src/app-components/ticket-event.scss
+++ b/client/src/app-components/ticket-event.scss
@@ -91,6 +91,14 @@
         }
     }
 
+    &__file {
+        background-color: $very-light-grey;
+        cursor: pointer;
+        text-align: right;
+        padding: 5px 10px;
+        font-size: 12px;
+    }
+
     &_staff {
         .ticket-event__icon {
             background-color: $primary-blue;
diff --git a/client/src/app-components/ticket-viewer.js b/client/src/app-components/ticket-viewer.js
index 6b6f1362..c00b875a 100644
--- a/client/src/app-components/ticket-viewer.js
+++ b/client/src/app-components/ticket-viewer.js
@@ -190,6 +190,7 @@ class TicketViewer extends React.Component {
                 <div className="ticket-viewer__response-field row">
                     <Form {...this.getCommentFormProps()}>
                         <FormField name="content" validation="TEXT_AREA" required field="textarea" />
+                        <FormField name="file" field="file"/>
                         <SubmitButton>{i18n('RESPOND_TICKET')}</SubmitButton>
                     </Form>
                 </div>
@@ -234,10 +235,12 @@ class TicketViewer extends React.Component {
             loading: this.state.loading,
             onChange: (formState) => {this.setState({
                 commentValue: formState.content,
+                commentFile: formState.file,
                 commentEdited: true
             })},
             values: {
-                'content': this.state.commentValue
+                'content': this.state.commentValue,
+                'file': this.state.commentFile
             }
         };
     }
diff --git a/client/src/app-components/ticket-viewer.scss b/client/src/app-components/ticket-viewer.scss
index 9f03fbf7..2a6658ed 100644
--- a/client/src/app-components/ticket-viewer.scss
+++ b/client/src/app-components/ticket-viewer.scss
@@ -48,13 +48,6 @@
         margin-top: 10px;
     }
 
-    &__file {
-        background-color: $very-light-grey;
-        text-align: right;
-        padding: 5px 10px;
-        font-size: 12px;
-    }
-
     &__comments {
         position: relative;
     }
diff --git a/client/src/data/fixtures/system-fixtures.js b/client/src/data/fixtures/system-fixtures.js
index b67d450f..ac2a5bbd 100644
--- a/client/src/data/fixtures/system-fixtures.js
+++ b/client/src/data/fixtures/system-fixtures.js
@@ -110,6 +110,14 @@ module.exports = [
             };
         }
     },
+    {
+        path: '/system/download',
+        time: 100,
+        contentType: 'application/octet-stream',
+        response: function () {
+            return 'text content';
+        }
+    },
     {
         path: '/system/get-mail-templates',
         time: 100,
diff --git a/client/src/data/fixtures/user-fixtures.js b/client/src/data/fixtures/user-fixtures.js
index 484d7c95..2ff9801b 100644
--- a/client/src/data/fixtures/user-fixtures.js
+++ b/client/src/data/fixtures/user-fixtures.js
@@ -166,7 +166,7 @@ module.exports = [
                                 name: 'Sales Support'
                             },
                             date: '201504090001',
-                            file: 'http://www.opensupports.com/some_file.zip',
+                            file: 'some_file.txt',
                             language: 'en',
                             unread: false,
                             closed: false,
@@ -385,7 +385,7 @@ module.exports = [
                                 name: 'Technical Issues'
                             },
                             date: '201604161427',
-                            file: 'http://www.opensupports.com/some_file.zip',
+                            file: 'some_file.txt',
                             language: 'en',
                             unread: true,
                             closed: false,
@@ -504,7 +504,7 @@ module.exports = [
                                 name: 'Sales Support'
                             },
                             date: '201604150849',
-                            file: 'http://www.opensupports.com/some_file.zip',
+                            file: 'some_file.txt',
                             language: 'en',
                             unread: false,
                             closed: false,
@@ -607,7 +607,7 @@ module.exports = [
                                 name: 'Sales Support'
                             },
                             date: '201504091032',
-                            file: 'http://www.opensupports.com/some_file.zip',
+                            file: 'some_file.txt',
                             language: 'en',
                             unread: false,
                             closed: false,
diff --git a/client/src/lib-app/api-call.js b/client/src/lib-app/api-call.js
index f65ba752..032c5bc0 100644
--- a/client/src/lib-app/api-call.js
+++ b/client/src/lib-app/api-call.js
@@ -12,13 +12,13 @@ function processData (data) {
 }
 
 module.exports = {
-    call: function ({path, data}) {
+    call: function ({path, data, plain}) {
         return new Promise(function (resolve, reject) {
             APIUtils.post(root + path, processData(data))
                 .then(function (result) {
                     console.log(result);
 
-                    if (result.status === 'success') {
+                    if (plain || result.status === 'success') {
                         resolve(result);
                     } else if (reject) {
                         reject(result);
diff --git a/client/src/lib-app/fixtures-loader.js b/client/src/lib-app/fixtures-loader.js
index 1850cb34..dc02c382 100644
--- a/client/src/lib-app/fixtures-loader.js
+++ b/client/src/lib-app/fixtures-loader.js
@@ -24,7 +24,7 @@ fixtures.add(require('data/fixtures/article-fixtures'));
 
 _.each(fixtures.getAll(), function (fixture) {
     mockjax({
-        contentType: 'application/json',
+        contentType: fixture.contentType || 'application/json',
         url: 'http://localhost:3000/api' + fixture.path,
         responseTime: fixture.time || 500,
         response: function (settings) {
diff --git a/server/controllers/system/download.php b/server/controllers/system/download.php
index 8c6699b3..278ddc1b 100644
--- a/server/controllers/system/download.php
+++ b/server/controllers/system/download.php
@@ -10,7 +10,7 @@ class DownloadController extends Controller {
             'permission' => 'user',
             'requestData' => [
                 'file' => [
-                    'validation' => DataValidator::alnum('_.')->noWhitespace(),
+                    'validation' => DataValidator::alnum('_.-')->noWhitespace(),
                     'error' => ERRORS::INVALID_FILE
                 ]
             ]

From 575f34256aa6e7e77849e4acd53951a6b1db868d Mon Sep 17 00:00:00 2001
From: Ivan Diaz <ivan@opensupports.com>
Date: Sat, 14 Jan 2017 23:51:31 -0300
Subject: [PATCH 20/21] Ivan - Fix unit testing backend

---
 server/tests/__mocks__/APIKeyMock.php         | 12 ++++++++++++
 server/tests/__mocks__/UserMock.php           |  2 ++
 server/tests/libs/validations/captchaTest.php |  4 ++++
 3 files changed, 18 insertions(+)
 create mode 100644 server/tests/__mocks__/APIKeyMock.php

diff --git a/server/tests/__mocks__/APIKeyMock.php b/server/tests/__mocks__/APIKeyMock.php
new file mode 100644
index 00000000..7ef32d77
--- /dev/null
+++ b/server/tests/__mocks__/APIKeyMock.php
@@ -0,0 +1,12 @@
+<?php
+include_once 'tests/__mocks__/NullDataStoreMock.php';
+
+class APIKey extends \Mock {
+    public static $functionList = array();
+
+    public static function initStubs() {
+        parent::setStatics(array(
+            'getDataStore' => parent::stub()->returns(new NullDataStore()),
+        ));
+    }
+}
\ No newline at end of file
diff --git a/server/tests/__mocks__/UserMock.php b/server/tests/__mocks__/UserMock.php
index c8edb25a..d7913a23 100644
--- a/server/tests/__mocks__/UserMock.php
+++ b/server/tests/__mocks__/UserMock.php
@@ -5,6 +5,7 @@ class User extends \Mock {
     public static function initStubs() {
         parent::setStatics(array(
             'authenticate' => parent::stub()->returns(self::getUserInstanceMock()),
+            'getDataStore' => parent::stub()->returns(self::getUserInstanceMock())
         ));
     }
     
@@ -18,6 +19,7 @@ class User extends \Mock {
         $mockUserInstance->id = 'MOCK_ID';
         $mockUserInstance->email = 'MOCK_EMAIL';
         $mockUserInstance->password = 'MOCK_PASSWORD';
+        $mockUserInstance->verificationToken = null;
 
         return $mockUserInstance;
     }
diff --git a/server/tests/libs/validations/captchaTest.php b/server/tests/libs/validations/captchaTest.php
index e2e86eaf..c85bfcf7 100644
--- a/server/tests/libs/validations/captchaTest.php
+++ b/server/tests/libs/validations/captchaTest.php
@@ -2,6 +2,8 @@
 include_once 'tests/__lib__/Mock.php';
 include_once 'tests/__mocks__/RespectMock.php';
 include_once 'tests/__mocks__/SettingMock.php';
+include_once 'tests/__mocks__/APIKeyMock.php';
+include_once 'tests/__mocks__/ControllerMock.php';
 include_once 'tests/__mocks__/ReCaptchaMock.php';
 
 include_once 'libs/validations/captcha.php';
@@ -10,6 +12,8 @@ class CaptchaValidationTest extends PHPUnit_Framework_TestCase {
 
     protected function setUp() {
         Setting::initStubs();
+        Controller::initStubs();
+        APIKey::initStubs();
         \ReCaptcha\ReCaptcha::initVerify();
 
         $_SERVER['REMOTE_ADDR'] = 'MOCK_REMOTE';

From e8e29d8701ff06228291fdd6bd95cca8cd5ef3ea Mon Sep 17 00:00:00 2001
From: Ivan Diaz <ivan@opensupports.com>
Date: Sun, 15 Jan 2017 01:36:04 -0300
Subject: [PATCH 21/21] Ivan - Fix static ticketevent

---
 server/models/APIKey.php      | 1 +
 server/models/Ticketevent.php | 2 +-
 2 files changed, 2 insertions(+), 1 deletion(-)

diff --git a/server/models/APIKey.php b/server/models/APIKey.php
index 2cfc3783..91218cac 100644
--- a/server/models/APIKey.php
+++ b/server/models/APIKey.php
@@ -9,6 +9,7 @@ class APIKey extends DataStore {
             'token'
         ];
     }
+
     public function toArray() {
         return [
             'name' => $this->name,
diff --git a/server/models/Ticketevent.php b/server/models/Ticketevent.php
index 4bd7ebb1..a8a23fed 100644
--- a/server/models/Ticketevent.php
+++ b/server/models/Ticketevent.php
@@ -36,7 +36,7 @@ class Ticketevent extends DataStore {
         return $ticketEvent;
     }
 
-    public function getProps() {
+    public static function getProps() {
         return [
             'type',
             'content',