Browse Source

Merged in feature/annoy-button (pull request #14)

Feature/annoy button

Approved-by: Fela Maslen <fela.maslen@mubaloo.com>
Fela Maslen 7 năm trước cách đây
mục cha
commit
d83062d3ab

+ 1 - 0
.env.example

@@ -1,6 +1,7 @@
 SLACK_TOKEN=slacktoken
 SLACK_WEBHOOK_URL=https://hooks.slack.com/services/your/service/webhook/url
 SYNERGIST_URL=http://synergist.server/
+SYNERGIST_SUPERUSER=tips
 SYNERGIST_PASSWORD=resourceGuruPassword
 WHOSOFF_API_KEY=whosoffapikey
 PASSWORD=yoursecretpassword

+ 9 - 0
src/actions/annoy.js

@@ -0,0 +1,9 @@
+import {
+    ANNOYED,
+    ANNOY_RESPONDED
+} from 'constants/actions';
+
+export const annoyed = () => ({ type: ANNOYED });
+
+export const annoyResponded = (err = null) => ({ type: ANNOY_RESPONDED, err });
+

+ 13 - 7
src/components/App/index.js

@@ -2,6 +2,7 @@ import React from 'react';
 import PropTypes from 'prop-types';
 import { Provider } from 'react-redux';
 
+import AnnoyButton from 'containers/AnnoyButton';
 import EmployeeAdmin from 'components/EmployeeAdmin';
 import PhraseAdmin from 'components/PhraseAdmin';
 
@@ -9,13 +10,18 @@ import './style.scss';
 
 export default function App({ store }) {
     return (
-        <div className="gurubot2">
-            <Provider store={store}>
-                <h1>{'GuruBot2 Admin Panel'}</h1>
-                <EmployeeAdmin />
-                <PhraseAdmin />
-            </Provider>
-        </div>
+        <Provider store={store}>
+            <div className="gurubot2">
+                <header className="main-header">
+                    <h1>{'GuruBot2 Admin Panel'}</h1>
+                    <AnnoyButton />
+                </header>
+                <div className="main-body">
+                    <EmployeeAdmin />
+                    <PhraseAdmin />
+                </div>
+            </div>
+        </Provider>
     );
 }
 

+ 3 - 0
src/constants/actions.js

@@ -7,3 +7,6 @@ export const DOC_UPDATE_RESPONDED = 'DOC_UPDATE_RESPONDED';
 export const DOC_DELETED = 'DOC_DELETED';
 export const DOC_DELETE_RESPONDED = 'DOC_DELETE_RESPONDED';
 
+export const ANNOYED = 'ANNOYED';
+export const ANNOY_RESPONDED = 'ANNOY_RESPONDED';
+

+ 40 - 0
src/containers/AnnoyButton/index.js

@@ -0,0 +1,40 @@
+import { connect } from 'react-redux';
+import React from 'react';
+import PropTypes from 'prop-types';
+import classNames from 'classnames';
+
+import { annoyed } from 'actions/annoy';
+
+import './style.scss';
+
+const mapStateToProps = state => ({
+    loading: state.annoy.loading,
+    error: state.annoy.error
+});
+
+const mapDispatchToProps = dispatch => ({
+    onAnnoy: () => dispatch(annoyed())
+});
+
+function AnnoyButton({ loading, error, onAnnoy }) {
+    return (
+        <div className={classNames('annoy-button-wrapper', {
+            loading,
+            error
+        })}>
+            <button className="button-annoy"
+                onClick={onAnnoy}
+                disabled={loading}
+            >{'Annoy'}</button>
+        </div>
+    );
+}
+
+AnnoyButton.propTypes = {
+    loading: PropTypes.bool.isRequired,
+    error: PropTypes.bool.isRequired,
+    onAnnoy: PropTypes.func.isRequired
+};
+
+export default connect(mapStateToProps, mapDispatchToProps)(AnnoyButton);
+

+ 0 - 0
src/containers/AnnoyButton/style.scss


+ 37 - 0
src/reducers/annoy.js

@@ -0,0 +1,37 @@
+import { createReducerObject } from 'create-reducer-object';
+
+import {
+    ANNOYED,
+    ANNOY_RESPONDED
+} from 'constants/actions';
+
+const initialState = {
+    loading: false,
+    error: false
+};
+
+function onAnnoy(state) {
+    if (state.loading) {
+        return {};
+    }
+
+    return {
+        loading: true,
+        error: false
+    };
+}
+
+function onAnnoyed(state, { err }) {
+    return {
+        loading: false,
+        error: Boolean(err)
+    };
+}
+
+const reducerMap = {
+    [ANNOYED]: onAnnoy,
+    [ANNOY_RESPONDED]: onAnnoyed
+};
+
+export const annoy = createReducerObject(reducerMap, initialState);
+

+ 2 - 0
src/reducers/index.js

@@ -1,8 +1,10 @@
 import { combineReducers } from 'redux';
 
+import { annoy } from 'reducers/annoy';
 import { crud } from 'reducers/crud';
 
 export const rootReducer = combineReducers({
+    annoy,
     crud
 });
 

+ 18 - 0
src/sagas/annoy.js

@@ -0,0 +1,18 @@
+import { take, call, put } from 'redux-saga/effects';
+
+import { apiRequest } from 'sagas/api';
+
+import { ANNOYED } from 'constants/actions';
+
+import { annoyResponded } from 'actions/annoy';
+
+export function *annoySaga() {
+    while (true) {
+        yield take(ANNOYED);
+
+        const { err } = yield call(apiRequest, 'post', 'annoy');
+
+        yield put(annoyResponded(err));
+    }
+}
+

+ 25 - 0
src/sagas/api.js

@@ -0,0 +1,25 @@
+import { call } from 'redux-saga/effects';
+import axios from 'axios';
+
+export function *apiRequest(method, route, params = [], data = null) {
+    let path = `/api1/${route}`;
+    if (params.length) {
+        path = `${path}/${params.join('/')}`;
+    }
+
+    const args = [[axios, method], path];
+
+    if (['post', 'put'].includes(method)) {
+        args.push(data);
+    }
+
+    try {
+        const response = yield call(...args);
+
+        return { response, err: null };
+
+    } catch (err) {
+        return { response: null, err };
+    }
+}
+

+ 1 - 22
src/sagas/crud.js

@@ -1,5 +1,4 @@
 import { takeEvery, call, put } from 'redux-saga/effects';
-import axios from 'axios';
 
 import {
     DOC_CREATED,
@@ -15,27 +14,7 @@ import {
     docDeleteResponded
 } from 'actions/crud';
 
-export function *apiRequest(method, route, params = [], data = null) {
-    let path = `/api1/${route}`;
-    if (params.length) {
-        path = `${path}/${params.join('/')}`;
-    }
-
-    const args = [[axios, method], path];
-
-    if (['post', 'put'].includes(method)) {
-        args.push(data);
-    }
-
-    try {
-        const response = yield call(...args);
-
-        return { response, err: null };
-
-    } catch (err) {
-        return { response: null, err };
-    }
-}
+import { apiRequest } from 'sagas/api';
 
 export function *createDoc({ route, pendingId, fields }) {
     const { response, err } = yield call(apiRequest, 'post', route, [], fields);

+ 2 - 0
src/sagas/index.js

@@ -1,8 +1,10 @@
 import { fork } from 'redux-saga/effects';
 
 import { crudSaga } from 'sagas/crud';
+import { annoySaga } from 'sagas/annoy';
 
 export function *rootSaga() {
     yield fork(crudSaga);
+    yield fork(annoySaga);
 }
 

+ 1 - 0
src/server/config.js

@@ -18,6 +18,7 @@ module.exports = () => ({
     synergist: {
         version: 3,
         company: 1,
+        superuser: process.env.SYNERGIST_SUPERUSER || '',
         apiUrl: process.env.SYNERGIST_URL || '',
         password: process.env.SYNERGIST_PASSWORD
     },

+ 1 - 1
src/server/modules/logger.js

@@ -11,7 +11,7 @@ function getLogLevel(config) {
         return 'debug';
     }
     if (config.__PROD__) {
-        return 'error';
+        return 'info';
     }
 
     return 'verbose';

+ 3 - 3
src/server/modules/slack.js

@@ -24,17 +24,17 @@ function generateAnnoyMessage(config, now, usersList, phrases) {
 }
 
 function annoySlackUsers(config, logger, now, users, phrases) {
-    const usersList = ['foo', 'bar']; // users.map(({ id }) => `<@${id}>`);
+    const usersList = users.map(({ id }) => `<@${id}>`);
 
     const message = generateAnnoyMessage(config, now, usersList, phrases);
 
     if (!message) {
-        logger.debug('[SLACK] Not annoying users');
+        logger.info('[SLACK] No users to annoy');
 
         return null;
     }
 
-    logger.debug('[SLACK] Annoying users...');
+    logger.info('[SLACK] Annoying users:', users.map(({ name }) => name));
 
     const t0 = Date.now();
 

+ 8 - 6
src/server/modules/synergist.js

@@ -1,12 +1,12 @@
 const request = require('request-promise');
 const humanizeDuration = require('humanize-duration');
 
-function getTimesheetSummaryTotalsRequest(config, username, password, date) {
+function getTimesheetSummaryTotalsRequest(config, username, date) {
     const options = {
         uri: `${config.synergist.apiUrl}jsonapi/timesheets.json`,
         qs: {
             username,
-            password,
+            password: config.synergist.password,
             date,
             user: config.synergist.superuser,
             version: config.synergist.version,
@@ -19,9 +19,9 @@ function getTimesheetSummaryTotalsRequest(config, username, password, date) {
 }
 
 function makeGetTimesheetCompleted(config, logger, date) {
-    return async (shortName, password) => {
+    return async shortName => {
         try {
-            const response = await getTimesheetSummaryTotalsRequest(config, shortName, password, date);
+            const response = await getTimesheetSummaryTotalsRequest(config, shortName, date);
 
             try {
                 const data = JSON.parse(response);
@@ -55,10 +55,12 @@ async function getUsersWithMissingTimesheets(config, logger, range, users) {
     logger.debug('[SYNERGIST] Getting employee timesheet information...');
     const t0 = Date.now();
 
-    const getTimesheetCompleted = makeGetTimesheetCompleted(config, logger, range.start);
+    const startDate = range.start.format('DD/MM/YYYY');
+
+    const getTimesheetCompleted = makeGetTimesheetCompleted(config, logger, startDate);
 
     const results = await Promise.all(users.map(user =>
-        getTimesheetCompleted(user.shortName, config.synergist.password)
+        getTimesheetCompleted(user.shortName)
     ));
 
     const t1 = Date.now();

+ 38 - 0
test/actions/annoy.spec.js

@@ -0,0 +1,38 @@
+import { expect } from 'chai';
+
+import {
+    ANNOYED,
+    ANNOY_RESPONDED
+} from 'constants/actions';
+
+import {
+    annoyed,
+    annoyResponded
+} from 'actions/annoy';
+
+describe('Annoy actions', () => {
+    describe('annoyed', () => {
+        it('should return ANNOYED', () => {
+            expect(annoyed()).to.deep.equal({
+                type: ANNOYED
+            });
+        });
+    });
+
+    describe('annoyResponded', () => {
+        it('should return ANNOY_RESPONDED with optional error', () => {
+            const err = new Error('something bad happened');
+
+            expect(annoyResponded(err)).to.deep.equal({
+                type: ANNOY_RESPONDED,
+                err
+            });
+
+            expect(annoyResponded()).to.deep.equal({
+                type: ANNOY_RESPONDED,
+                err: null
+            });
+        });
+    });
+});
+

+ 45 - 0
test/sagas/annoy.spec.js

@@ -0,0 +1,45 @@
+/* eslint-disable prefer-reflect */
+import { testSaga } from 'redux-saga-test-plan';
+
+import {
+    annoySaga
+} from 'sagas/annoy';
+
+import {
+    apiRequest
+} from 'sagas/api';
+
+import {
+    ANNOYED
+} from 'constants/actions';
+
+import {
+    annoyResponded
+} from 'actions/annoy';
+
+describe('Annoy saga', () => {
+    it('should listen to ANNOYED and send an API request', () => {
+        testSaga(annoySaga)
+            .next()
+            .take(ANNOYED)
+            .next()
+            .call(apiRequest, 'post', 'annoy')
+            .next({ err: null, response: { isResponse: true } })
+            .put(annoyResponded(null))
+            .next()
+            .take(ANNOYED);
+
+        const err = new Error('something bad happened');
+
+        testSaga(annoySaga)
+            .next()
+            .take(ANNOYED)
+            .next()
+            .call(apiRequest, 'post', 'annoy')
+            .next({ err, response: null })
+            .put(annoyResponded(err))
+            .next()
+            .take(ANNOYED);
+    });
+});
+

+ 48 - 0
test/sagas/api.spec.js

@@ -0,0 +1,48 @@
+/* eslint-disable prefer-reflect */
+import { testSaga } from 'redux-saga-test-plan';
+import axios from 'axios';
+
+import {
+    apiRequest
+} from 'sagas/api';
+
+describe('API sagas', () => {
+    describe('apiRequest', () => {
+        const route = 'employees';
+        const fields = {
+            name: 'John Doe',
+            email: 'john.doe@mubaloo.com'
+        };
+
+        it('should make an API POST request', () => {
+            const response = { isResponse: true };
+
+            testSaga(apiRequest, 'post', route, [], fields)
+                .next()
+                .call([axios, 'post'], '/api1/employees', fields)
+                .next(response)
+                .returns({ response, err: null });
+        });
+
+        it('should handle errors', () => {
+            const err = new Error('something bad happened');
+
+            testSaga(apiRequest, 'post', route, [], fields)
+                .next()
+                .call([axios, 'post'], '/api1/employees', fields)
+                .throw(err)
+                .returns({ response: null, err });
+        });
+
+        it('should make an API GET request', () => {
+            const response = { isResponse: true };
+
+            testSaga(apiRequest, 'get', route, ['a0b'])
+                .next()
+                .call([axios, 'get'], '/api1/employees/a0b')
+                .next(response)
+                .returns({ response, err: null });
+        });
+    });
+});
+

+ 4 - 44
test/sagas/crud.spec.js

@@ -1,20 +1,18 @@
 /* eslint-disable prefer-reflect */
 import { testSaga } from 'redux-saga-test-plan';
-import axios from 'axios';
 
 import {
-    apiRequest,
     createDoc,
-    createListener,
     readDoc,
-    readListener,
     updateDoc,
-    updateListener,
     deleteDoc,
-    deleteListener,
     crudSaga
 } from 'sagas/crud';
 
+import {
+    apiRequest
+} from 'sagas/api';
+
 import {
     DOC_CREATED,
     DOC_READ,
@@ -34,44 +32,6 @@ import {
 } from 'actions/crud';
 
 describe('crudSaga', () => {
-    describe('apiRequest', () => {
-        const route = 'employees';
-        const fields = {
-            name: 'John Doe',
-            email: 'john.doe@mubaloo.com'
-        };
-
-        it('should make an API POST request', () => {
-            const response = { isResponse: true };
-
-            testSaga(apiRequest, 'post', route, [], fields)
-                .next()
-                .call([axios, 'post'], '/api1/employees', fields)
-                .next(response)
-                .returns({ response, err: null });
-        });
-
-        it('should handle errors', () => {
-            const err = new Error('something bad happened');
-
-            testSaga(apiRequest, 'post', route, [], fields)
-                .next()
-                .call([axios, 'post'], '/api1/employees', fields)
-                .throw(err)
-                .returns({ response: null, err });
-        });
-
-        it('should make an API GET request', () => {
-            const response = { isResponse: true };
-
-            testSaga(apiRequest, 'get', route, ['a0b'])
-                .next()
-                .call([axios, 'get'], '/api1/employees/a0b')
-                .next(response)
-                .returns({ response, err: null });
-        });
-    });
-
     describe('createDoc', () => {
         const route = 'employees';
         const pendingId = '<pendingId>';

+ 6 - 0
test/sagas/index.spec.js

@@ -8,12 +8,18 @@ import {
     crudSaga
 } from 'sagas/crud';
 
+import {
+    annoySaga
+} from 'sagas/annoy';
+
 describe('Root saga', () => {
     it('should fork other sagas', () => {
         testSaga(rootSaga)
             .next()
             .fork(crudSaga)
             .next()
+            .fork(annoySaga)
+            .next()
             .isDone();
     });
 });