From 491c8eebfd181bc9da6f88da1d562229720c6231 Mon Sep 17 00:00:00 2001 From: Tristan Cole Date: Tue, 1 Dec 2020 10:01:07 +1100 Subject: [PATCH 1/6] Antify Message Bar (#566) * nuke messages * bump --- .../__fixtures__/message/messageData.ts | 4 -- .../reducers/message/actions.test.ts | 45 ---------------- .../reducers/message/reducers.test.ts | 52 ------------------- .../functional/sagas/messageSaga.test.ts | 31 ----------- app/client/components/messageBar.tsx | 50 ------------------ app/client/components/navBar/page.tsx | 2 - .../components/user/SingleUserManagement.tsx | 12 ++--- app/client/reducers/message/actions.ts | 10 ---- app/client/reducers/message/reducers.ts | 36 ------------- app/client/reducers/message/types.ts | 10 ---- app/client/reducers/rootReducer.ts | 2 - app/client/sagas/authSagas.ts | 25 +++------ app/client/sagas/businessVerificationSaga.ts | 26 +++------- app/client/sagas/creditTransferSagas.ts | 32 ++++-------- app/client/sagas/filterSaga.ts | 10 ++-- app/client/sagas/messageSaga.ts | 41 --------------- app/client/sagas/organisationSagas.ts | 14 ++--- app/client/sagas/rootSaga.ts | 2 - app/client/sagas/spreadsheetSagas.ts | 6 +-- app/client/sagas/transferAccountSagas.ts | 16 ++---- app/client/sagas/transferUsageSagas.ts | 6 +-- app/client/sagas/userSagas.ts | 32 +++--------- app/client/sagas/wyreSaga.ts | 22 ++------ app/package.json | 2 +- 24 files changed, 53 insertions(+), 435 deletions(-) delete mode 100644 app/client/__fixtures__/message/messageData.ts delete mode 100644 app/client/__tests__/functional/reducers/message/actions.test.ts delete mode 100644 app/client/__tests__/functional/reducers/message/reducers.test.ts delete mode 100644 app/client/__tests__/functional/sagas/messageSaga.test.ts delete mode 100644 app/client/components/messageBar.tsx delete mode 100644 app/client/reducers/message/actions.ts delete mode 100644 app/client/reducers/message/reducers.ts delete mode 100644 app/client/reducers/message/types.ts delete mode 100644 app/client/sagas/messageSaga.ts diff --git a/app/client/__fixtures__/message/messageData.ts b/app/client/__fixtures__/message/messageData.ts deleted file mode 100644 index 53b7369e0..000000000 --- a/app/client/__fixtures__/message/messageData.ts +++ /dev/null @@ -1,4 +0,0 @@ -export const AddMessageData = { - error: false, - message: "Fake Message" -}; diff --git a/app/client/__tests__/functional/reducers/message/actions.test.ts b/app/client/__tests__/functional/reducers/message/actions.test.ts deleted file mode 100644 index ddfd18dc2..000000000 --- a/app/client/__tests__/functional/reducers/message/actions.test.ts +++ /dev/null @@ -1,45 +0,0 @@ -import "../../../setup/setupTests.js"; -import store from "../../../../createStore.js"; -import { MessageActionTypes } from "../../../../reducers/message/types"; -import { MessageAction } from "../../../../reducers/message/actions"; -import { AddMessageData } from "../../../../__fixtures__/message/messageData"; - -describe("add message redux", () => { - beforeEach(() => { - store.clearActions(); - }); - - it("should dispatch the addMessage action", () => { - const expectedActions = [ - { - payload: AddMessageData, - type: MessageActionTypes.ADD_FLASH_MESSAGE - } - ]; - store.dispatch(MessageAction.addMessage(AddMessageData)); - - expect(store.getActions()).toEqual(expectedActions); - }); - - it("should dispatch the showFlash action", () => { - const expectedActions = [ - { - type: MessageActionTypes.SHOW_FLASH - } - ]; - store.dispatch(MessageAction.showFlash()); - - expect(store.getActions()).toEqual(expectedActions); - }); - - it("should dispatch the clearFlash action", () => { - const expectedActions = [ - { - type: MessageActionTypes.CLEAR_FLASH - } - ]; - store.dispatch(MessageAction.clearFlash()); - - expect(store.getActions()).toEqual(expectedActions); - }); -}); diff --git a/app/client/__tests__/functional/reducers/message/reducers.test.ts b/app/client/__tests__/functional/reducers/message/reducers.test.ts deleted file mode 100644 index 05c46829a..000000000 --- a/app/client/__tests__/functional/reducers/message/reducers.test.ts +++ /dev/null @@ -1,52 +0,0 @@ -import "../../../setup/setupTests.js"; -import { - message, - initialMessageState, - MessageState -} from "../../../../reducers/message/reducers"; -import { AddMessageData } from "../../../../__fixtures__/message/messageData"; -import { MessageAction } from "../../../../reducers/message/actions"; - -describe("message reducer", () => { - let updatedState: MessageState; - - beforeEach(() => { - updatedState = message( - initialMessageState, - MessageAction.addMessage(AddMessageData) - ); - }); - - it("add single message", () => { - expect(updatedState.messageList).toHaveLength(1); - expect(updatedState.messageList).toEqual([AddMessageData.message]); - }); - - it("add second message", () => { - const secondMessageState = message( - updatedState, - MessageAction.addMessage(AddMessageData) - ); - expect(secondMessageState.messageList).toHaveLength(2); - }); - - it("show flash", () => { - const showFlashState = message(updatedState, MessageAction.showFlash()); - expect(showFlashState.showMessage).toEqual(true); - }); - - it("add second message, clear flash", () => { - const secondMessageState = message( - updatedState, - MessageAction.addMessage(AddMessageData) - ); - expect(secondMessageState.messageList).toHaveLength(2); - - const clearFlashState = message( - secondMessageState, - MessageAction.clearFlash() - ); - expect(clearFlashState.messageList).toHaveLength(1); - expect(clearFlashState.showMessage).toEqual(false); - }); -}); diff --git a/app/client/__tests__/functional/sagas/messageSaga.test.ts b/app/client/__tests__/functional/sagas/messageSaga.test.ts deleted file mode 100644 index 75b4bc5a9..000000000 --- a/app/client/__tests__/functional/sagas/messageSaga.test.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { MessageAction } from "../../../reducers/message/actions"; -import { onAddFlashMessage } from "../../../sagas/messageSaga"; -import { recordSaga } from "../../setup/testUtils"; -import { AddMessageData } from "../../../__fixtures__/message/messageData"; -import { - initialMessageState, - message -} from "../../../reducers/message/reducers"; - -describe("messageSaga", () => { - it("onAddFlashMessage - no messages", async () => { - const dispatched = await recordSaga(onAddFlashMessage, null, { - message: initialMessageState - }); - - expect(dispatched).toEqual([]); - }); - - it("onAddFlashMessage - one message", async () => { - const updatedState = message( - initialMessageState, - MessageAction.addMessage(AddMessageData) - ); - - const dispatched = await recordSaga(onAddFlashMessage, null, { - message: updatedState - }); - - expect(dispatched).toEqual([MessageAction.showFlash()]); - }); -}); diff --git a/app/client/components/messageBar.tsx b/app/client/components/messageBar.tsx deleted file mode 100644 index f67a687e5..000000000 --- a/app/client/components/messageBar.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import React, { useState } from "react"; -import { useSelector } from "react-redux"; -import styled from "styled-components"; -import { PageWrapper } from "./styledElements.js"; -import { ReduxState } from "../reducers/rootReducer"; - -export default function MessageBar() { - const loggedIn = useSelector( - (state: ReduxState) => state.login.token != null - ); - const message = useSelector((state: ReduxState) => state.message); - - return ( - - - {message.messageList[0]} - - - ); -} - -const Message = styled.p` - width: calc(100% - 3em); - margin: 1em auto; - padding: 0.5em; - font-weight: 500; - color: white; - box-shadow: 0px 2px 0px 0 rgba(51, 51, 79, 0.08); - max-width: 300px; - transition-property: all; - transition-duration: 0.25s; - transition-timing-function: cubic-bezier(0, 1, 0.5, 1); -`; diff --git a/app/client/components/navBar/page.tsx b/app/client/components/navBar/page.tsx index 4caac7f1e..6fa2e817b 100644 --- a/app/client/components/navBar/page.tsx +++ b/app/client/components/navBar/page.tsx @@ -6,7 +6,6 @@ import NavBar from "../navBar"; import { isMobileQuery, withMediaQuery } from "../helpers/responsive"; import IntercomSetup from "../intercom/IntercomSetup"; -import MessageBar from "../messageBar"; import ErrorBoundary from "../ErrorBoundary"; import LoadingSpinner from "../loadingSpinner"; @@ -51,7 +50,6 @@ const Page: React.FunctionComponent = props => { return ( - {noNav ? null : ( diff --git a/app/client/components/user/SingleUserManagement.tsx b/app/client/components/user/SingleUserManagement.tsx index 1c02cb618..c79f8de20 100644 --- a/app/client/components/user/SingleUserManagement.tsx +++ b/app/client/components/user/SingleUserManagement.tsx @@ -1,11 +1,10 @@ import * as React from "react"; import { connect } from "react-redux"; +import { message } from "antd"; import { TransferUsage } from "../../reducers/transferUsage/types"; import { ReduxState } from "../../reducers/rootReducer"; import EditUserForm, { IEditUser } from "./EditUserForm"; -import { TransferAccountTypes } from "../transferAccount/types"; -import { MessageAction } from "../../reducers/message/actions"; import { DeleteUserAction, EditUserAction, @@ -16,13 +15,11 @@ import { ResetPinPayload, DeleteUserPayload } from "../../reducers/user/types"; -import { any } from "prop-types"; interface DispatchProps { editUser: (body: User, path: number) => EditUserAction; resetPin: (payload: ResetPinPayload) => ResetPinAction; deleteUser: (payload: DeleteUserPayload) => DeleteUserAction; - addMsg: (msg: string) => MessageAction; } interface StateProps { @@ -88,7 +85,7 @@ class SingleUserManagement extends React.Component { if (del === "DELETE") { this.props.deleteUser({ path: selectedUser.id }); } else { - this.props.addMsg("Action Canceled"); + message.error("Action Canceled"); } } @@ -119,10 +116,7 @@ const mapDispatchToProps = (dispatch: any): DispatchProps => { editUser: (body: User, path: number) => dispatch(EditUserAction.editUserRequest({ body, path })), resetPin: payload => dispatch(ResetPinAction.resetPinRequest(payload)), - deleteUser: payload => - dispatch(DeleteUserAction.deleteUserRequest(payload)), - addMsg: msg => - dispatch(MessageAction.addMessage({ error: true, message: msg })) + deleteUser: payload => dispatch(DeleteUserAction.deleteUserRequest(payload)) }; }; diff --git a/app/client/reducers/message/actions.ts b/app/client/reducers/message/actions.ts deleted file mode 100644 index a29bda33e..000000000 --- a/app/client/reducers/message/actions.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { AddMessagePayload, MessageActionTypes } from "./types"; -import { createAction, ActionsUnion } from "../../reduxUtils"; - -export const MessageAction = { - addMessage: (payload: AddMessagePayload) => - createAction(MessageActionTypes.ADD_FLASH_MESSAGE, payload), - showFlash: () => createAction(MessageActionTypes.SHOW_FLASH), - clearFlash: () => createAction(MessageActionTypes.CLEAR_FLASH) -}; -export type MessageAction = ActionsUnion; diff --git a/app/client/reducers/message/reducers.ts b/app/client/reducers/message/reducers.ts deleted file mode 100644 index 1b4eef023..000000000 --- a/app/client/reducers/message/reducers.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { MessageActionTypes } from "./types"; -import { MessageAction } from "./actions"; - -export interface MessageState { - showMessage: boolean; - messageList: string[]; - error: boolean; -} - -export const initialMessageState: MessageState = { - showMessage: false, - messageList: [], - error: false -}; - -export const message = (state = initialMessageState, action: MessageAction) => { - switch (action.type) { - case MessageActionTypes.ADD_FLASH_MESSAGE: - return { - ...state, - error: action.payload.error, - messageList: [...state.messageList, action.payload.message] - }; - case MessageActionTypes.SHOW_FLASH: - return { ...state, showMessage: true }; - case MessageActionTypes.CLEAR_FLASH: - return { - ...state, - showMessage: false, - messageList: state.messageList.slice(1), - error: false - }; - default: - return state; - } -}; diff --git a/app/client/reducers/message/types.ts b/app/client/reducers/message/types.ts deleted file mode 100644 index 214ab2c8c..000000000 --- a/app/client/reducers/message/types.ts +++ /dev/null @@ -1,10 +0,0 @@ -export enum MessageActionTypes { - ADD_FLASH_MESSAGE = "ADD_FLASH_MESSAGE", - SHOW_FLASH = "SHOW_FLASH", - CLEAR_FLASH = "CLEAR_FLASH" -} - -export interface AddMessagePayload { - error: boolean; - message: string; -} diff --git a/app/client/reducers/rootReducer.ts b/app/client/reducers/rootReducer.ts index 53d8bedbd..fdeca46f8 100755 --- a/app/client/reducers/rootReducer.ts +++ b/app/client/reducers/rootReducer.ts @@ -16,7 +16,6 @@ import { datasetList } from "./spreadsheet/reducers"; import { ExportReducer } from "./export/reducers"; -import { message } from "./message/reducers"; import { creditTransfers } from "./creditTransfer/reducers"; import { transferAccounts } from "./transferAccount/reducers"; import { users } from "./user/reducers"; @@ -43,7 +42,6 @@ const appReducer = combineReducers({ datasetSave, datasetList, export: ExportReducer, - message, transferAccounts, users, creditTransfers, diff --git a/app/client/sagas/authSagas.ts b/app/client/sagas/authSagas.ts index 0695c4772..1a0031537 100755 --- a/app/client/sagas/authSagas.ts +++ b/app/client/sagas/authSagas.ts @@ -7,6 +7,7 @@ import { select } from "redux-saga/effects"; import { normalize } from "normalizr"; +import { message } from "antd"; import { handleError, @@ -83,7 +84,6 @@ import { } from "../reducers/auth/actions"; import { browserHistory } from "../createStore"; -import { MessageAction } from "../reducers/message/actions"; import { OrganisationAction } from "../reducers/organisation/actions"; import { ActionWithPayload } from "../reduxUtils"; import { ReduxState } from "../reducers/rootReducer"; @@ -285,12 +285,7 @@ function* register( ) { // manual sign up, need to activate email yield put(RegisterAction.registerSuccess()); - yield put( - MessageAction.addMessage({ - error: false, - message: registered_account.message - }) - ); + message.success(registered_account.message); browserHistory.push("/login"); } else if (registered_account.auth_token && !registered_account.tfa_url) { storeSessionToken(registered_account.auth_token); @@ -471,13 +466,9 @@ function* updateUserRequest( } yield put(EditAdminUserAction.editAdminUserSuccess()); - - yield put( - MessageAction.addMessage({ error: false, message: result.message }) - ); + message.success(result.message); } catch (error) { - yield put(EditAdminUserAction.editAdminUserFailure(error)); - yield put(MessageAction.addMessage({ error: true, message: error })); + message.error(error); } } @@ -507,9 +498,7 @@ function* deleteInvite( delete invites[action.payload.body.invite_id]; yield put(InviteUserListAction.updateInviteUsers(invites)); - yield put( - MessageAction.addMessage({ error: false, message: result.message }) - ); + message.success(result.message); } catch (fetch_error) { const error = yield call(handleError, fetch_error); yield put(DeleteInviteAction.deleteInviteFailure(error.message)); @@ -529,9 +518,7 @@ function* inviteUserRequest( try { const result = yield call(inviteUserAPI, action.payload); yield put(InviteUserAction.inviteUserSuccess()); - yield put( - MessageAction.addMessage({ error: false, message: result.message }) - ); + message.success(result.message); browserHistory.push("/settings"); } catch (fetch_error) { const error = yield call(handleError, fetch_error); diff --git a/app/client/sagas/businessVerificationSaga.ts b/app/client/sagas/businessVerificationSaga.ts index 40bbb13e1..004026066 100644 --- a/app/client/sagas/businessVerificationSaga.ts +++ b/app/client/sagas/businessVerificationSaga.ts @@ -1,4 +1,5 @@ import { put, takeEvery, call, all } from "redux-saga/effects"; +import { message } from "antd"; import { BusinessVerificationAction } from "../reducers/businessVerification/actions"; import { @@ -19,7 +20,6 @@ import { createBankAccountAPI, editBankAccountAPI } from "../api/businessVerificationAPI"; -import { MessageAction } from "../reducers/message/actions"; import { handleError } from "../utils"; function* updateStateFromBusinessVerificationStep(data: any) { @@ -48,9 +48,7 @@ function* editBusinessVerification({ payload }: EditBusinessSagaPayload) { BusinessVerificationAction.editBusinessVerificationFailure(error) ); - yield put( - MessageAction.addMessage({ error: true, message: error.message }) - ); + message.error(error.message); } } @@ -113,10 +111,7 @@ function* createBusinessVerification({ yield put( BusinessVerificationAction.createBusinessVerificationFailure(error) ); - - yield put( - MessageAction.addMessage({ error: true, message: error.message }) - ); + message.error(error.message); } } @@ -139,10 +134,7 @@ function* uploadDocument({ payload }: UploadDocumentSagaPayload) { const error = yield call(handleError, fetch_error); yield put(BusinessVerificationAction.uploadDocumentFailure(error)); - - yield put( - MessageAction.addMessage({ error: true, message: error.message }) - ); + message.error(error.message); } } @@ -167,10 +159,7 @@ function* createBankAccount({ payload }: CreateBankAccountSagaPayload) { const error = yield call(handleError, fetch_error); yield put(BusinessVerificationAction.createBankAccountFailure(error)); - - yield put( - MessageAction.addMessage({ error: true, message: error.message }) - ); + message.error(error.message); } } @@ -195,10 +184,7 @@ function* editBankAccount({ payload }: EditBankAccountSagaPayload) { const error = yield call(handleError, fetch_error); yield put(BusinessVerificationAction.editBankAccountFailure(error)); - - yield put( - MessageAction.addMessage({ error: true, message: error.message }) - ); + message.error(error.message); } } diff --git a/app/client/sagas/creditTransferSagas.ts b/app/client/sagas/creditTransferSagas.ts index 77b24a787..7187b20d6 100644 --- a/app/client/sagas/creditTransferSagas.ts +++ b/app/client/sagas/creditTransferSagas.ts @@ -1,5 +1,6 @@ import { put, takeEvery, call, all } from "redux-saga/effects"; import { normalize } from "normalizr"; +import { message } from "antd"; import { LoadCreditTransferActionTypes, @@ -20,7 +21,6 @@ import { } from "../api/creditTransferAPI"; import { creditTransferSchema } from "../schemas"; import { handleError } from "../utils"; -import { MessageAction } from "../reducers/message/actions"; import { UserListAction } from "../reducers/user/actions"; import { MetricAction } from "../reducers/metric/actions"; import { TransferAccountAction } from "../reducers/transferAccount/actions"; @@ -64,19 +64,15 @@ function* updateStateFromCreditTransfer(result: CreditLoadApiResult) { credit_transfer_list ) ); - yield put( - MessageAction.addMessage({ error: false, message: result.message }) - ); + message.success(result.message); } if (result.bulk_responses) { // bulk transfers created! - yield put( - MessageAction.addMessage({ - error: result.bulk_responses[0].status !== 201, - message: result.bulk_responses[0].message - }) - ); + let firstResponse = result.bulk_responses[0]; + firstResponse.status !== 201 + ? message.error(firstResponse.message) + : message.success(firstResponse.message); } const metrics = result.data.transfer_stats; @@ -109,9 +105,7 @@ function* loadCreditTransferList({ payload }: CreditTransferListAPIResult) { yield put(LoadCreditTransferAction.loadCreditTransferListFailure(error)); - yield put( - MessageAction.addMessage({ error: true, message: error.message }) - ); + message.error(error.message); } } @@ -154,17 +148,13 @@ function* modifyTransfer({ yield put(ModifyCreditTransferAction.modifyTransferSuccess(result)); - yield put( - MessageAction.addMessage({ error: false, message: result.message }) - ); + message.success(result.message); } catch (fetch_error) { const error = yield call(handleError, fetch_error); yield put(ModifyCreditTransferAction.modifyTransferFailure(error)); - yield put( - MessageAction.addMessage({ error: true, message: error.message }) - ); + message.error(error.message); } } @@ -196,9 +186,7 @@ function* createTransfer({ yield put(CreditTransferAction.createTransferFailure(error)); - yield put( - MessageAction.addMessage({ error: true, message: error.message }) - ); + message.error(error.message); } } diff --git a/app/client/sagas/filterSaga.ts b/app/client/sagas/filterSaga.ts index c4e3a9aa1..1becbd9cf 100644 --- a/app/client/sagas/filterSaga.ts +++ b/app/client/sagas/filterSaga.ts @@ -1,5 +1,6 @@ import { put, takeEvery, call, all } from "redux-saga/effects"; import { normalize } from "normalizr"; +import { message } from "antd"; import { handleError } from "../utils"; import { filterSchema } from "../schemas"; @@ -17,7 +18,6 @@ import { } from "../reducers/filter/types"; import { loadSavedFilters, createFilterAPI } from "../api/filterAPI"; -import { MessageAction } from "../reducers/message/actions"; import { ActionWithPayload } from "../reduxUtils"; function* updateStateFromFilter(data: FilterData) { @@ -66,17 +66,13 @@ function* createFilter( yield put(CreateFilterAction.createFilterSuccess()); - yield put( - MessageAction.addMessage({ error: false, message: result.message }) - ); + message.success(result.message); } catch (fetch_error) { const error = yield call(handleError, fetch_error); yield put(CreateFilterAction.createFilterFailure(error.message)); - yield put( - MessageAction.addMessage({ error: true, message: error.message }) - ); + message.error(error.message); } } diff --git a/app/client/sagas/messageSaga.ts b/app/client/sagas/messageSaga.ts deleted file mode 100644 index 471099300..000000000 --- a/app/client/sagas/messageSaga.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { put, select, takeEvery, delay, all } from "redux-saga/effects"; - -import { ReduxState } from "../reducers/rootReducer"; -import { MessageAction } from "../reducers/message/actions"; -import { MessageActionTypes } from "../reducers/message/types"; - -const getMessages = (state: ReduxState): string[] => state.message.messageList; - -const FIVE_SECONDS = 5000; - -export function* showFlashMessage() { - yield put(MessageAction.showFlash()); - yield delay(FIVE_SECONDS); // wait 5 secs - yield put(MessageAction.clearFlash()); -} - -export function* onAddFlashMessage() { - while (true) { - const flashQueue = yield select(getMessages); - - // empty whats in the queue first - if (flashQueue.length > 0) { - while (true) { - const queue = yield select(getMessages); - if (queue.length === 0) { - break; - } - - yield showFlashMessage(); - } - } - } -} - -function* watchOnAddFlashMessage() { - yield takeEvery([MessageActionTypes.ADD_FLASH_MESSAGE], onAddFlashMessage); -} - -export default function* messageSagas() { - yield all([watchOnAddFlashMessage()]); -} diff --git a/app/client/sagas/organisationSagas.ts b/app/client/sagas/organisationSagas.ts index e05ab3b7e..c49a5d02d 100644 --- a/app/client/sagas/organisationSagas.ts +++ b/app/client/sagas/organisationSagas.ts @@ -1,4 +1,5 @@ import { put, takeEvery, call, all } from "redux-saga/effects"; +import { message } from "antd"; import { handleError } from "../utils"; import { normalize } from "normalizr"; @@ -21,7 +22,6 @@ import { } from "../api/organisationApi"; import { organisationSchema } from "../schemas"; -import { MessageAction } from "../reducers/message/actions"; import { ActionWithPayload } from "../reduxUtils"; function* updateStateFromOrganisation(data: OrganisationData) { @@ -53,9 +53,7 @@ function* loadOrganisation() { yield put(LoadOrganisationAction.loadOrganisationFailure(error.message)); - yield put( - MessageAction.addMessage({ error: true, message: error.message }) - ); + message.error(error.message); } } @@ -79,17 +77,13 @@ function* editOrganisation( yield put(EditOrganisationAction.editOrganisationSuccess()); - yield put( - MessageAction.addMessage({ error: false, message: load_result.message }) - ); + message.success(load_result.message); } catch (fetch_error) { const error = yield call(handleError, fetch_error); yield put(EditOrganisationAction.editOrganisationFailure(error.message)); - yield put( - MessageAction.addMessage({ error: true, message: error.message }) - ); + message.error(error.message); } } diff --git a/app/client/sagas/rootSaga.ts b/app/client/sagas/rootSaga.ts index a9cb88fdc..2cc586a5a 100755 --- a/app/client/sagas/rootSaga.ts +++ b/app/client/sagas/rootSaga.ts @@ -6,7 +6,6 @@ import credit_transferSagas from "./creditTransferSagas"; import transferAccountSagas from "./transferAccountSagas"; import newExportSaga from "./exportSaga"; import userSagas from "./userSagas"; -import messageSagas from "./messageSaga"; import filterSagas from "./filterSaga"; import businessVerificationSaga from "./businessVerificationSaga"; import wyreSaga from "./wyreSaga"; @@ -20,7 +19,6 @@ export default function* rootSaga() { authSagas(), spreadsheetSagas(), credit_transferSagas(), - messageSagas(), transferAccountSagas(), newExportSaga(), userSagas(), diff --git a/app/client/sagas/spreadsheetSagas.ts b/app/client/sagas/spreadsheetSagas.ts index b1359187e..e35558d7d 100644 --- a/app/client/sagas/spreadsheetSagas.ts +++ b/app/client/sagas/spreadsheetSagas.ts @@ -1,4 +1,5 @@ import { put, takeEvery, call, all } from "redux-saga/effects"; +import { message } from "antd"; import { browserHistory } from "../createStore"; import { SpreadsheetAction } from "../reducers/spreadsheet/actions"; @@ -14,7 +15,6 @@ import { loadDatasetListAPI } from "../api/spreadsheetAPI"; import { handleError } from "../utils"; -import { MessageAction } from "../reducers/message/actions"; function* spreadsheetUpload({ payload }: SpreadsheetUploadAPIRequest) { console.log("spreadsheetUpload", payload); @@ -31,9 +31,7 @@ function* spreadsheetUpload({ payload }: SpreadsheetUploadAPIRequest) { yield put(SpreadsheetAction.uploadSpreadsheetFailure(error)); - yield put( - MessageAction.addMessage({ error: true, message: error.message }) - ); + message.error(error.message); } } diff --git a/app/client/sagas/transferAccountSagas.ts b/app/client/sagas/transferAccountSagas.ts index 95ad55f59..ba64030eb 100644 --- a/app/client/sagas/transferAccountSagas.ts +++ b/app/client/sagas/transferAccountSagas.ts @@ -1,5 +1,6 @@ import { put, takeEvery, call, all } from "redux-saga/effects"; import { normalize } from "normalizr"; +import { message } from "antd"; import { handleError } from "../utils"; import { transferAccountSchema } from "../schemas"; @@ -24,7 +25,6 @@ import { editTransferAccountAPI } from "../api/transferAccountAPI"; -import { MessageAction } from "../reducers/message/actions"; import { UserListAction } from "../reducers/user/actions"; import { @@ -91,9 +91,7 @@ function* loadTransferAccounts({ payload }: TransferAccountLoadApiResult) { yield put(LoadTransferAccountAction.loadTransferAccountsFailure(error)); - yield put( - MessageAction.addMessage({ error: true, message: error.message }) - ); + message.error(error.message); } } @@ -111,18 +109,12 @@ function* editTransferAccount({ payload }: TransferAccountEditApiResult) { yield call(updateStateFromTransferAccount, edit_response.data); yield put(EditTransferAccountAction.editTransferAccountSuccess()); - - yield put( - MessageAction.addMessage({ error: false, message: edit_response.message }) - ); + message.success(edit_response.message); } catch (fetch_error) { const error = yield call(handleError, fetch_error); yield put(EditTransferAccountAction.editTransferAccountFailure(error)); - - yield put( - MessageAction.addMessage({ error: true, message: error.message }) - ); + message.error(error.message); } } diff --git a/app/client/sagas/transferUsageSagas.ts b/app/client/sagas/transferUsageSagas.ts index 499c6d6a7..a1c3fff70 100644 --- a/app/client/sagas/transferUsageSagas.ts +++ b/app/client/sagas/transferUsageSagas.ts @@ -1,4 +1,5 @@ import { put, takeEvery, call, all } from "redux-saga/effects"; +import { message } from "antd"; import { handleError } from "../utils"; import { @@ -8,7 +9,6 @@ import { } from "../reducers/transferUsage/types"; import { loadTransferUsagesAPI } from "../api/transferUsagesAPI"; -import { MessageAction } from "../reducers/message/actions"; import { LoadTransferUsagesAction, TransferUsageAction @@ -42,9 +42,7 @@ function* loadTransferUsages( LoadTransferUsagesAction.loadTransferUsagesFailure(error.message) ); - yield put( - MessageAction.addMessage({ error: true, message: error.message }) - ); + message.error(error.message); } } diff --git a/app/client/sagas/userSagas.ts b/app/client/sagas/userSagas.ts index d13fd0d82..b7c2f62bd 100644 --- a/app/client/sagas/userSagas.ts +++ b/app/client/sagas/userSagas.ts @@ -1,5 +1,6 @@ import { put, takeEvery, call, all, select } from "redux-saga/effects"; import { normalize } from "normalizr"; +import { message } from "antd"; import { handleError } from "../utils"; import { userSchema } from "../schemas"; @@ -14,7 +15,6 @@ import { import { TransferAccountAction } from "../reducers/transferAccount/actions"; import { browserHistory } from "../createStore"; -import { MessageAction } from "../reducers/message/actions"; import { CreateUserAction, @@ -96,17 +96,13 @@ function* editUser( yield put(EditUserAction.editUserSuccess()); - yield put( - MessageAction.addMessage({ error: false, message: edit_response.message }) - ); + message.success(edit_response.message); } catch (fetch_error) { const error = yield call(handleError, fetch_error); yield put(EditUserAction.editUserFailure(error.message)); - yield put( - MessageAction.addMessage({ error: true, message: error.message }) - ); + message.error(error.message); } } @@ -144,21 +140,14 @@ function* deleteUser( yield put(UserListAction.replaceUserList(users)); yield put(TransferAccountAction.updateTransferAccounts(transferAccounts)); - yield put( - MessageAction.addMessage({ - error: false, - message: delete_response.message - }) - ); + message.success(delete_response.message); browserHistory.push("/accounts"); } catch (fetch_error) { const error = yield call(handleError, fetch_error); yield put(DeleteUserAction.deleteUserFailure(error.message)); - yield put( - MessageAction.addMessage({ error: true, message: error.message }) - ); + message.error(error.message); } } @@ -179,20 +168,13 @@ function* resetPin( yield put(ResetPinAction.resetPinSuccess()); - yield put( - MessageAction.addMessage({ - error: false, - message: reset_response.message - }) - ); + message.success(reset_response.message); } catch (fetch_error) { const error = yield call(handleError, fetch_error); yield put(ResetPinAction.resetPinFailure(error.message)); - yield put( - MessageAction.addMessage({ error: true, message: error.message }) - ); + message.error(error.message); } } diff --git a/app/client/sagas/wyreSaga.ts b/app/client/sagas/wyreSaga.ts index dc97538c4..7b34bbefc 100644 --- a/app/client/sagas/wyreSaga.ts +++ b/app/client/sagas/wyreSaga.ts @@ -1,5 +1,5 @@ import { put, takeEvery, call, all } from "redux-saga/effects"; - +import { message } from "antd"; import { WyreAction } from "../reducers/wyre/actions"; import { WyreActionTypes, WyreState } from "../reducers/wyre/types"; @@ -10,7 +10,6 @@ import { createWyreTransferRequest } from "../api/wyreAPI"; import { handleError } from "../utils"; -import { MessageAction } from "../reducers/message/actions"; function* updateStateFromWyreDetails(data: WyreState) { let payload = data; @@ -31,9 +30,7 @@ function* loadWyreExchangeRates() { yield put(WyreAction.loadExchangeRatesFailure(error)); - yield put( - MessageAction.addMessage({ error: true, message: error.message }) - ); + message.error(error.message); } } @@ -60,9 +57,7 @@ function* loadWyreAccount() { yield put(WyreAction.loadWyreAccountFailure(error)); - yield put( - MessageAction.addMessage({ error: true, message: error.message }) - ); + message.error(error.message); } } @@ -82,12 +77,7 @@ function* createWyreTransfer({ payload }: WyreTransferResult) { yield call(updateStateFromWyreDetails, create_result.data); if (create_result.data.wyre_transfer.id !== "PREVIEW") { - yield put( - MessageAction.addMessage({ - error: false, - message: "Transfer Initiated! Check emails." - }) - ); + message.success("Transfer Initiated! Check emails."); } yield put(WyreAction.createWyreTransferSuccess()); @@ -96,9 +86,7 @@ function* createWyreTransfer({ payload }: WyreTransferResult) { yield put(WyreAction.createWyreTransferFailure(error)); - yield put( - MessageAction.addMessage({ error: true, message: error.message }) - ); + message.error(error.message); } } diff --git a/app/package.json b/app/package.json index e307c1a49..8d2dcddcb 100644 --- a/app/package.json +++ b/app/package.json @@ -1,6 +1,6 @@ { "name": "SempoBlockchain", - "version": "1.2.45", + "version": "1.2.46", "description": "Sempo blockchain web app using Python-Flask and React", "main": "index.js", "homepage": "./", From 917b387e20ac0b018e41de16f5d2c9e577842392 Mon Sep 17 00:00:00 2001 From: Michiel Date: Wed, 2 Dec 2020 09:14:04 -0400 Subject: [PATCH 2/6] Custom attribute bugfix (#549) * do it * version --- app/client/components/user/EditUserForm.tsx | 3 ++- app/package.json | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app/client/components/user/EditUserForm.tsx b/app/client/components/user/EditUserForm.tsx index 0b1a08354..db4595f49 100644 --- a/app/client/components/user/EditUserForm.tsx +++ b/app/client/components/user/EditUserForm.tsx @@ -86,7 +86,8 @@ class EditUserForm extends React.Component< account_types = Object.values(selectedUser.roles || []); account_types = account_types.map((role: any) => role); - let custom_attr_keys = customAttributes && Object.keys(customAttributes); + let custom_attr_keys = + (customAttributes && Object.keys(customAttributes)) || []; let attr_dict = {}; custom_attr_keys.map(key => { (attr_dict as attr_dict)[key] = customAttributes[key]; diff --git a/app/package.json b/app/package.json index 8d2dcddcb..e9ac3c354 100644 --- a/app/package.json +++ b/app/package.json @@ -1,6 +1,6 @@ { "name": "SempoBlockchain", - "version": "1.2.46", + "version": "1.2.47", "description": "Sempo blockchain web app using Python-Flask and React", "main": "index.js", "homepage": "./", From 6bcad316e00c13d853fdde8c5847f5f314c91f77 Mon Sep 17 00:00:00 2001 From: Michiel Date: Wed, 2 Dec 2020 17:48:55 -0400 Subject: [PATCH 3/6] Silence loading spinner warning (#568) * Fix * version --- app/client/components/dashboard/MetricsCard.jsx | 4 +++- app/package.json | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/app/client/components/dashboard/MetricsCard.jsx b/app/client/components/dashboard/MetricsCard.jsx index dec6f8015..982a5f17f 100644 --- a/app/client/components/dashboard/MetricsCard.jsx +++ b/app/client/components/dashboard/MetricsCard.jsx @@ -67,7 +67,9 @@ class MetricsCard extends React.Component { if (metricsLoadStatus.success && selectedData) { dataModule = (
- +
Date: Thu, 3 Dec 2020 02:24:58 -0400 Subject: [PATCH 4/6] Enable multiple chains (#496) * Do most of it * Update config * More changes to make tests work * Fix circular dep * Will this fix circle? * Fix tests a bit more * . * . * Move get_chain * . * . * Remove chain name from endpoint * Finally got it! * Remove prints * Tidy * Nix prints * fix frontend piece * chain * Bump version * Nix unused migration * Nix from local_config * 1 more * merge fixes * change config * More config * merge heads * heads * bad merge element * adding some defaults and fixing a launch bug * removing chunk of ancient code that was causing master wallet link issues * adding multi-chain * merge heads Co-authored-by: Nick Williams --- .../components/dashboard/MasterWalletCard.jsx | 7 +- app/migrations/versions/04be49fe82d4_.py | 43 +++++++ app/migrations/versions/30bb8c7c0f68_.py | 24 ++++ app/migrations/versions/9a0d2407d3ed_.py | 4 +- app/migrations/versions/9dc0dfafc492_.py | 24 ++++ app/migrations/versions/c100263acd35_.py | 24 ++++ app/migrations/versions/e9e13a28eefc_.py | 24 ++++ app/server/__init__.py | 2 - app/server/api/auth_api.py | 7 +- app/server/models/organisation.py | 9 +- app/server/models/token.py | 8 +- app/server/templates/index.html | 14 +-- app/server/utils/auth.py | 8 +- app/server/utils/blockchain_tasks.py | 17 +-- app/server/utils/multi_chain.py | 4 + app/server/utils/user.py | 6 +- app/server/utils/worker_simulator.py | 24 ++-- app/server/views/index.py | 5 +- app/test_app/conftest.py | 2 +- .../api/test_credit_transfer_api.py | 2 +- .../functional/api/test_exchange_api.py | 2 +- .../api/test_synchronization_filter_api.py | 4 +- app/test_app/functional/conftest.py | 2 +- app/test_app/functional/views/test_index.py | 6 +- config.py | 111 +++++++++--------- config_files/public/docker_test_config.ini | 2 + config_files/public/local_config.ini | 2 + config_files/public/test_config.ini | 2 + eth_worker/eth_src/celery_app.py | 17 ++- eth_worker/eth_src/celery_tasks.py | 45 +++---- eth_worker/eth_src/celery_utils.py | 10 +- .../eth_manager/eth_transaction_processor.py | 6 +- .../eth_manager/transaction_supervisor.py | 3 +- eth_worker/eth_src/higher_order_tasks.py | 6 +- .../eth_src/sql_persistence/interface.py | 4 +- eth_worker/test_eth_worker/conftest.py | 3 +- 36 files changed, 321 insertions(+), 162 deletions(-) create mode 100644 app/migrations/versions/04be49fe82d4_.py create mode 100644 app/migrations/versions/30bb8c7c0f68_.py create mode 100644 app/migrations/versions/9dc0dfafc492_.py create mode 100644 app/migrations/versions/c100263acd35_.py create mode 100644 app/migrations/versions/e9e13a28eefc_.py create mode 100644 app/server/utils/multi_chain.py diff --git a/app/client/components/dashboard/MasterWalletCard.jsx b/app/client/components/dashboard/MasterWalletCard.jsx index a6935c8d3..8abc42e9e 100644 --- a/app/client/components/dashboard/MasterWalletCard.jsx +++ b/app/client/components/dashboard/MasterWalletCard.jsx @@ -38,12 +38,7 @@ class MasterWalletCard extends React.Component { console.log("Master wallet balance is", masterWalletBalance); - const tracker_link = - window.ETH_EXPLORER_URL + - "/address/" + - (window.USING_EXTERNAL_ERC20 - ? window.master_wallet_address - : window.ETH_CONTRACT_ADDRESS); + const tracker_link = `${window.ETH_EXPLORER_URL}/address/${window.master_wallet_address}`; var options = { animation: false, diff --git a/app/migrations/versions/04be49fe82d4_.py b/app/migrations/versions/04be49fe82d4_.py new file mode 100644 index 000000000..60f17f885 --- /dev/null +++ b/app/migrations/versions/04be49fe82d4_.py @@ -0,0 +1,43 @@ +"""adds chain name to token + +Revision ID: 04be49fe82d4 +Revises: 9a0d2407d3ed +Create Date: 2020-10-14 14:03:25.776682 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import Session + + +# revision identifiers, used by Alembic. +revision = '04be49fe82d4' +down_revision = '9a0d2407d3ed' +branch_labels = None +depends_on = None + +Base = declarative_base() + +class Token(Base): + __tablename__ = 'token' + id = sa.Column(sa.Integer, primary_key=True) + chain = sa.Column(sa.String) + +def upgrade(): + conn = op.get_bind() + session = Session(bind=conn) + + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('token', sa.Column('chain', sa.String(), default='ETHEREUM', nullable=True)) + for o in session.query(Token).execution_options(show_all=True).all(): + if not o.chain: + o.chain = 'ETHEREUM' + session.commit() + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('token', 'chain') + # ### end Alembic commands ### diff --git a/app/migrations/versions/30bb8c7c0f68_.py b/app/migrations/versions/30bb8c7c0f68_.py new file mode 100644 index 000000000..8616c7713 --- /dev/null +++ b/app/migrations/versions/30bb8c7c0f68_.py @@ -0,0 +1,24 @@ +"""empty message + +Revision ID: 30bb8c7c0f68 +Revises: f29a1bee9016, c100263acd35 +Create Date: 2020-12-03 13:27:54.156962 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '30bb8c7c0f68' +down_revision = ('f29a1bee9016', 'c100263acd35') +branch_labels = None +depends_on = None + + +def upgrade(): + pass + + +def downgrade(): + pass diff --git a/app/migrations/versions/9a0d2407d3ed_.py b/app/migrations/versions/9a0d2407d3ed_.py index fd77caac0..5d5c59da7 100644 --- a/app/migrations/versions/9a0d2407d3ed_.py +++ b/app/migrations/versions/9a0d2407d3ed_.py @@ -1,9 +1,7 @@ -"""empty message - +"""Merge Heads! Revision ID: 9a0d2407d3ed Revises: 17269ee60c54, d478fb29a859 Create Date: 2020-10-08 17:27:25.137076 - """ from alembic import op import sqlalchemy as sa diff --git a/app/migrations/versions/9dc0dfafc492_.py b/app/migrations/versions/9dc0dfafc492_.py new file mode 100644 index 000000000..a3f9f4185 --- /dev/null +++ b/app/migrations/versions/9dc0dfafc492_.py @@ -0,0 +1,24 @@ +"""Merge heads! + +Revision ID: 9dc0dfafc492 +Revises: e9e13a28eefc +Create Date: 2020-11-17 11:48:48.010061 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '9dc0dfafc492' +down_revision = 'e9e13a28eefc' +branch_labels = None +depends_on = None + + +def upgrade(): + pass + + +def downgrade(): + pass diff --git a/app/migrations/versions/c100263acd35_.py b/app/migrations/versions/c100263acd35_.py new file mode 100644 index 000000000..13f724ce2 --- /dev/null +++ b/app/migrations/versions/c100263acd35_.py @@ -0,0 +1,24 @@ +"""empty message + +Revision ID: c100263acd35 +Revises: 6488be258421, 9dc0dfafc492 +Create Date: 2020-11-17 12:03:10.804973 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'c100263acd35' +down_revision = ('6488be258421', '9dc0dfafc492') +branch_labels = None +depends_on = None + + +def upgrade(): + pass + + +def downgrade(): + pass diff --git a/app/migrations/versions/e9e13a28eefc_.py b/app/migrations/versions/e9e13a28eefc_.py new file mode 100644 index 000000000..bba62d8fa --- /dev/null +++ b/app/migrations/versions/e9e13a28eefc_.py @@ -0,0 +1,24 @@ +"""Merge heads + +Revision ID: e9e13a28eefc +Revises: 04be49fe82d4, 380a71c24bba +Create Date: 2020-11-10 10:37:16.046020 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'e9e13a28eefc' +down_revision = ('04be49fe82d4', '380a71c24bba') +branch_labels = None +depends_on = None + + +def upgrade(): + pass + + +def downgrade(): + pass diff --git a/app/server/__init__.py b/app/server/__init__.py index 4c5f9211c..2ee6591ef 100644 --- a/app/server/__init__.py +++ b/app/server/__init__.py @@ -291,8 +291,6 @@ class AppQuery(BaseQuery): backend=config.REDIS_URL, task_serializer='json') - -encrypted_private_key = encrypt_string(config.MASTER_WALLET_PRIVATE_KEY) prior_tasks = None red = redis.Redis.from_url(config.REDIS_URL) diff --git a/app/server/api/auth_api.py b/app/server/api/auth_api.py index cf3dd60f6..c7371596b 100644 --- a/app/server/api/auth_api.py +++ b/app/server/api/auth_api.py @@ -1,4 +1,5 @@ from flask import Blueprint, request, make_response, jsonify, g, current_app +import config from flask.views import MethodView import sentry_sdk from server import db @@ -14,6 +15,7 @@ from server.utils.phone import proccess_phone_number from server.utils.amazon_ses import send_reset_email, send_activation_email, send_invite_email from server.utils.misc import decrypt_string, attach_host +from server.utils.multi_chain import get_chain from sqlalchemy.sql import func import random @@ -876,11 +878,12 @@ class BlockchainKeyAPI(MethodView): @requires_auth(allowed_roles={'ADMIN': 'superadmin'}) def get(self): + chain = get_chain() response_object = { 'status': 'success', 'message': 'Key loaded', - 'private_key': current_app.config['MASTER_WALLET_PRIVATE_KEY'], - 'address': current_app.config['MASTER_WALLET_ADDRESS'] + 'private_key': current_app.config['CHAINS'][chain]['MASTER_WALLET_PRIVATE_KEY'], + 'address': current_app.config['CHAINS'][chain]['MASTER_WALLET_ADDRESS'] } return make_response(jsonify(attach_host(response_object))), 200 diff --git a/app/server/models/organisation.py b/app/server/models/organisation.py index 6d61f0f8e..653084e97 100644 --- a/app/server/models/organisation.py +++ b/app/server/models/organisation.py @@ -172,17 +172,16 @@ def bind_token(self, token): def __init__(self, token=None, is_master=False, valid_roles=None, **kwargs): super(Organisation, self).__init__(**kwargs) - + chain = self.token.chain if self.token else current_app.config['DEFAULT_CHAIN'] self.external_auth_username = 'admin_'+ self.name.lower().replace(' ', '_') self.external_auth_password = secrets.token_hex(16) self.valid_roles = valid_roles or list(ASSIGNABLE_TIERS.keys()) if is_master: if Organisation.query.filter_by(is_master=True).first(): raise Exception("A master organisation already exists") - self.is_master = True self.system_blockchain_address = bt.create_blockchain_wallet( - private_key=current_app.config['MASTER_WALLET_PRIVATE_KEY'], + private_key=current_app.config['CHAINS'][chain]['MASTER_WALLET_PRIVATE_KEY'], wei_target_balance=0, wei_topup_threshold=0, ) @@ -193,8 +192,8 @@ def __init__(self, token=None, is_master=False, valid_roles=None, **kwargs): self.is_master = False self.system_blockchain_address = bt.create_blockchain_wallet( - wei_target_balance=current_app.config['SYSTEM_WALLET_TARGET_BALANCE'], - wei_topup_threshold=current_app.config['SYSTEM_WALLET_TOPUP_THRESHOLD'], + wei_target_balance=current_app.config['CHAINS'][chain]['SYSTEM_WALLET_TARGET_BALANCE'], + wei_topup_threshold=current_app.config['CHAINS'][chain]['SYSTEM_WALLET_TOPUP_THRESHOLD'], ) self.primary_blockchain_address = bt.create_blockchain_wallet() diff --git a/app/server/models/token.py b/app/server/models/token.py index 02d46ccfb..3cbb92824 100644 --- a/app/server/models/token.py +++ b/app/server/models/token.py @@ -4,6 +4,7 @@ import config from server import db, bt +from flask import current_app from server.models.transfer_account import TransferAccount, TransferAccountType from server.models.utils import ( ModelBase, @@ -26,6 +27,8 @@ class Token(ModelBase): token_type = db.Column(db.Enum(TokenType)) + chain = db.Column(db.String, default='ETHEREUM') + organisations = db.relationship('Organisation', backref='token', lazy=True, @@ -84,10 +87,11 @@ def token_amount_to_system(self, token_amount, queue='high-priority'): def system_amount_to_token(self, system_amount, queue='high-priority'): return int(system_amount/100 * 10**self.get_decimals(queue)) - def __init__(self, **kwargs): + def __init__(self, chain='ETHEREUM', **kwargs): + self.chain = chain super(Token, self).__init__(**kwargs) float_transfer_account = TransferAccount( - private_key=config.ETH_FLOAT_PRIVATE_KEY, + private_key=current_app.config['CHAINS'][self.chain]['FLOAT_PRIVATE_KEY'], account_type=TransferAccountType.FLOAT, token=self, is_approved=True diff --git a/app/server/templates/index.html b/app/server/templates/index.html index 5ef7ff51c..80bb75cfc 100644 --- a/app/server/templates/index.html +++ b/app/server/templates/index.html @@ -114,16 +114,16 @@ window.PUSHER_KEY = "{{ config.PUSHER_KEY }}"; window.PUSHER_ENV_CHANNEL = "{{ config.PUSHER_ENV_CHANNEL }}"; window.INTERCOM_APP_ID = "{{ config.INTERCOM_APP_ID }}"; - window.ETH_CONTRACT_ADDRESS = "{{ config.ETH_CONTRACT_ADDRESS }}"; - window.master_wallet_address = "{{ config.MASTER_WALLET_ADDRESS }}"; - window.ETH_EXPLORER_URL = "{{ config.ETH_EXPLORER_URL }}"; + window.ETH_CONTRACT_ADDRESS = + "{{ config.CHAINS[chain]['CONTRACT_ADDRESS'] }}"; + window.master_wallet_address = + "{{ config.CHAINS[chain]['MASTER_WALLET_ADDRESS'] }}"; + window.ETH_EXPLORER_URL = "{{ config.CHAINS[chain]['EXPLORER_URL'] }}"; window.USING_EXTERNAL_ERC20 = "{{ config.USING_EXTERNAL_ERC20 }}" === "True"; window.SENTRY_REACT_DSN = "{{ config.SENTRY_REACT_DSN }}"; - window.IS_USING_BITCOIN = "{{ config.IS_USING_BITCOIN }}" === "True"; - window.IS_BITCOIN_TESTNET = "{{ config.IS_BITCOIN_TESTNET }}" === "True"; - window.BITCOIN_MASTER_WALLET_ADDRESS = - "{{config.BITCOIN_MASTER_WALLET_ADDRESS}}"; + window.IS_USING_BITCOIN = + "{{ config.CHAINS[chain]['IS_USING_BITCOIN'] }}" === "True";
diff --git a/app/server/utils/auth.py b/app/server/utils/auth.py index 81634c8cc..5fa31f979 100644 --- a/app/server/utils/auth.py +++ b/app/server/utils/auth.py @@ -354,12 +354,12 @@ def get_denominations(currency_symbol=None): def create_user_response_object(user, auth_token, message): - if current_app.config['IS_USING_BITCOIN']: - try: + try: + if current_app.config['CHAINS'][user.default_organisation.token.chain]['IS_USING_BITCOIN']: usd_to_satoshi_rate = get_usd_to_satoshi_rate() - except Exception: + else: usd_to_satoshi_rate = None - else: + except Exception: usd_to_satoshi_rate = None conversion_rate = 1 diff --git a/app/server/utils/blockchain_tasks.py b/app/server/utils/blockchain_tasks.py index 49bebe0a7..0c9b99e9a 100644 --- a/app/server/utils/blockchain_tasks.py +++ b/app/server/utils/blockchain_tasks.py @@ -1,3 +1,5 @@ +from flask import g +import config from typing import Optional, List, Dict from functools import partial from flask import current_app @@ -14,18 +16,19 @@ bonding_curve_reserve_to_tokens, bonding_curve_token1_to_token2 ) - +from server.utils.multi_chain import get_chain class BlockchainTasker(object): + def _eth_endpoint(self, endpoint): celery_tasks_name = 'celery_tasks' - return f'{celery_tasks_name}.{endpoint}' + return f'{get_chain()}.{celery_tasks_name}.{endpoint}' def _execute_synchronous_celery(self, task, kwargs=None, args=None, timeout=None, queue='high-priority'): async_result = task_runner.delay_task(task, kwargs, args, queue=queue) try: response = async_result.get( - timeout=timeout or current_app.config['SYNCRONOUS_TASK_TIMEOUT'], + timeout=timeout or current_app.config['CHAINS'][get_chain()]['SYNCRONOUS_TASK_TIMEOUT'], propagate=True, interval=0.3) except Exception as e: @@ -94,7 +97,7 @@ def await_task_success(self, elapsed = 0 if timeout is None: - timeout = current_app.config['SYNCRONOUS_TASK_TIMEOUT'] + timeout = current_app.config['CHAINS'][get_chain()]['SYNCRONOUS_TASK_TIMEOUT'] while timeout is None or elapsed <= timeout: task = self.get_blockchain_task(task_uuid) @@ -431,7 +434,7 @@ def deploy_exchange_network(self, deploying_address): return self._execute_synchronous_celery( self._eth_endpoint('deploy_exchange_network'), args=[deploying_address], - timeout=current_app.config['SYNCRONOUS_TASK_TIMEOUT'] * 25 + timeout=current_app.config['CHAINS'][get_chain()]['SYNCRONOUS_TASK_TIMEOUT'] * 25 ) def deploy_and_fund_reserve_token(self, deploying_address, name, symbol, fund_amount_wei): @@ -439,7 +442,7 @@ def deploy_and_fund_reserve_token(self, deploying_address, name, symbol, fund_am return self._execute_synchronous_celery( self._eth_endpoint('deploy_and_fund_reserve_token'), args=args, - timeout=current_app.config['SYNCRONOUS_TASK_TIMEOUT'] * 20 + timeout=current_app.config['CHAINS'][get_chain()]['SYNCRONOUS_TASK_TIMEOUT'] * 20 ) def deploy_smart_token(self, @@ -462,7 +465,7 @@ def deploy_smart_token(self, return self._execute_synchronous_celery( self._eth_endpoint('deploy_smart_token'), args=args, - timeout=current_app.config['SYNCRONOUS_TASK_TIMEOUT'] * 15 + timeout=current_app.config['CHAINS'][get_chain()]['SYNCRONOUS_TASK_TIMEOUT'] * 15 ) def topup_wallet_if_required(self, wallet_address, queue='high-priority'): diff --git a/app/server/utils/multi_chain.py b/app/server/utils/multi_chain.py new file mode 100644 index 000000000..43334f0fe --- /dev/null +++ b/app/server/utils/multi_chain.py @@ -0,0 +1,4 @@ +from flask import g, current_app + +def get_chain(): + return g.active_organisation.token.chain if g.get('active_organisation', False) and g.active_organisation.token else current_app.config['DEFAULT_CHAIN'] diff --git a/app/server/utils/user.py b/app/server/utils/user.py index 346be2fdf..f63df8e83 100644 --- a/app/server/utils/user.py +++ b/app/server/utils/user.py @@ -7,6 +7,7 @@ from flask import current_app, g from eth_utils import to_checksum_address import sentry_sdk +import config from server import db from server.models.device_info import DeviceInfo @@ -29,7 +30,7 @@ from server.utils.amazon_s3 import generate_new_filename, save_to_s3_from_url, LoadFileException from server.utils.internationalization import i18n_for from server.utils.misc import rounded_dollars - +from server.utils.multi_chain import get_chain def save_photo_and_check_for_duplicate(url, new_filename, image_id): save_to_s3_from_url(url, new_filename) @@ -497,7 +498,8 @@ def proccess_create_or_modify_user_request( raise Exception(f'{at} not a valid role for this organisation. Please choose one of the following: {g.active_organisation.valid_roles}') roles_to_set.append((ASSIGNABLE_TIERS[at], at)) - if current_app.config['IS_USING_BITCOIN']: + chain = get_chain() + if current_app.config['CHAINS'][chain]['IS_USING_BITCOIN']: try: base58.b58decode_check(blockchain_address) except ValueError: diff --git a/app/server/utils/worker_simulator.py b/app/server/utils/worker_simulator.py index c1a2d4f82..5e91d414b 100644 --- a/app/server/utils/worker_simulator.py +++ b/app/server/utils/worker_simulator.py @@ -1,18 +1,18 @@ from . import blockchain_tasks_simulator endpoint_simulators = { - 'eth_manager.celery_tasks.deploy_contract': blockchain_tasks_simulator.deploy_contract, - 'eth_manager.celery_tasks.call_contract_function': blockchain_tasks_simulator.call_contract_function, - 'eth_manager.celery_tasks.transact_with_contract_function': blockchain_tasks_simulator.transact_with_contract_function, - 'eth_manager.celery_tasks.get_task': blockchain_tasks_simulator.get_task, - 'eth_manager.celery_tasks.retry_task': blockchain_tasks_simulator.retry_task, - 'eth_manager.celery_tasks.retry_failed': blockchain_tasks_simulator.retry_failed, - 'eth_manager.celery_tasks.create_new_blockchain_wallet': blockchain_tasks_simulator.create_new_blockchain_wallet, - 'eth_manager.celery_tasks.deploy_exchange_network': blockchain_tasks_simulator.deploy_exchange_network, - 'eth_manager.celery_tasks.deploy_and_fund_reserve_token': blockchain_tasks_simulator.deploy_and_fund_reserve_token, - 'eth_manager.celery_tasks.deploy_smart_token': blockchain_tasks_simulator.deploy_smart_token, - 'eth_manager.celery_tasks.topup_wallet_if_required': blockchain_tasks_simulator.topup_wallet_if_required, - 'eth_manager.celery_tasks.send_eth': blockchain_tasks_simulator.send_eth, + 'ETHEREUM.celery_tasks.deploy_contract': blockchain_tasks_simulator.deploy_contract, + 'ETHEREUM.celery_tasks.call_contract_function': blockchain_tasks_simulator.call_contract_function, + 'ETHEREUM.celery_tasks.transact_with_contract_function': blockchain_tasks_simulator.transact_with_contract_function, + 'ETHEREUM.celery_tasks.get_task': blockchain_tasks_simulator.get_task, + 'ETHEREUM.celery_tasks.retry_task': blockchain_tasks_simulator.retry_task, + 'ETHEREUM.celery_tasks.retry_failed': blockchain_tasks_simulator.retry_failed, + 'ETHEREUM.celery_tasks.create_new_blockchain_wallet': blockchain_tasks_simulator.create_new_blockchain_wallet, + 'ETHEREUM.celery_tasks.deploy_exchange_network': blockchain_tasks_simulator.deploy_exchange_network, + 'ETHEREUM.celery_tasks.deploy_and_fund_reserve_token': blockchain_tasks_simulator.deploy_and_fund_reserve_token, + 'ETHEREUM.celery_tasks.deploy_smart_token': blockchain_tasks_simulator.deploy_smart_token, + 'ETHEREUM.celery_tasks.topup_wallet_if_required': blockchain_tasks_simulator.topup_wallet_if_required, + 'ETHEREUM.celery_tasks.send_eth': blockchain_tasks_simulator.send_eth, } def simulate(task, kwargs, args, queue): diff --git a/app/server/views/index.py b/app/server/views/index.py index 96bbc2463..cfca01ff7 100644 --- a/app/server/views/index.py +++ b/app/server/views/index.py @@ -1,6 +1,8 @@ # -*- coding: utf-8 -*- -from flask import render_template, Blueprint, current_app +from flask import render_template, Blueprint, current_app, g +import config from server.utils.auth import requires_auth +from server.utils.multi_chain import get_chain import glob, os from pathlib import Path @@ -24,6 +26,7 @@ def catch_all(path): 'index.html', js_bundle_main=get_js_bundle_filename('main.bundle.*.js'), js_bundle_vendor=get_js_bundle_filename('vendors~main.bundle.*.js'), + chain=get_chain(), ) @index_view.route('/whatsapp-sync/') diff --git a/app/test_app/conftest.py b/app/test_app/conftest.py index 7b7c232bb..8427a6094 100644 --- a/app/test_app/conftest.py +++ b/app/test_app/conftest.py @@ -410,7 +410,7 @@ def loaded_master_wallet_address(load_account): """ from server import bt - deploying_address = bt.create_blockchain_wallet(private_key=config.MASTER_WALLET_PRIVATE_KEY) + deploying_address = bt.create_blockchain_wallet(private_key=current_app.config['CHAINS']['ETHEREUM']['MASTER_WALLET_PRIVATE_KEY']) load_account(deploying_address) diff --git a/app/test_app/functional/api/test_credit_transfer_api.py b/app/test_app/functional/api/test_credit_transfer_api.py index 528bb191c..ec3419749 100644 --- a/app/test_app/functional/api/test_credit_transfer_api.py +++ b/app/test_app/functional/api/test_credit_transfer_api.py @@ -316,7 +316,7 @@ def post_to_credit_transfer_internal(sender_blockchain_address, recipient_blockc def test_force_third_party_transaction_sync(): if will_func_test_blockchain(): task_uuid = bt.force_third_party_transaction_sync() - bt.await_task_success(task_uuid, timeout=config.SYNCRONOUS_TASK_TIMEOUT * 48) + bt.await_task_success(task_uuid, timeout=config.CHAINS['ETHEREUM']['SYNCRONOUS_TASK_TIMEOUT'] * 48) @pytest.mark.parametrize("is_bulk, invert_recipient_list, transfer_amount, transfer_type, status_code", [ [True, False, 1000, 'DISBURSEMENT', 201], diff --git a/app/test_app/functional/api/test_exchange_api.py b/app/test_app/functional/api/test_exchange_api.py index a77abd20f..a8135e41b 100644 --- a/app/test_app/functional/api/test_exchange_api.py +++ b/app/test_app/functional/api/test_exchange_api.py @@ -35,5 +35,5 @@ def test_exchange(test_client, user_with_reserve_balance, initialised_blockchain if status_code == 200 and will_func_test_blockchain(): task_uuid = response.json['data']['exchange']['blockchain_task_uuid'] time.sleep(1) # Have to wait til after_request for task to be submitted - result = bt.await_task_success(task_uuid, timeout=config.SYNCRONOUS_TASK_TIMEOUT * 12) + result = bt.await_task_success(task_uuid, timeout=config.CHAINS['ETHEREUM']['SYNCRONOUS_TASK_TIMEOUT'] * 12) assert result['status'] == 'SUCCESS' diff --git a/app/test_app/functional/api/test_synchronization_filter_api.py b/app/test_app/functional/api/test_synchronization_filter_api.py index 8e99dfb51..addc57d3e 100644 --- a/app/test_app/functional/api/test_synchronization_filter_api.py +++ b/app/test_app/functional/api/test_synchronization_filter_api.py @@ -3,7 +3,7 @@ from server import bt @pytest.mark.parametrize("contract_address, contract_type, filter_type, filter_parameters, status_code", [ - (config.ETH_CONTRACT_ADDRESS, "ERC20", 'TRANSFER', None, 201), + (config.CHAINS['ETHEREUM']['CONTRACT_ADDRESS'], "ERC20", 'TRANSFER', None, 201), ]) def test_sync_filter_api(test_client, create_ip_address, complete_admin_auth_token, contract_address, contract_type, filter_type, filter_parameters, status_code): # Super basic test for creation of sync filters @@ -15,7 +15,7 @@ def test_sync_filter_api(test_client, create_ip_address, complete_admin_auth_tok Accept='application/json' ), json={ - 'contract_address': config.ETH_CONTRACT_ADDRESS, + 'contract_address': config.CHAINS['ETHEREUM']['CONTRACT_ADDRESS'], 'contract_type': contract_type, 'filter_type': filter_type, 'filter_parameters': filter_parameters diff --git a/app/test_app/functional/conftest.py b/app/test_app/functional/conftest.py index 73d8660e9..5e7164e90 100644 --- a/app/test_app/functional/conftest.py +++ b/app/test_app/functional/conftest.py @@ -25,7 +25,7 @@ def load_account(): def inner(address): if will_func_test_blockchain(): - w3 = Web3(HTTPProvider(config.ETH_HTTP_PROVIDER)) + w3 = Web3(HTTPProvider(config.CHAINS['ETHEREUM']['HTTP_PROVIDER'])) tx_hash = w3.eth.sendTransaction( {'to': address, 'from': w3.eth.accounts[0], 'value': 5 * 10 ** 18}) diff --git a/app/test_app/functional/views/test_index.py b/app/test_app/functional/views/test_index.py index 8cc1c7881..1a1fac74a 100644 --- a/app/test_app/functional/views/test_index.py +++ b/app/test_app/functional/views/test_index.py @@ -22,9 +22,9 @@ def test_index(test_client): assert config.HEAP_ANALYTICS_ID.encode() in response.data # blockchain - assert config.ETH_EXPLORER_URL.encode() in response.data - assert config.MASTER_WALLET_ADDRESS.encode() in response.data - assert config.ETH_CONTRACT_ADDRESS.encode() in response.data + assert config.CHAINS['ETHEREUM']['EXPLORER_URL'].encode() in response.data + assert config.CHAINS['ETHEREUM']['MASTER_WALLET_ADDRESS'].encode() in response.data + assert config.CHAINS['ETHEREUM']['CONTRACT_ADDRESS'].encode() in response.data # correct JS bundles assert get_js_bundle_filename('main.bundle.*.js').encode() in response.data diff --git a/config.py b/config.py index 7966a1de9..bc7d8eb38 100755 --- a/config.py +++ b/config.py @@ -5,7 +5,7 @@ logging.basicConfig(level=env_loglevel) logg = logging.getLogger(__name__) -VERSION = '1.7.30' # Remember to bump this in every PR +VERSION = '1.7.31' # Remember to bump this in every PR logg.info('Loading configs at UTC {}'.format(datetime.datetime.utcnow())) @@ -260,6 +260,9 @@ def get_database_uri(name, host, censored=True): # https://ropsten.infura.io/9CAC3Lb5OjaoecQIpPNP # https://kovan.infura.io/9CAC3Lb5OjaoecQIpPNP +CHAIN_NAMES = list(config_parser['APP']['chains'].split(',')) or ['ETHEREUM'] +DEFAULT_CHAIN = config_parser['APP']['default_chain'] or 'ETHEREUM' +CHAINS = {} try: from eth_keys import keys from eth_utils import keccak @@ -269,73 +272,73 @@ def address_from_private_key(private_key): if isinstance(private_key, str): private_key = bytes.fromhex(private_key.replace('0x', '')) return keys.PrivateKey(private_key).public_key.to_checksum_address() + for chain in CHAIN_NAMES: + configs = {} + configs['HTTP_PROVIDER'] = config_parser[chain]['http_provider'] + configs['WEBSOCKET_PROVIDER'] = config_parser[chain].get('websocket_provider') + configs['CHAIN_ID'] = config_parser[chain].get('chain_id') + configs['EXPLORER_URL'] = (config_parser[chain].get('explorer_url') or 'https://etherscan.io').strip('/') + configs['OWNER_PRIVATE_KEY'] = secrets_parser[chain]['owner_private_key'] + configs['OWNER_ADDRESS'] = address_from_private_key(configs['OWNER_PRIVATE_KEY'] ) + configs['FLOAT_PRIVATE_KEY'] = secrets_parser[chain]['float_private_key'] + configs['CONTRACT_VERSION'] = config_parser[chain]['contract_version'] + configs['GAS_PRICE'] = int(config_parser[chain]['gas_price_gwei'] or 0) + configs['GAS_LIMIT'] = int(config_parser[chain]['gas_limit'] or 0) + configs['TARGET_TRANSACTION_TIME'] = int(config_parser[chain]['target_transaction_time'] or 120) + configs['GAS_PRICE_PROVIDER'] = config_parser[chain]['gas_price_provider'] + configs['CONTRACT_NAME'] = 'SempoCredit{}_v{}'.format(DEPLOYMENT_NAME, str(configs['CONTRACT_VERSION'])) + + configs['CHECK_TRANSACTION_BASE_TIME'] = 20 + configs['CHECK_TRANSACTION_RETRIES'] = int(config_parser[chain]['check_transaction_retries']) + configs['CHECK_TRANSACTION_RETRIES_TIME_LIMIT'] = sum( + [configs['CHECK_TRANSACTION_BASE_TIME'] * 2 ** i for i in range(1, configs['CHECK_TRANSACTION_RETRIES'] + 1)] + ) - ETH_PENDING_TRANSACTION_EXPIRY_SECONDS = config_parser['ETHEREUM'].getint('transaction_expiry_seconds', 30) - ETH_HTTP_PROVIDER = config_parser['ETHEREUM']['http_provider'] - ETH_WEBSOCKET_PROVIDER = config_parser['ETHEREUM'].get('websocket_provider') - ETH_CHAIN_ID = config_parser['ETHEREUM'].get('chain_id') - ETH_EXPLORER_URL = (config_parser['ETHEREUM'].get('explorer_url') or 'https://etherscan.io').strip('/') - ETH_OWNER_PRIVATE_KEY = secrets_parser['ETHEREUM']['owner_private_key'] - ETH_OWNER_ADDRESS = address_from_private_key(ETH_OWNER_PRIVATE_KEY) - ETH_FLOAT_PRIVATE_KEY = secrets_parser['ETHEREUM']['float_private_key'] - ETH_CONTRACT_VERSION = config_parser['ETHEREUM']['contract_version'] - ETH_GAS_PRICE = int(config_parser['ETHEREUM']['gas_price_gwei'] or 0) - ETH_GAS_LIMIT = int(config_parser['ETHEREUM']['gas_limit'] or 0) - ETH_TARGET_TRANSACTION_TIME = int(config_parser['ETHEREUM']['target_transaction_time'] or 120) - ETH_GAS_PRICE_PROVIDER = config_parser['ETHEREUM']['gas_price_provider'] - ETH_CONTRACT_NAME = 'SempoCredit{}_v{}'.format(DEPLOYMENT_NAME, str(ETH_CONTRACT_VERSION)) - - ETH_CHECK_TRANSACTION_BASE_TIME = 20 - ETH_CHECK_TRANSACTION_RETRIES = int(config_parser['ETHEREUM']['check_transaction_retries']) - ETH_CHECK_TRANSACTION_RETRIES_TIME_LIMIT = sum( - [ETH_CHECK_TRANSACTION_BASE_TIME * 2 ** i for i in range(1, ETH_CHECK_TRANSACTION_RETRIES + 1)] - ) - - INTERNAL_TO_TOKEN_RATIO = float(config_parser['ETHEREUM'].get('internal_to_token_ratio', 1)) - FORCE_ETH_DISBURSEMENT_AMOUNT = float(config_parser['ETHEREUM'].get('force_eth_disbursement_amount', 0)) - - unchecksummed_withdraw_to_address = config_parser['ETHEREUM'].get('withdraw_to_address') - if unchecksummed_withdraw_to_address: - WITHDRAW_TO_ADDRESS = Web3.toChecksumAddress(unchecksummed_withdraw_to_address) - else: - WITHDRAW_TO_ADDRESS = None + configs['INTERNAL_TO_TOKEN_RATIO'] = float(config_parser[chain].get('internal_to_token_ratio', 1)) + configs['FORCE_ETH_DISBURSEMENT_AMOUNT'] = float(config_parser[chain].get('force_eth_disbursement_amount', 0)) + configs['PENDING_TRANSACTION_EXPIRY_SECONDS'] = config_parser[chain].getint('transaction_expiry_seconds', 30) - MASTER_WALLET_PRIVATE_KEY = secrets_parser['ETHEREUM'].get('master_wallet_private_key') - if MASTER_WALLET_PRIVATE_KEY: - master_wallet_private_key = bytes.fromhex(MASTER_WALLET_PRIVATE_KEY.replace('0x', '')) - else: - master_wallet_private_key = keccak(text=SECRET_KEY + DEPLOYMENT_NAME) - MASTER_WALLET_PRIVATE_KEY = master_wallet_private_key.hex() + unchecksummed_withdraw_to_address = config_parser[chain].get('withdraw_to_address') + if unchecksummed_withdraw_to_address: + configs['WITHDRAW_TO_ADDRESS'] = Web3.toChecksumAddress(unchecksummed_withdraw_to_address) + else: + configs['WITHDRAW_TO_ADDRESS'] = None - MASTER_WALLET_ADDRESS = address_from_private_key(master_wallet_private_key) + configs['MASTER_WALLET_PRIVATE_KEY'] = secrets_parser[chain].get('master_wallet_private_key') + if configs['MASTER_WALLET_PRIVATE_KEY']: + master_wallet_private_key = bytes.fromhex(configs['MASTER_WALLET_PRIVATE_KEY'].replace('0x', '')) + else: + master_wallet_private_key = keccak(text=SECRET_KEY + DEPLOYMENT_NAME) + configs['MASTER_WALLET_PRIVATE_KEY'] = master_wallet_private_key.hex() - RESERVE_TOKEN_ADDRESS = config_parser['ETHEREUM'].get('reserve_token_address') - RESERVE_TOKEN_NAME = config_parser['ETHEREUM'].get('reserve_token_name') - RESERVE_TOKEN_SYMBOL = config_parser['ETHEREUM'].get('reserve_token_symbol') + configs['MASTER_WALLET_ADDRESS'] = address_from_private_key(master_wallet_private_key) - SYSTEM_WALLET_TARGET_BALANCE = int(config_parser['ETHEREUM'].get('system_wallet_target_balance', 0)) - SYSTEM_WALLET_TOPUP_THRESHOLD = int(config_parser['ETHEREUM'].get('system_wallet_topup_threshold', 0)) + configs['RESERVE_TOKEN_ADDRESS'] = config_parser[chain].get('reserve_token_address') + configs['RESERVE_TOKEN_NAME'] = config_parser[chain].get('reserve_token_name') + configs['RESERVE_TOKEN_SYMBOL'] = config_parser[chain].get('reserve_token_symbol') - ETH_CONTRACT_TYPE = config_parser['ETHEREUM'].get('contract_type', 'standard').lower() - ETH_CONTRACT_ADDRESS = config_parser['ETHEREUM'].get('contract_address') - USING_EXTERNAL_ERC20 = ETH_CONTRACT_TYPE != 'mintable' + configs['SYSTEM_WALLET_TARGET_BALANCE'] = int(config_parser[chain].get('system_wallet_target_balance', 0)) + configs['SYSTEM_WALLET_TOPUP_THRESHOLD'] = int(config_parser[chain].get('system_wallet_topup_threshold', 0)) - if config_parser['ETHEREUM'].get('dai_contract_address'): - # support of old config file syntax - ETH_CONTRACT_ADDRESS = config_parser['ETHEREUM'].get('dai_contract_address') + configs['CONTRACT_TYPE'] = config_parser[chain].get('contract_type', 'standard').lower() + configs['CONTRACT_ADDRESS'] = config_parser[chain].get('contract_address') + configs['USING_EXTERNAL_ERC20'] = configs['CONTRACT_TYPE'] != 'mintable' - IS_USING_BITCOIN = False + if config_parser[chain].get('dai_contract_address'): + # support of old config file syntax + configs['CONTRACT_ADDRESS'] = config_parser[chain].get('dai_contract_address') - EXCHANGE_CONTRACT_ADDRESS = config_parser['ETHEREUM'].get('exchange_contract_address') + configs['IS_USING_BITCOIN'] = False - SYNCRONOUS_TASK_TIMEOUT = config_parser['ETHEREUM'].getint('synchronous_task_timeout', 4) - CALL_TIMEOUT = config_parser['ETHEREUM'].getint('call_timeout', 2) + configs['EXCHANGE_CONTRACT_ADDRESS'] = config_parser[chain].get('exchange_contract_address') + + configs['SYNCRONOUS_TASK_TIMEOUT'] = config_parser[chain].getint('synchronous_task_timeout', 4) + configs['CALL_TIMEOUT'] = config_parser[chain].getint('call_timeout', 2) + CHAINS[chain] = configs except ImportError: logg.warn("Cannot import Ethereum Packages, make sure they're not required!") - - FACEBOOK_TOKEN = common_secrets_parser['FACEBOOK']['token'] FACEBOOK_VERIFY_TOKEN = common_secrets_parser['FACEBOOK']['verify_token'] diff --git a/config_files/public/docker_test_config.ini b/config_files/public/docker_test_config.ini index 042fc4e96..3ced2bf6d 100644 --- a/config_files/public/docker_test_config.ini +++ b/config_files/public/docker_test_config.ini @@ -20,6 +20,8 @@ enable_simulator_mode = false sempoadmin_emails = admin@acme.org,example@example.com default_country = AU third_party_sync_epoch = latest +chains = ETHEREUM +default_chain = ETHEREUM [DATABASE] host = postgres diff --git a/config_files/public/local_config.ini b/config_files/public/local_config.ini index fa8fae1d9..b5ab1c140 100644 --- a/config_files/public/local_config.ini +++ b/config_files/public/local_config.ini @@ -20,6 +20,8 @@ enable_simulator_mode = false sempoadmin_emails = admin@acme.org,example@example.com default_country = AU third_party_sync_epoch = latest +chains = ETHEREUM +default_chain = ETHEREUM [DATABASE] host = localhost diff --git a/config_files/public/test_config.ini b/config_files/public/test_config.ini index ebb0a8c4f..f818ff493 100644 --- a/config_files/public/test_config.ini +++ b/config_files/public/test_config.ini @@ -20,6 +20,8 @@ enable_simulator_mode = false sempoadmin_emails = admin@acme.org,example@example.com default_country = AU third_party_sync_epoch = latest +chains = ETHEREUM +default_chain = ETHEREUM [DATABASE] host = localhost diff --git a/eth_worker/eth_src/celery_app.py b/eth_worker/eth_src/celery_app.py index 3adefe5ba..f12066dbc 100644 --- a/eth_worker/eth_src/celery_app.py +++ b/eth_worker/eth_src/celery_app.py @@ -45,10 +45,7 @@ with configure_scope() as scope: scope.set_tag("domain", config.APP_HOST) -ETH_CHECK_TRANSACTION_RETRIES = config.ETH_CHECK_TRANSACTION_RETRIES -ETH_CHECK_TRANSACTION_RETRIES_TIME_LIMIT = config.ETH_CHECK_TRANSACTION_RETRIES_TIME_LIMIT -ETH_CHECK_TRANSACTION_BASE_TIME = config.ETH_CHECK_TRANSACTION_BASE_TIME - +chain_config = config.CHAINS[celery_utils.chain] app = Celery('tasks', broker=config.REDIS_URL, @@ -68,9 +65,9 @@ } } -w3 = Web3(HTTPProvider(config.ETH_HTTP_PROVIDER)) +w3 = Web3(HTTPProvider(chain_config['HTTP_PROVIDER'])) -w3_websocket = Web3(WebsocketProvider(config.ETH_WEBSOCKET_PROVIDER)) +w3_websocket = Web3(WebsocketProvider(chain_config['WEBSOCKET_PROVIDER'])) red = redis.Redis.from_url(config.REDIS_URL) @@ -81,9 +78,9 @@ ) processor = EthTransactionProcessor( - ethereum_chain_id=config.ETH_CHAIN_ID, - gas_price_wei=w3.toWei(config.ETH_GAS_PRICE, 'gwei'), - gas_limit=config.ETH_GAS_LIMIT, + ethereum_chain_id=chain_config['CHAIN_ID'], + gas_price_wei=w3.toWei(chain_config['GAS_PRICE'], 'gwei'), + gas_limit=chain_config['GAS_LIMIT'], w3=w3, persistence=persistence_module ) @@ -100,7 +97,7 @@ if os.environ.get('CONTAINER_TYPE') == 'PRIMARY': persistence_module.create_blockchain_wallet_from_private_key( - config.MASTER_WALLET_PRIVATE_KEY, + chain_config['MASTER_WALLET_PRIVATE_KEY'], allow_existing=True ) diff --git a/eth_worker/eth_src/celery_tasks.py b/eth_worker/eth_src/celery_tasks.py index 183cb7971..5cbf4321e 100644 --- a/eth_worker/eth_src/celery_tasks.py +++ b/eth_worker/eth_src/celery_tasks.py @@ -1,7 +1,8 @@ import celery import config -from celery_app import app, processor, supervisor, task_manager, persistence_module, blockchain_sync +from celery_app import app, processor, supervisor, task_manager, persistence_module, blockchain_sync, chain_config +from celery_utils import eth_endpoint from exceptions import ( LockedNotAcquired ) @@ -72,11 +73,11 @@ def on_success(self, retval, task_id, args, kwargs): # + '\n' # + kwargs.get('einfo').traceback) -@app.task(**low_priority_config) +@app.task(name=eth_endpoint('synchronize_third_party_transactions'), **low_priority_config) def synchronize_third_party_transactions(self): return blockchain_sync.synchronize_third_party_transactions() -@app.task(**low_priority_config) +@app.task(name=eth_endpoint('add_transaction_filter'), **low_priority_config) def add_transaction_filter(self, contract_address, contract_type, filter_parameters, filter_type, decimals = 18, block_epoch = None): f = blockchain_sync.add_transaction_filter(contract_address, contract_type, filter_parameters, filter_type, decimals, block_epoch) if f: @@ -91,19 +92,19 @@ def add_transaction_filter(self, contract_address, contract_type, filter_paramet else: return True -@app.task(**base_task_config) +@app.task(name=eth_endpoint('deploy_exchange_network'), **base_task_config) def deploy_exchange_network(self, deploying_address): return composite.deploy_exchange_network(deploying_address) -@app.task(**base_task_config) +@app.task(name=eth_endpoint('deploy_and_fund_reserve_token'), **base_task_config) def deploy_and_fund_reserve_token(self, deploying_address, name, symbol, fund_amount_wei): return composite.deploy_and_fund_reserve_token( deploying_address, name, symbol, fund_amount_wei) -@app.task(**base_task_config) +@app.task(name=eth_endpoint('deploy_smart_token'), **base_task_config) def deploy_smart_token(self, deploying_address, name, symbol, decimals, reserve_deposit_wei, @@ -122,7 +123,7 @@ def deploy_smart_token(self, deploying_address, reserve_ratio_ppm) -@app.task(**base_task_config) +@app.task(name=eth_endpoint('create_new_blockchain_wallet'), **base_task_config) def create_new_blockchain_wallet(self, wei_target_balance=0, wei_topup_threshold=0, private_key=None): wallet = persistence_module.create_new_blockchain_wallet(wei_target_balance=wei_target_balance, wei_topup_threshold=wei_topup_threshold, @@ -135,12 +136,12 @@ def topup_wallets(self): return composite.topup_wallets() # Set retry attempts to zero since beat will retry shortly anyway -@app.task(**no_retry_config) +@app.task(name=eth_endpoint('topup_wallet_if_required'), **no_retry_config) def topup_wallet_if_required(self, address): return composite.topup_if_required(address) -@app.task(**base_task_config) +@app.task(name=eth_endpoint('transact_with_contract_function'), **base_task_config) def transact_with_contract_function(self, contract_address, function, abi_type=None, args=None, kwargs=None, signing_address=None, encrypted_private_key=None, gas_limit=None, prior_tasks=None, reverses_task=None): @@ -151,7 +152,7 @@ def transact_with_contract_function(self, contract_address, function, abi_type= gas_limit, prior_tasks, reverses_task) -@app.task(**base_task_config) +@app.task(name=eth_endpoint('deploy_contract'), **base_task_config) def deploy_contract(self, contract_name, args=None, kwargs=None, signing_address=None, encrypted_private_key=None, gas_limit=None, prior_tasks=None): @@ -162,7 +163,7 @@ def deploy_contract(self, contract_name, args=None, kwargs=None, gas_limit, prior_tasks) -@app.task(**base_task_config) +@app.task(name=eth_endpoint('send_eth'), **base_task_config) def send_eth(self, amount_wei, recipient_address, signing_address=None, encrypted_private_key=None, prior_tasks=None, posterior_tasks=None): @@ -173,62 +174,62 @@ def send_eth(self, amount_wei, recipient_address, prior_tasks, posterior_tasks) -@app.task(**no_retry_config) +@app.task(name=eth_endpoint('retry_task'), **no_retry_config) def retry_task(self, task_uuid): return task_manager.retry_task(task_uuid) -@app.task(**no_retry_config) +@app.task(name=eth_endpoint('retry_failed'), **no_retry_config) def retry_failed(self, min_task_id=None, max_task_id=None, retry_unstarted=False): return task_manager.retry_failed(min_task_id, max_task_id, retry_unstarted) -@app.task(**base_task_config) +@app.task(name=eth_endpoint('get_task'), **base_task_config) def get_task(self, task_uuid): return persistence_module.get_serialised_task_from_uuid(task_uuid) -@app.task(base=SqlAlchemyTask) +@app.task(name=eth_endpoint('_handle_error'), base=SqlAlchemyTask) def _handle_error(request, exc, traceback, transaction_id): return supervisor.handle_error(request, exc, traceback, transaction_id) -@app.task(base=SqlAlchemyTask, bind=True, max_retries=config.ETH_CHECK_TRANSACTION_RETRIES, soft_time_limit=300) +@app.task(name=eth_endpoint('_check_transaction_response'), base=SqlAlchemyTask, bind=True, max_retries=chain_config['CHECK_TRANSACTION_RETRIES'], soft_time_limit=300) def _check_transaction_response(self, transaction_id): return supervisor.check_transaction_response(self, transaction_id) -@app.task(**base_task_config) +@app.task(name=eth_endpoint('attempt_transaction'), **base_task_config) def attempt_transaction(self, task_uuid): return supervisor.attempt_transaction(task_uuid) -@app.task(**processor_task_config) +@app.task(name=eth_endpoint('_process_send_eth_transaction'), **processor_task_config) def _process_send_eth_transaction(self, transaction_id, recipient_address, amount, task_id=None): return processor.process_send_eth_transaction(transaction_id, recipient_address, amount, task_id) -@app.task(**processor_task_config) +@app.task(name=eth_endpoint('_process_function_transaction'), **processor_task_config) def _process_function_transaction(self, transaction_id, contract_address, abi_type, function, args=None, kwargs=None, gas_limit=None, task_id=None): return processor.process_function_transaction(transaction_id, contract_address, abi_type, function, args, kwargs, gas_limit, task_id) -@app.task(**processor_task_config) +@app.task(name=eth_endpoint('_process_deploy_contract_transaction'), **processor_task_config) def _process_deploy_contract_transaction(self, transaction_id, contract_name, args=None, kwargs=None, gas_limit=None, task_id=None): return processor.process_deploy_contract_transaction(transaction_id, contract_name, args, kwargs, gas_limit, task_id) -@app.task(**base_task_config) +@app.task(name=eth_endpoint('register_contract'), **base_task_config) def register_contract(self, contract_address, abi, contract_name=None, require_name_matches=False): return processor.registry.register_contract( contract_address, abi, contract_name, require_name_matches ) -@app.task(**base_task_config) +@app.task(name=eth_endpoint('call_contract_function'), **base_task_config) def call_contract_function(self, contract_address, function, abi_type=None, args=None, kwargs=None, signing_address=None): return processor.call_contract_function(contract_address, abi_type, function, args, kwargs, diff --git a/eth_worker/eth_src/celery_utils.py b/eth_worker/eth_src/celery_utils.py index 90885aa53..fa8984122 100644 --- a/eth_worker/eth_src/celery_utils.py +++ b/eth_worker/eth_src/celery_utils.py @@ -1,16 +1,20 @@ from time import sleep from celery import signature +import os +import config + +chain = os.environ.get('CHAIN', config.DEFAULT_CHAIN) celery_tasks_name = 'celery_tasks' -eth_endpoint = lambda endpoint: f'{celery_tasks_name}.{endpoint}' -import config +eth_endpoint = lambda endpoint: f'{chain}.{celery_tasks_name}.{endpoint}' + def execute_synchronous_celery(signature): async_result = signature.delay() try: response = async_result.get( - timeout=config.SYNCRONOUS_TASK_TIMEOUT, + timeout=config.CHAINS[chain]['SYNCRONOUS_TASK_TIMEOUT'], propagate=True, interval=0.3) diff --git a/eth_worker/eth_src/eth_manager/eth_transaction_processor.py b/eth_worker/eth_src/eth_manager/eth_transaction_processor.py index 0c10e3965..77acbb528 100644 --- a/eth_worker/eth_src/eth_manager/eth_transaction_processor.py +++ b/eth_worker/eth_src/eth_manager/eth_transaction_processor.py @@ -8,9 +8,9 @@ from exceptions import PreBlockchainError from eth_manager.contract_registry.contract_registry import ContractRegistry from celery_utils import eth_endpoint +import celery_app from web3.exceptions import TransactionNotFound - class EthTransactionProcessor(object): """ Does the grunt work of trying to get a transaction onto an ethereum chain. @@ -216,10 +216,10 @@ def _send_signed_transaction(self, signed_transaction, transaction_id): def _get_gas_price(self, target_transaction_time=None): if not target_transaction_time: - target_transaction_time = config.ETH_TARGET_TRANSACTION_TIME + target_transaction_time = celery_app.chain_config['TARGET_TRANSACTION_TIME'] try: - gas_price_req = requests.get(config.ETH_GAS_PRICE_PROVIDER + '/price', + gas_price_req = requests.get(celery_app.chain_config['GAS_PRICE_PROVIDER'] + '/price', params={'max_wait_seconds': target_transaction_time}).json() gas_price = min(gas_price_req['gas_price'], self.gas_price_wei) diff --git a/eth_worker/eth_src/eth_manager/transaction_supervisor.py b/eth_worker/eth_src/eth_manager/transaction_supervisor.py index d7e63fa0e..3f4c6a49e 100644 --- a/eth_worker/eth_src/eth_manager/transaction_supervisor.py +++ b/eth_worker/eth_src/eth_manager/transaction_supervisor.py @@ -1,6 +1,7 @@ import datetime import celery_utils +import celery_app from celery import chain, signature from celery_utils import eth_endpoint @@ -181,7 +182,7 @@ def _topup_if_required(self, wallet, posterior_task_uuid): if balance <= wei_topup_threshold and wei_target_balance > balance: task_uuid = self.queue_send_eth( - signing_address=config.MASTER_WALLET_ADDRESS, + signing_address=celery_app.chain_config['MASTER_WALLET_ADDRESS'], amount_wei=wei_target_balance - balance, recipient_address=wallet.address, prior_tasks=[], diff --git a/eth_worker/eth_src/higher_order_tasks.py b/eth_worker/eth_src/higher_order_tasks.py index 0c37938c6..00e2aaf61 100644 --- a/eth_worker/eth_src/higher_order_tasks.py +++ b/eth_worker/eth_src/higher_order_tasks.py @@ -4,10 +4,10 @@ from celery import signature import config -from celery_app import persistence_module, w3, red, task_manager, processor +from celery_app import persistence_module, w3, red, task_manager, processor, chain_config from eth_src import celery_utils -timeout = config.SYNCRONOUS_TASK_TIMEOUT +timeout = chain_config['SYNCRONOUS_TASK_TIMEOUT'] def call_contract_function(contract_address, contract_type, func, args=None): sig = processor.sigs.call_contract_function(contract_address, contract_type, func, args) @@ -88,7 +88,7 @@ def topup_if_required(address): if balance <= wei_topup_threshold and wei_target_balance > balance: sig = signature(celery_utils.eth_endpoint('send_eth'), kwargs={ - 'signing_address': config.MASTER_WALLET_ADDRESS, + 'signing_address': chain_config['MASTER_WALLET_ADDRESS'], 'amount_wei': wei_target_balance - balance, 'recipient_address': address, 'prior_tasks': [] diff --git a/eth_worker/eth_src/sql_persistence/interface.py b/eth_worker/eth_src/sql_persistence/interface.py index b1db397a9..3eaf3ecdd 100644 --- a/eth_worker/eth_src/sql_persistence/interface.py +++ b/eth_worker/eth_src/sql_persistence/interface.py @@ -4,6 +4,7 @@ from sempo_types import UUID, UUIDList +from celery_utils import chain import config from sql_persistence.models import ( @@ -540,8 +541,7 @@ def set_block_range_status(self, start, end, status, filter_id): self.session.commit() - def __init__(self, red, session, first_block_hash, PENDING_TRANSACTION_EXPIRY_SECONDS=config.ETH_PENDING_TRANSACTION_EXPIRY_SECONDS): - + def __init__(self, red, session, first_block_hash, PENDING_TRANSACTION_EXPIRY_SECONDS=config.CHAINS[chain]['PENDING_TRANSACTION_EXPIRY_SECONDS']): self.red = red self.session = session diff --git a/eth_worker/test_eth_worker/conftest.py b/eth_worker/test_eth_worker/conftest.py index 31419e4bd..dad237dcd 100644 --- a/eth_worker/test_eth_worker/conftest.py +++ b/eth_worker/test_eth_worker/conftest.py @@ -19,7 +19,6 @@ from web3 import Web3 import config -from eth_src.eth_manager.eth_transaction_processor import EthTransactionProcessor from eth_src.eth_manager.transaction_supervisor import TransactionSupervisor from eth_src.eth_manager.task_manager import TaskManager from eth_src.eth_manager.blockchain_sync.blockchain_sync import BlockchainSyncer @@ -109,7 +108,7 @@ def mock_estimate_gas(*args, **kwargs): @pytest.fixture(scope='function') def processor(persistence_module, mock_w3): - + from eth_src.eth_manager.eth_transaction_processor import EthTransactionProcessor p = EthTransactionProcessor( ethereum_chain_id=1, w3=mock_w3, From 711b6eea2f2561bf59fd7c2c34005e097459b514 Mon Sep 17 00:00:00 2001 From: Michiel Date: Thu, 3 Dec 2020 08:45:38 -0400 Subject: [PATCH 5/6] Group by Sender and Recipient (#567) * do it * Test it!!! * Fix some oddball formatting * bit of formatting * Version ++ * PR Change * version * Bump version --- .../components/filterModule/FilterModule.jsx | 57 ++- app/package.json | 2 +- app/server/utils/metrics/group.py | 53 ++- app/server/utils/metrics/metrics_const.py | 7 +- .../utils/metrics/postprocessing_actions.py | 14 +- .../metrics_outputs/all_by_account_type.json | 2 +- ...by_account_type_filtered_by_recipient.json | 2 +- ...ll_by_account_type_filtered_by_sender.json | 2 +- .../all_by_recipient_colour.json | 348 ++++++++++++++++ .../all_by_recipient_location.json | 385 ++++++++++++++++++ .../metrics_outputs/all_by_sender_colour.json | 276 +++++++++++++ ...ation.json => all_by_sender_location.json} | 2 +- .../requested_metric_active_users.json | 2 +- .../metrics_outputs/user_by_account_type.json | 2 +- .../functional/api/test_metrics_api.py | 112 +++-- config.py | 2 +- 16 files changed, 1190 insertions(+), 78 deletions(-) create mode 100644 app/test_app/functional/api/metrics_outputs/all_by_recipient_colour.json create mode 100644 app/test_app/functional/api/metrics_outputs/all_by_recipient_location.json create mode 100644 app/test_app/functional/api/metrics_outputs/all_by_sender_colour.json rename app/test_app/functional/api/metrics_outputs/{all_by_location.json => all_by_sender_location.json} (99%) diff --git a/app/client/components/filterModule/FilterModule.jsx b/app/client/components/filterModule/FilterModule.jsx index 4a9af0874..8efd60bf0 100644 --- a/app/client/components/filterModule/FilterModule.jsx +++ b/app/client/components/filterModule/FilterModule.jsx @@ -5,7 +5,7 @@ import React from "react"; import { Space, Select } from "antd"; -const { Option } = Select; +const { Option, OptGroup } = Select; import { connect } from "react-redux"; import { LoadMetricAction } from "../../reducers/metric/actions"; @@ -50,8 +50,6 @@ class FilterModule extends React.Component { }; this.props.loadAllowedFilters(this.props.filterObject); - - console.log("Default groupby is", props.defaultGroupBy); } componentDidMount() { @@ -111,6 +109,22 @@ class FilterModule extends React.Component { render() { let { allowedGroups, defaultGroupBy, isMobile } = this.props; + const senderGroups = allowedGroups + ? Object.keys(allowedGroups).filter( + groupName => allowedGroups[groupName].sender_or_recipient == "sender" + ) + : []; + const recipientGroups = allowedGroups + ? Object.keys(allowedGroups).filter( + groupName => + allowedGroups[groupName].sender_or_recipient == "recipient" + ) + : []; + const ungroupedGroups = allowedGroups + ? Object.keys(allowedGroups).filter( + groupName => !allowedGroups[groupName].sender_or_recipient + ) + : []; let groupByModule = ( @@ -124,15 +138,34 @@ class FilterModule extends React.Component { style={{ width: 200 }} onChange={this.updateGroupBy} > - {allowedGroups - ? Object.keys(allowedGroups).map(key => { - return ( - - ); - }) - : null} + if(ungroupedGroups) + {ungroupedGroups.map(group => { + return ; + })} + if(senderGroups) + { + + {senderGroups.map(group => { + let label = allowedGroups[group]["name"]; + label = + this.state.groupBy == group ? "Sender ".concat(label) : label; + return ; + })} + + } + if(recipientGroups) + { + + {recipientGroups.map(group => { + let label = allowedGroups[group]["name"]; + label = + this.state.groupBy == group + ? "Recipient ".concat(label) + : label; + return ; + })} + + } ); diff --git a/app/package.json b/app/package.json index a1262e26b..4efbaeb55 100644 --- a/app/package.json +++ b/app/package.json @@ -1,6 +1,6 @@ { "name": "SempoBlockchain", - "version": "1.2.48", + "version": "1.2.49", "description": "Sempo blockchain web app using Python-Flask and React", "main": "index.js", "homepage": "./", diff --git a/app/server/utils/metrics/group.py b/app/server/utils/metrics/group.py index c7fc5ffe1..12a4dc568 100644 --- a/app/server/utils/metrics/group.py +++ b/app/server/utils/metrics/group.py @@ -13,7 +13,7 @@ from server import db # Every permutation of tables we want to join with, and how we would actually make that join -group_joining_strategies = { +sender_group_joining_strategies = { CreditTransfer.__tablename__: { User.__tablename__: lambda query : query.join(User, User.id == CreditTransfer.sender_user_id), TransferAccount.__tablename__: lambda query : query.join(TransferAccount, TransferAccount.id == CreditTransfer.sender_transfer_account_id), @@ -33,6 +33,26 @@ } } +recipient_group_joining_strategies = { + CreditTransfer.__tablename__: { + User.__tablename__: lambda query : query.join(User, User.id == CreditTransfer.recipient_user_id), + TransferAccount.__tablename__: lambda query : query.join(TransferAccount, TransferAccount.id == CreditTransfer.recipient_transfer_account_id), + CustomAttributeUserStorage.__tablename__: lambda query, name : query.join(User, CreditTransfer.recipient_user_id == User.id)\ + .join(CustomAttributeUserStorage, User.id == CustomAttributeUserStorage.user_id)\ + .join(CustomAttribute, CustomAttribute.id == CustomAttributeUserStorage.custom_attribute_id)\ + .filter(CustomAttribute.name == name), + TransferUsage.__tablename__: lambda query : query.join(credit_transfer_transfer_usage_association_table, + credit_transfer_transfer_usage_association_table.c.credit_transfer_id == CreditTransfer.id)\ + .join(TransferUsage, credit_transfer_transfer_usage_association_table.c.transfer_usage_id == TransferUsage.id) + }, + User.__tablename__: { + CustomAttributeUserStorage.__tablename__: lambda query, name : query.join(CustomAttributeUserStorage, User.id == CustomAttributeUserStorage.user_id)\ + .join(CustomAttribute, CustomAttribute.id == CustomAttributeUserStorage.custom_attribute_id)\ + .filter(CustomAttribute.name == name), + TransferAccount.__tablename__: lambda query : query.join(TransferAccount, TransferAccount.id == User.default_transfer_account_id), + } +} + class Group(object): def build_query_group_by_with_join(self, query, query_object_model): """ @@ -49,7 +69,11 @@ def build_query_group_by_with_join(self, query, query_object_model): args = (grouped_query, self.custom_attribute_field_name) else: args = (grouped_query,) - return group_joining_strategies[query_object_model.__tablename__][self.group_object_model.__tablename__](*args) + if self.sender_or_recipient == 'recipient': + return recipient_group_joining_strategies[query_object_model.__tablename__][self.group_object_model.__tablename__](*args) + else: + return sender_group_joining_strategies[query_object_model.__tablename__][self.group_object_model.__tablename__](*args) + except KeyError: raise Exception(f'No strategy to join tables {query_object_model.__tablename__} and {self.group_object_model.__tablename__}') return grouped_query @@ -58,13 +82,15 @@ def get_api_representation(self): return { 'name': self.name, 'table': self.group_object_model.__tablename__, + 'sender_or_recipient': self.sender_or_recipient } def __init__(self, name, group_object_model, group_by_column, - custom_attribute_field_name = None): + custom_attribute_field_name = None, + sender_or_recipient = None): """ :param name: The title of the group :param group_object_model: The object model of the thing we're grouping by @@ -74,6 +100,7 @@ def __init__(self, self.group_object_model = group_object_model self.group_by_column = group_by_column self.custom_attribute_field_name = custom_attribute_field_name + self.sender_or_recipient = sender_or_recipient # Builds Group objects for all custom attributes in the database def get_custom_attribute_groups(): @@ -83,7 +110,10 @@ def get_custom_attribute_groups(): # Build those into group objects groups = {} for ao in attribute_options: - groups[ao.name] = Group(ao.name.capitalize(), CustomAttributeUserStorage, CustomAttributeUserStorage.value, ao.name) + if ao.group_visibility == MetricsVisibility.SENDER or ao.group_visibility == MetricsVisibility.SENDER_AND_RECIPIENT: + groups[ao.name+',sender'] = Group(ao.name.capitalize(), CustomAttributeUserStorage, CustomAttributeUserStorage.value, ao.name, sender_or_recipient='sender') + if ao.group_visibility == MetricsVisibility.RECIPIENT or ao.group_visibility == MetricsVisibility.SENDER_AND_RECIPIENT: + groups[ao.name+',recipient'] = Group(ao.name.capitalize(), CustomAttributeUserStorage, CustomAttributeUserStorage.value, ao.name, sender_or_recipient='recipient') return groups class Groups(object): @@ -91,12 +121,15 @@ class Groups(object): def GROUP_TYPES(self): fixed_groups = { UNGROUPED: None, - LOCATION: Group('Location', User, User._location), TRANSFER_TYPE: Group('Transfer Type', CreditTransfer, CreditTransfer.public_transfer_type), - TRANSFER_STATUS: Group('Transfer Status', CreditTransfer, CreditTransfer.transfer_status), TRANSFER_MODE: Group('Transfer Mode', CreditTransfer, CreditTransfer.transfer_mode), - ACCOUNT_TYPE: Group('Account Type', TransferAccount, TransferAccount.account_type), + TRANSFER_STATUS: Group('Transfer Status', CreditTransfer, CreditTransfer.transfer_status), TRANSFER_USAGE: Group('Transfer Usages', TransferUsage, TransferUsage._name), + SENDER_LOCATION: Group('Location', User, User._location, sender_or_recipient='sender'), + SENDER_ACCOUNT_TYPE: Group('Account Type', TransferAccount, TransferAccount.account_type, sender_or_recipient='sender'), + RECIPIENT_LOCATION: Group('Location', User, User._location, sender_or_recipient='recipient'), + RECIPIENT_ACCOUNT_TYPE: Group('Account Type', TransferAccount, TransferAccount.account_type, sender_or_recipient='recipient'), + } custom_attribute_groups = get_custom_attribute_groups() return {**fixed_groups, **custom_attribute_groups} @@ -110,8 +143,10 @@ def TRANSFER_GROUPS(self): @property def USER_GROUPS(self): fixed_groups = { - ACCOUNT_TYPE: self.GROUP_TYPES[ACCOUNT_TYPE], - LOCATION: self.GROUP_TYPES[LOCATION], + SENDER_ACCOUNT_TYPE: self.GROUP_TYPES[SENDER_ACCOUNT_TYPE], + RECIPIENT_ACCOUNT_TYPE: self.GROUP_TYPES[RECIPIENT_ACCOUNT_TYPE], + SENDER_LOCATION: self.GROUP_TYPES[SENDER_LOCATION], + RECIPIENT_LOCATION: self.GROUP_TYPES[RECIPIENT_LOCATION], UNGROUPED: None, } custom_attribute_groups = get_custom_attribute_groups() diff --git a/app/server/utils/metrics/metrics_const.py b/app/server/utils/metrics/metrics_const.py index d68b7241c..8d67a9b27 100644 --- a/app/server/utils/metrics/metrics_const.py +++ b/app/server/utils/metrics/metrics_const.py @@ -47,13 +47,14 @@ ] # Group by values -LOCATION = 'location' TRANSFER_TYPE = 'transfer_type' TRANSFER_MODE = 'transfer_mode' -ACCOUNT_TYPE = 'account_type' -TRANSFER_SUBTYPE = 'transfer_subtype' TRANSFER_STATUS = 'transfer_status' TRANSFER_USAGE = 'transfer_usage' +SENDER_LOCATION = 'sender,location' +SENDER_ACCOUNT_TYPE = 'sender,account_type' +RECIPIENT_LOCATION = 'recipient,location' +RECIPIENT_ACCOUNT_TYPE = 'recipient,account_type' UNGROUPED = 'ungrouped' diff --git a/app/server/utils/metrics/postprocessing_actions.py b/app/server/utils/metrics/postprocessing_actions.py index bed59daa6..b60bbed37 100644 --- a/app/server/utils/metrics/postprocessing_actions.py +++ b/app/server/utils/metrics/postprocessing_actions.py @@ -146,14 +146,15 @@ def calculate_timeseries_per_user(query_result, population_query_result): try: last_successful_population_lookup = population_dates[result[1]] - - product = result[0] / last_successful_population_lookup[index] - + if last_successful_population_lookup[index] != 0: + product = result[0] / (last_successful_population_lookup[index]) + else: + product = 0 except KeyError as e: # Makes the data slightly more robust to missing info # by allowing us to fallback to the last population info if last_successful_population_lookup: - product = result[0] / last_successful_population_lookup[index] + product = result[0] / last_successful_population_lookup[index] if index in last_successful_population_lookup else 0 else: product = 0 @@ -176,7 +177,10 @@ def calculate_aggregate_per_user(query_result, population_query_result): if pqr[1] == last_day: category_populations[pqr[2]] = pqr[0] for r in query_result: - result.append((r[0]/category_populations[r[1]], r[1])) + if r[1] in category_populations: + result.append((r[0]/category_populations[r[1]], r[1])) + else: + result.append((0, r[1])) return result elif UNGROUPED in population_query_result: diff --git a/app/test_app/functional/api/metrics_outputs/all_by_account_type.json b/app/test_app/functional/api/metrics_outputs/all_by_account_type.json index f12c622ce..281556276 100644 --- a/app/test_app/functional/api/metrics_outputs/all_by_account_type.json +++ b/app/test_app/functional/api/metrics_outputs/all_by_account_type.json @@ -1,6 +1,6 @@ { "active_filters": [], - "active_group_by": "account_type", + "active_group_by": "sender,account_type", "active_users": { "aggregate": { "ORGANISATION": 1, diff --git a/app/test_app/functional/api/metrics_outputs/all_by_account_type_filtered_by_recipient.json b/app/test_app/functional/api/metrics_outputs/all_by_account_type_filtered_by_recipient.json index 6f8b04a1f..efac2b053 100644 --- a/app/test_app/functional/api/metrics_outputs/all_by_account_type_filtered_by_recipient.json +++ b/app/test_app/functional/api/metrics_outputs/all_by_account_type_filtered_by_recipient.json @@ -1,6 +1,6 @@ { "active_filters": ["rounded_account_balance,recipient"], - "active_group_by": "account_type", + "active_group_by": "sender,account_type", "active_users": { "aggregate": { "USER": 4, diff --git a/app/test_app/functional/api/metrics_outputs/all_by_account_type_filtered_by_sender.json b/app/test_app/functional/api/metrics_outputs/all_by_account_type_filtered_by_sender.json index 4631e16f8..59ba3e78b 100644 --- a/app/test_app/functional/api/metrics_outputs/all_by_account_type_filtered_by_sender.json +++ b/app/test_app/functional/api/metrics_outputs/all_by_account_type_filtered_by_sender.json @@ -1,6 +1,6 @@ { "active_filters": ["rounded_account_balance,sender"], - "active_group_by": "account_type", + "active_group_by": "sender,account_type", "active_users": { "aggregate": { "USER": 4, diff --git a/app/test_app/functional/api/metrics_outputs/all_by_recipient_colour.json b/app/test_app/functional/api/metrics_outputs/all_by_recipient_colour.json new file mode 100644 index 000000000..ca536483c --- /dev/null +++ b/app/test_app/functional/api/metrics_outputs/all_by_recipient_colour.json @@ -0,0 +1,348 @@ +{ + "active_filters": [], + "active_group_by": "colour,recipient", + "active_users": { + "aggregate": { + "blue": 2, + "green": 2, + "percent_change": 100, + "red": 4, + "total": 6 + }, + "timeseries": { + "blue": [ + { + "date": "2020-11-23T00:00:00", + "value": 1 + }, + { + "date": "2020-11-29T00:00:00", + "value": 1 + }, + { + "date": "2020-11-30T00:00:00", + "value": 1 + } + ], + "green": [ + { + "date": "2020-11-11T00:00:00", + "value": 1 + }, + { + "date": "2020-11-25T00:00:00", + "value": 1 + } + ], + "red": [ + { + "date": "2020-11-11T00:00:00", + "value": 1 + }, + { + "date": "2020-11-27T00:00:00", + "value": 1 + }, + { + "date": "2020-11-30T00:00:00", + "value": 1 + }, + { + "date": "2020-12-01T00:00:00", + "value": 2 + } + ] + }, + "type": { + "display_decimals": 0, + "value_type": "count" + } + }, + "all_payments_volume": { + "aggregate": { + "blue": 436, + "green": 222, + "percent_change": 97.04433497536947, + "red": 1029, + "total": 1687 + }, + "timeseries": { + "blue": [ + { + "date": "2020-11-23T00:00:00", + "value": 201 + }, + { + "date": "2020-11-29T00:00:00", + "value": 210 + }, + { + "date": "2020-11-30T00:00:00", + "value": 25 + } + ], + "green": [ + { + "date": "2020-11-11T00:00:00", + "value": 202 + }, + { + "date": "2020-11-25T00:00:00", + "value": 20 + } + ], + "red": [ + { + "date": "2020-11-11T00:00:00", + "value": 204 + }, + { + "date": "2020-11-27T00:00:00", + "value": 20 + }, + { + "date": "2020-11-30T00:00:00", + "value": 5 + }, + { + "date": "2020-12-01T00:00:00", + "value": 800 + } + ] + }, + "type": { + "currency_name": "AUD Reserve Token", + "currency_symbol": "AUD", + "display_decimals": 2, + "value_type": "currency" + } + }, + "daily_transaction_count": { + "aggregate": { + "blue": 1, + "green": 1, + "percent_change": null, + "red": 3, + "total": 5 + }, + "timeseries": { + "blue": [ + { + "date": "2020-11-30T00:00:00", + "value": 1 + } + ], + "green": [ + { + "date": "2020-11-25T00:00:00", + "value": 1 + } + ], + "red": [ + { + "date": "2020-11-27T00:00:00", + "value": 1 + }, + { + "date": "2020-11-30T00:00:00", + "value": 1 + }, + { + "date": "2020-12-01T00:00:00", + "value": 1 + } + ] + }, + "type": { + "display_decimals": 0, + "value_type": "count" + } + }, + "mandatory_filter": {}, + "master_wallet_balance": -1517, + "total_distributed": 1517, + "total_reclaimed": 0, + "total_withdrawn": 0, + "trades_per_user": { + "aggregate": { + "blue": 0.5, + "green": 1, + "percent_change": null, + "red": 1, + "total": 0.7142857142857143 + }, + "timeseries": { + "blue": [ + { + "date": "2020-11-30T00:00:00", + "value": 0.5 + } + ], + "green": [ + { + "date": "2020-11-25T00:00:00", + "value": 1 + } + ], + "red": [ + { + "date": "2020-11-27T00:00:00", + "value": 1 + }, + { + "date": "2020-11-30T00:00:00", + "value": 1 + }, + { + "date": "2020-12-01T00:00:00", + "value": 0.3333333333333333 + } + ] + }, + "type": { + "display_decimals": 2, + "value_type": "count_average" + } + }, + "transfer_amount_per_user": { + "aggregate": { + "blue": 12.5, + "green": 20, + "percent_change": null, + "red": 41.666666666666664, + "total": 24.285714285714285 + }, + "timeseries": { + "blue": [ + { + "date": "2020-11-30T00:00:00", + "value": 12.5 + } + ], + "green": [ + { + "date": "2020-11-25T00:00:00", + "value": 20 + } + ], + "red": [ + { + "date": "2020-11-27T00:00:00", + "value": 20 + }, + { + "date": "2020-11-30T00:00:00", + "value": 5 + }, + { + "date": "2020-12-01T00:00:00", + "value": 33.333333333333336 + } + ] + }, + "type": { + "currency_name": "AUD Reserve Token", + "currency_symbol": "AUD", + "display_decimals": 2, + "value_type": "currency" + } + }, + "users_created": { + "aggregate": { + "blue": 2, + "green": 1, + "percent_change": 50, + "red": 3, + "total": 7 + }, + "timeseries": { + "blue": [ + { + "date": "2020-11-27T00:00:00", + "value": 1 + }, + { + "date": "2020-11-30T00:00:00", + "value": 1 + } + ], + "green": [ + { + "date": "2020-11-21T00:00:00", + "value": 1 + } + ], + "red": [ + { + "date": "2020-11-21T00:00:00", + "value": 1 + }, + { + "date": "2020-12-01T00:00:00", + "value": 2 + } + ] + }, + "type": { + "display_decimals": 0, + "value_type": "count" + } + }, + "users_who_made_purchase": { + "aggregate": { + "blue": 2, + "green": 2, + "percent_change": 100, + "red": 4, + "total": 6 + }, + "timeseries": { + "blue": [ + { + "date": "2020-11-23T00:00:00", + "value": 1 + }, + { + "date": "2020-11-29T00:00:00", + "value": 1 + }, + { + "date": "2020-11-30T00:00:00", + "value": 1 + } + ], + "green": [ + { + "date": "2020-11-11T00:00:00", + "value": 1 + }, + { + "date": "2020-11-25T00:00:00", + "value": 1 + } + ], + "red": [ + { + "date": "2020-11-11T00:00:00", + "value": 1 + }, + { + "date": "2020-11-27T00:00:00", + "value": 1 + }, + { + "date": "2020-11-30T00:00:00", + "value": 1 + }, + { + "date": "2020-12-01T00:00:00", + "value": 2 + } + ] + }, + "type": { + "display_decimals": 0, + "value_type": "count" + } + } +} diff --git a/app/test_app/functional/api/metrics_outputs/all_by_recipient_location.json b/app/test_app/functional/api/metrics_outputs/all_by_recipient_location.json new file mode 100644 index 000000000..0f3c89897 --- /dev/null +++ b/app/test_app/functional/api/metrics_outputs/all_by_recipient_location.json @@ -0,0 +1,385 @@ +{ + "active_filters": [], + "active_group_by": "recipient,location", + "active_users": { + "aggregate": { + "Dartmouth": 1, + "Lower Sackville": 2, + "None": 2, + "Sunnyvale": 3, + "Truro": 2, + "percent_change": 100, + "total": 6 + }, + "timeseries": { + "Dartmouth": [ + { + "date": "2020-11-29T00:00:00", + "value": 1 + } + ], + "Lower Sackville": [ + { + "date": "2020-11-23T00:00:00", + "value": 1 + }, + { + "date": "2020-11-30T00:00:00", + "value": 1 + } + ], + "None": [ + { + "date": "2020-11-11T00:00:00", + "value": 1 + }, + { + "date": "2020-11-27T00:00:00", + "value": 1 + } + ], + "Sunnyvale": [ + { + "date": "2020-11-30T00:00:00", + "value": 1 + }, + { + "date": "2020-12-01T00:00:00", + "value": 2 + } + ], + "Truro": [ + { + "date": "2020-11-11T00:00:00", + "value": 1 + }, + { + "date": "2020-11-25T00:00:00", + "value": 1 + } + ] + }, + "type": { + "display_decimals": 0, + "value_type": "count" + } + }, + "all_payments_volume": { + "aggregate": { + "Dartmouth": 210, + "Lower Sackville": 226, + "None": 224, + "Sunnyvale": 805, + "Truro": 222, + "percent_change": 97.04433497536947, + "total": 1687 + }, + "timeseries": { + "Dartmouth": [ + { + "date": "2020-11-29T00:00:00", + "value": 210 + } + ], + "Lower Sackville": [ + { + "date": "2020-11-23T00:00:00", + "value": 201 + }, + { + "date": "2020-11-30T00:00:00", + "value": 25 + } + ], + "None": [ + { + "date": "2020-11-11T00:00:00", + "value": 204 + }, + { + "date": "2020-11-27T00:00:00", + "value": 20 + } + ], + "Sunnyvale": [ + { + "date": "2020-11-30T00:00:00", + "value": 5 + }, + { + "date": "2020-12-01T00:00:00", + "value": 800 + } + ], + "Truro": [ + { + "date": "2020-11-11T00:00:00", + "value": 202 + }, + { + "date": "2020-11-25T00:00:00", + "value": 20 + } + ] + }, + "type": { + "currency_name": "AUD Reserve Token", + "currency_symbol": "AUD", + "display_decimals": 2, + "value_type": "currency" + } + }, + "daily_transaction_count": { + "aggregate": { + "Lower Sackville": 1, + "None": 1, + "Sunnyvale": 2, + "Truro": 1, + "percent_change": null, + "total": 5 + }, + "timeseries": { + "Lower Sackville": [ + { + "date": "2020-11-30T00:00:00", + "value": 1 + } + ], + "None": [ + { + "date": "2020-11-27T00:00:00", + "value": 1 + } + ], + "Sunnyvale": [ + { + "date": "2020-11-30T00:00:00", + "value": 1 + }, + { + "date": "2020-12-01T00:00:00", + "value": 1 + } + ], + "Truro": [ + { + "date": "2020-11-25T00:00:00", + "value": 1 + } + ] + }, + "type": { + "display_decimals": 0, + "value_type": "count" + } + }, + "mandatory_filter": {}, + "master_wallet_balance": -1517, + "total_distributed": 1517, + "total_reclaimed": 0, + "total_withdrawn": 0, + "trades_per_user": { + "aggregate": { + "Lower Sackville": 1, + "None": 0.5, + "Sunnyvale": 1, + "Truro": 1, + "percent_change": null, + "total": 0.7142857142857143 + }, + "timeseries": { + "Lower Sackville": [ + { + "date": "2020-11-30T00:00:00", + "value": 1 + } + ], + "None": [ + { + "date": "2020-11-27T00:00:00", + "value": 1 + } + ], + "Sunnyvale": [ + { + "date": "2020-11-30T00:00:00", + "value": 0 + }, + { + "date": "2020-12-01T00:00:00", + "value": 0.5 + } + ], + "Truro": [ + { + "date": "2020-11-25T00:00:00", + "value": 1 + } + ] + }, + "type": { + "display_decimals": 2, + "value_type": "count_average" + } + }, + "transfer_amount_per_user": { + "aggregate": { + "Lower Sackville": 25, + "None": 10, + "Sunnyvale": 52.5, + "Truro": 20, + "percent_change": null, + "total": 24.285714285714285 + }, + "timeseries": { + "Lower Sackville": [ + { + "date": "2020-11-30T00:00:00", + "value": 25 + } + ], + "None": [ + { + "date": "2020-11-27T00:00:00", + "value": 20 + } + ], + "Sunnyvale": [ + { + "date": "2020-11-30T00:00:00", + "value": 0 + }, + { + "date": "2020-12-01T00:00:00", + "value": 50 + } + ], + "Truro": [ + { + "date": "2020-11-25T00:00:00", + "value": 20 + } + ] + }, + "type": { + "currency_name": "AUD Reserve Token", + "currency_symbol": "AUD", + "display_decimals": 2, + "value_type": "currency" + } + }, + "users_created": { + "aggregate": { + "Dartmouth": 1, + "Lower Sackville": 1, + "None": 2, + "Sunnyvale": 2, + "Truro": 1, + "percent_change": 50, + "total": 7 + }, + "timeseries": { + "Dartmouth": [ + { + "date": "2020-11-30T00:00:00", + "value": 1 + } + ], + "Lower Sackville": [ + { + "date": "2020-11-27T00:00:00", + "value": 1 + } + ], + "None": [ + { + "date": "2020-11-21T00:00:00", + "value": 1 + }, + { + "date": "2020-12-01T00:00:00", + "value": 1 + } + ], + "Sunnyvale": [ + { + "date": "2020-12-01T00:00:00", + "value": 2 + } + ], + "Truro": [ + { + "date": "2020-11-21T00:00:00", + "value": 1 + } + ] + }, + "type": { + "display_decimals": 0, + "value_type": "count" + } + }, + "users_who_made_purchase": { + "aggregate": { + "Dartmouth": 1, + "Lower Sackville": 2, + "None": 2, + "Sunnyvale": 3, + "Truro": 2, + "percent_change": 100, + "total": 6 + }, + "timeseries": { + "Dartmouth": [ + { + "date": "2020-11-29T00:00:00", + "value": 1 + } + ], + "Lower Sackville": [ + { + "date": "2020-11-23T00:00:00", + "value": 1 + }, + { + "date": "2020-11-30T00:00:00", + "value": 1 + } + ], + "None": [ + { + "date": "2020-11-11T00:00:00", + "value": 1 + }, + { + "date": "2020-11-27T00:00:00", + "value": 1 + } + ], + "Sunnyvale": [ + { + "date": "2020-11-30T00:00:00", + "value": 1 + }, + { + "date": "2020-12-01T00:00:00", + "value": 2 + } + ], + "Truro": [ + { + "date": "2020-11-11T00:00:00", + "value": 1 + }, + { + "date": "2020-11-25T00:00:00", + "value": 1 + } + ] + }, + "type": { + "display_decimals": 0, + "value_type": "count" + } + } +} diff --git a/app/test_app/functional/api/metrics_outputs/all_by_sender_colour.json b/app/test_app/functional/api/metrics_outputs/all_by_sender_colour.json new file mode 100644 index 000000000..9b689f5f0 --- /dev/null +++ b/app/test_app/functional/api/metrics_outputs/all_by_sender_colour.json @@ -0,0 +1,276 @@ +{ + "active_filters": [], + "active_group_by": "colour,sender", + "active_users": { + "aggregate": { + "blue": 2, + "green": 1, + "percent_change": 100, + "red": 2, + "total": 6 + }, + "timeseries": { + "blue": [ + { + "date": "2020-11-30T00:00:00", + "value": 2 + } + ], + "green": [ + { + "date": "2020-11-27T00:00:00", + "value": 1 + } + ], + "red": [ + { + "date": "2020-11-25T00:00:00", + "value": 1 + }, + { + "date": "2020-12-01T00:00:00", + "value": 1 + } + ] + }, + "type": { + "display_decimals": 0, + "value_type": "count" + } + }, + "all_payments_volume": { + "aggregate": { + "blue": 30, + "green": 20, + "percent_change": 97.04433497536947, + "red": 120, + "total": 1687 + }, + "timeseries": { + "blue": [ + { + "date": "2020-11-30T00:00:00", + "value": 30 + } + ], + "green": [ + { + "date": "2020-11-27T00:00:00", + "value": 20 + } + ], + "red": [ + { + "date": "2020-11-25T00:00:00", + "value": 20 + }, + { + "date": "2020-12-01T00:00:00", + "value": 100 + } + ] + }, + "type": { + "currency_name": "AUD Reserve Token", + "currency_symbol": "AUD", + "display_decimals": 2, + "value_type": "currency" + } + }, + "daily_transaction_count": { + "aggregate": { + "blue": 2, + "green": 1, + "percent_change": null, + "red": 2, + "total": 5 + }, + "timeseries": { + "blue": [ + { + "date": "2020-11-30T00:00:00", + "value": 2 + } + ], + "green": [ + { + "date": "2020-11-27T00:00:00", + "value": 1 + } + ], + "red": [ + { + "date": "2020-11-25T00:00:00", + "value": 1 + }, + { + "date": "2020-12-01T00:00:00", + "value": 1 + } + ] + }, + "type": { + "display_decimals": 0, + "value_type": "count" + } + }, + "mandatory_filter": {}, + "master_wallet_balance": -1517, + "total_distributed": 1517, + "total_reclaimed": 0, + "total_withdrawn": 0, + "trades_per_user": { + "aggregate": { + "blue": 1, + "green": 1, + "percent_change": null, + "red": 0.6666666666666666, + "total": 0.7142857142857143 + }, + "timeseries": { + "blue": [ + { + "date": "2020-11-30T00:00:00", + "value": 1 + } + ], + "green": [ + { + "date": "2020-11-27T00:00:00", + "value": 1 + } + ], + "red": [ + { + "date": "2020-11-25T00:00:00", + "value": 1 + }, + { + "date": "2020-12-01T00:00:00", + "value": 0.3333333333333333 + } + ] + }, + "type": { + "display_decimals": 2, + "value_type": "count_average" + } + }, + "transfer_amount_per_user": { + "aggregate": { + "blue": 15, + "green": 20, + "percent_change": null, + "red": 40, + "total": 24.285714285714285 + }, + "timeseries": { + "blue": [ + { + "date": "2020-11-30T00:00:00", + "value": 15 + } + ], + "green": [ + { + "date": "2020-11-27T00:00:00", + "value": 20 + } + ], + "red": [ + { + "date": "2020-11-25T00:00:00", + "value": 20 + }, + { + "date": "2020-12-01T00:00:00", + "value": 33.333333333333336 + } + ] + }, + "type": { + "currency_name": "AUD Reserve Token", + "currency_symbol": "AUD", + "display_decimals": 2, + "value_type": "currency" + } + }, + "users_created": { + "aggregate": { + "blue": 2, + "green": 1, + "percent_change": 50, + "red": 3, + "total": 7 + }, + "timeseries": { + "blue": [ + { + "date": "2020-11-27T00:00:00", + "value": 1 + }, + { + "date": "2020-11-30T00:00:00", + "value": 1 + } + ], + "green": [ + { + "date": "2020-11-21T00:00:00", + "value": 1 + } + ], + "red": [ + { + "date": "2020-11-21T00:00:00", + "value": 1 + }, + { + "date": "2020-12-01T00:00:00", + "value": 2 + } + ] + }, + "type": { + "display_decimals": 0, + "value_type": "count" + } + }, + "users_who_made_purchase": { + "aggregate": { + "blue": 2, + "green": 1, + "percent_change": 100, + "red": 2, + "total": 6 + }, + "timeseries": { + "blue": [ + { + "date": "2020-11-30T00:00:00", + "value": 2 + } + ], + "green": [ + { + "date": "2020-11-27T00:00:00", + "value": 1 + } + ], + "red": [ + { + "date": "2020-11-25T00:00:00", + "value": 1 + }, + { + "date": "2020-12-01T00:00:00", + "value": 1 + } + ] + }, + "type": { + "display_decimals": 0, + "value_type": "count" + } + } +} diff --git a/app/test_app/functional/api/metrics_outputs/all_by_location.json b/app/test_app/functional/api/metrics_outputs/all_by_sender_location.json similarity index 99% rename from app/test_app/functional/api/metrics_outputs/all_by_location.json rename to app/test_app/functional/api/metrics_outputs/all_by_sender_location.json index 6ad1f8d49..89c8a1173 100644 --- a/app/test_app/functional/api/metrics_outputs/all_by_location.json +++ b/app/test_app/functional/api/metrics_outputs/all_by_sender_location.json @@ -1,6 +1,6 @@ { "active_filters": [], - "active_group_by": "location", + "active_group_by": "sender,location", "active_users": { "aggregate": { "Dartmouth": 1, diff --git a/app/test_app/functional/api/metrics_outputs/requested_metric_active_users.json b/app/test_app/functional/api/metrics_outputs/requested_metric_active_users.json index 305965a49..e67fa6c59 100644 --- a/app/test_app/functional/api/metrics_outputs/requested_metric_active_users.json +++ b/app/test_app/functional/api/metrics_outputs/requested_metric_active_users.json @@ -1,6 +1,6 @@ { "active_filters": [], - "active_group_by": "account_type", + "active_group_by": "sender,account_type", "active_users": { "aggregate": { "ORGANISATION": 1, diff --git a/app/test_app/functional/api/metrics_outputs/user_by_account_type.json b/app/test_app/functional/api/metrics_outputs/user_by_account_type.json index ede1c007b..e0e72bc15 100644 --- a/app/test_app/functional/api/metrics_outputs/user_by_account_type.json +++ b/app/test_app/functional/api/metrics_outputs/user_by_account_type.json @@ -1,6 +1,6 @@ { "active_filters": [], - "active_group_by": "account_type", + "active_group_by": "sender,account_type", "active_users": { "aggregate": { "ORGANISATION": 1, diff --git a/app/test_app/functional/api/test_metrics_api.py b/app/test_app/functional/api/test_metrics_api.py index 59909f656..818ca5881 100644 --- a/app/test_app/functional/api/test_metrics_api.py +++ b/app/test_app/functional/api/test_metrics_api.py @@ -1,5 +1,6 @@ import pytest from server.models.transfer_usage import TransferUsage +from server.models.custom_attribute import CustomAttribute, MetricsVisibility from server.utils.transfer_filter import Filters from server.utils.credit_transfer import make_payment_transfer from server.utils.user import create_transfer_account_user, set_custom_attributes @@ -11,6 +12,9 @@ @pytest.fixture(scope='module') def generate_timeseries_metrics(create_organisation): + attribute_dict = {'custom_attributes': {}} + attribute_dict['custom_attributes']['colour'] = 'red' + # Generates metrics over timeline # User1 and User2 made today user1 = create_transfer_account_user(first_name='Ricky', @@ -20,6 +24,8 @@ def generate_timeseries_metrics(create_organisation): user1.default_transfer_account.is_approved = True user1.default_transfer_account._make_initial_disbursement(100, True) user1._location = 'Sunnyvale' + attribute_dict['custom_attributes']['colour'] = 'red' + set_custom_attributes(attribute_dict, user1) user2 = create_transfer_account_user(first_name='Bubbles', phone="+19025551235", @@ -27,6 +33,9 @@ def generate_timeseries_metrics(create_organisation): user2.default_transfer_account.is_approved = True user2.default_transfer_account._make_initial_disbursement(200, True) user2._location = 'Sunnyvale' + attribute_dict['custom_attributes']['colour'] = 'red' + + set_custom_attributes(attribute_dict, user2) # user3 made yesterday user3 = create_transfer_account_user(first_name='Julian', @@ -37,6 +46,8 @@ def generate_timeseries_metrics(create_organisation): user3.created = user3.created - timedelta(days=1) disburse.created = user3.created - timedelta(days=1) user3._location = 'Dartmouth' + attribute_dict['custom_attributes']['colour'] = 'blue' + set_custom_attributes(attribute_dict, user3) # user4 made 4 days ago user4 = create_transfer_account_user(first_name='Randy', @@ -47,6 +58,8 @@ def generate_timeseries_metrics(create_organisation): user4.created = user4.created - timedelta(days=4) disburse.created = user4.created - timedelta(days=4) user4._location = 'Lower Sackville' + attribute_dict['custom_attributes']['colour'] = 'blue' + set_custom_attributes(attribute_dict, user4) # user5/user6 made 10 days ago user5 = create_transfer_account_user(first_name='Cory', @@ -57,6 +70,8 @@ def generate_timeseries_metrics(create_organisation): user5.created = user5.created - timedelta(days=10) disburse.created = user5.created - timedelta(days=10) user5._location = 'Truro' + attribute_dict['custom_attributes']['colour'] = 'green' + set_custom_attributes(attribute_dict, user5) user6 = create_transfer_account_user(first_name='Trevor', phone="+19025111230", @@ -65,9 +80,9 @@ def generate_timeseries_metrics(create_organisation): disburse = user6.default_transfer_account._make_initial_disbursement(204, True) user6.created = user6.created - timedelta(days=10) disburse.created = user6.created - timedelta(days=10) - + attribute_dict['custom_attributes']['colour'] = 'red' + set_custom_attributes(attribute_dict, user6) db.session.commit() - tu1 = TransferUsage.find_or_create("Pepperoni") tu2 = TransferUsage.find_or_create("Jalepeno Chips") tu3 = TransferUsage.find_or_create("Shopping Carts") @@ -125,36 +140,11 @@ def generate_timeseries_metrics(create_organisation): p4.created = p4.created - timedelta(days=6) db.session.commit() -@pytest.mark.parametrize("metric_type, status_code", [ - ("user", 200), - ("all", 200), - ("credit_transfer", 200), - ("notarealmetrictype", 500), -]) -def test_get_metric_filters(test_client, complete_admin_auth_token, external_reserve_token, - metric_type, status_code): - # Tests getting list of availible metrics filters from the /api/v1/metrics/filters endpoint - response = test_client.get( - f'/api/v1/metrics/filters/?metric_type={metric_type}', - headers=dict( - Authorization=complete_admin_auth_token, - Accept='application/json' - ), - ) - - assert response.status_code == status_code - filters = Filters() - if status_code == 200: - if metric_type == 'user': - assert response.json['data']['filters'] == filters.USER_FILTERS - elif metric_type == 'transfer': - assert response.json['data']['filters'] == filters.TRANSFER_FILTERS - base_participant = { 'data': {'transfer_stats': {'active_filters': [], - 'active_group_by': 'account_type', + 'active_group_by': 'recipient,account_type', 'active_users': {'aggregate': {'percent_change': None, 'total': 0}, 'timeseries': {}, 'type': {'display_decimals': 0, 'value_type': 'count'}}, @@ -173,7 +163,7 @@ def test_get_metric_filters(test_client, complete_admin_auth_token, external_res 'data': {'transfer_stats': {'active_filters': [], - 'active_group_by': 'account_type', + 'active_group_by': 'recipient,account_type', 'active_users': {'aggregate': {'percent_change': None, 'total': 0}, 'timeseries': {}, 'type': {'display_decimals': 0, 'value_type': 'count'}}, @@ -201,7 +191,7 @@ def test_get_metric_filters(test_client, complete_admin_auth_token, external_res base_all_zero_decimals = {'data': {'transfer_stats': {'active_filters': [], - 'active_group_by': 'account_type', + 'active_group_by': 'recipient,account_type', 'active_users': {'aggregate': {'percent_change': None, 'total': 0}, 'timeseries': {}, 'type': {'display_decimals': 0, 'value_type': 'count'}}, @@ -233,7 +223,7 @@ def test_get_metric_filters(test_client, complete_admin_auth_token, external_res base_transfer = {'data': {'transfer_stats': {'active_filters': [], - 'active_group_by': 'account_type', + 'active_group_by': 'recipient,account_type', 'all_payments_volume': {'aggregate': {'percent_change': None, 'total': 0}, 'timeseries': {}, 'type': {'currency_name': 'AUD Reserve Token', 'currency_symbol': 'AUD', 'display_decimals': 2, 'value_type': 'currency'}}, 'daily_transaction_count': @@ -264,7 +254,7 @@ def test_get_zero_metrics(test_client, complete_admin_auth_token, external_reser create_organisation.token.display_decimals = display_decimals def get_metrics(metric_type): return test_client.get( - f'/api/v1/metrics/?metric_type={metric_type}&disable_cache=True&org={create_organisation.id}&group_by=account_type', + f'/api/v1/metrics/?metric_type={metric_type}&disable_cache=True&org={create_organisation.id}&group_by=recipient,account_type', headers=dict( Authorization=complete_admin_auth_token, Accept='application/json' @@ -288,21 +278,23 @@ def get_metrics(metric_type): elif metric_type == 'user': assert response.json == base_participant - @pytest.mark.parametrize("metric_type, params, status_code, requested_metric, group_by, output_file", [ - ("all", None, 200, None, 'account_type', 'all_by_account_type.json'), - ("all", None, 200, None ,'location', 'all_by_location.json'), + ("all", None, 200, None, 'sender,account_type', 'all_by_account_type.json'), + ("all", None, 200, None, 'colour,sender', 'all_by_sender_colour.json'), + ("all", None, 200, None, 'colour,recipient', 'all_by_recipient_colour.json'), + ("all", None, 200, None ,'sender,location', 'all_by_sender_location.json'), + ("all", None, 200, None ,'recipient,location', 'all_by_recipient_location.json'), ("all", None, 200, None, 'ungrouped', 'all_ungrouped.json'), - ("all", "rounded_account_balance,sender(GT)(2)", 200, None, 'account_type', 'all_by_account_type_filtered_by_sender.json'), - ("all", "rounded_account_balance,recipient(GT)(2)", 200, None, 'account_type', 'all_by_account_type_filtered_by_recipient.json'), + ("all", "rounded_account_balance,sender(GT)(2)", 200, None, 'sender,account_type', 'all_by_account_type_filtered_by_sender.json'), + ("all", "rounded_account_balance,recipient(GT)(2)", 200, None, 'sender,account_type', 'all_by_account_type_filtered_by_recipient.json'), ("credit_transfer", None, 200, None, 'transfer_usage', 'credit_transfer_by_transfer_usage.json'), - ("user", None, 200, None, 'account_type', 'user_by_account_type.json'), + ("user", None, 200, None, 'sender,account_type', 'user_by_account_type.json'), ("credit_transfer", None, 200, None, 'transfer_type', 'credit_transfer_by_transfer_type.json'), - ("all", None, 200, 'active_users', 'account_type', 'requested_metric_active_users.json'), + ("all", None, 200, 'active_users', 'sender,account_type', 'requested_metric_active_users.json'), ("all", None, 500, None, 'transfer_usage', ''), # 500 because can't group all by transfer_usage ("user", None, 500, None, 'transfer_usage', ''), # 500 because can't group user by transfer_usage - ("user", 'transfer_amount(LT)(50)', 500, None, 'account_type', ''), # 500 because can't filter user by transfer_amount - ("all", 'transfer_amount(LT)(50)', 500, None, 'account_type', ''), # 500 because can't filter all by transfer_amount + ("user", 'transfer_amount(LT)(50)', 500, None, 'sender,account_type', ''), # 500 because can't filter user by transfer_amount + ("all", 'transfer_amount(LT)(50)', 500, None, 'sender,account_type', ''), # 500 because can't filter all by transfer_amount ("notarealmetrictype", None, 500, None, 'transfer_usage', ''), ]) def test_get_summed_metrics( @@ -344,3 +336,41 @@ def get_metrics(metric_type): sorted_desired_stats = ts_sort(desired_output[do]['timeseries'][timeseries_category]) for idx in range(len(returned_stats[do]['timeseries'][timeseries_category])): assert sorted_returned_stats[idx]['value'] == sorted_desired_stats[idx]['value'] + +@pytest.mark.parametrize("metric_type, status_code, hide_sender", [ + ("user", 200, False), + ("all", 200, False), + ("credit_transfer", 200, False), + ("notarealmetrictype", 500, False), + ("all", 200, True), +]) +def test_get_metric_filters(test_client, complete_admin_auth_token, external_reserve_token, + metric_type, status_code, generate_timeseries_metrics, hide_sender): + if hide_sender: + db.session.query(CustomAttribute).first().group_visibility = MetricsVisibility.RECIPIENT + + db.session.commit() + # Tests getting list of availible metrics filters from the /api/v1/metrics/filters endpoint + response = test_client.get( + f'/api/v1/metrics/filters/?metric_type={metric_type}', + headers=dict( + Authorization=complete_admin_auth_token, + Accept='application/json' + ), + ) + assert response.status_code == status_code + filters = Filters() + if status_code == 200: + if metric_type == 'user': + assert response.json['data']['filters'] == filters.USER_FILTERS + elif metric_type == 'transfer': + assert response.json['data']['filters'] == filters.TRANSFER_FILTERS + assert response.json['data']['groups']['colour,recipient']['name'] == 'Colour' + assert response.json['data']['groups']['colour,recipient']['sender_or_recipient'] == 'recipient' + assert response.json['data']['groups']['colour,recipient']['table'] == 'custom_attribute_user_storage' + if not hide_sender: + assert response.json['data']['groups']['colour,sender']['name'] == 'Colour' + assert response.json['data']['groups']['colour,sender']['sender_or_recipient'] == 'sender' + assert response.json['data']['groups']['colour,sender']['table'] == 'custom_attribute_user_storage' + else: + assert 'colour,sender' not in response.json['data']['groups'] diff --git a/config.py b/config.py index bc7d8eb38..ca9f37426 100755 --- a/config.py +++ b/config.py @@ -5,7 +5,7 @@ logging.basicConfig(level=env_loglevel) logg = logging.getLogger(__name__) -VERSION = '1.7.31' # Remember to bump this in every PR +VERSION = '1.7.32' # Remember to bump this in every PR logg.info('Loading configs at UTC {}'.format(datetime.datetime.utcnow())) From 4bed1002275bbe9dd6a146da9af00dd5b80584b4 Mon Sep 17 00:00:00 2001 From: Tristan Cole Date: Mon, 7 Dec 2020 09:50:29 +1100 Subject: [PATCH 6/6] feat: small improvements to accessibility across the web app (#562) * a11y * add label's to img * document title * updating labels * updating how we handle title * update PR template * adding aria labels to forms * improving colour contrast across the app * improving colour contrast across the app * bump * bump --- .github/pull_request_template.md | 2 +- app/client/components/AsyncButton.jsx | 3 +++ app/client/components/NoDataMessage.jsx | 5 +++- .../components/adminUser/InviteForm.jsx | 3 +++ .../components/adminUser/adminUserList.jsx | 6 ++++- app/client/components/auth/TFAForm.jsx | 5 ++++ app/client/components/auth/TFAValidator.jsx | 5 +++- app/client/components/auth/authModule.jsx | 14 +++++++--- app/client/components/auth/loginForm.jsx | 5 +++- app/client/components/auth/registerForm.jsx | 3 +++ .../components/auth/requestResetEmailForm.jsx | 2 ++ .../components/auth/resetPasswordForm.jsx | 5 +++- .../creditTransfer/creditTransferList.jsx | 1 + .../components/dashboard/MetricsCard.jsx | 3 +++ .../dashboard/beneficiaryLiveFeed.jsx | 16 ++++++++--- .../datasetList/compareListsView.jsx | 2 -- .../components/datasetList/datasetFeed.jsx | 2 -- .../filterModule/SearchBoxWithFilter.jsx | 4 +++ app/client/components/filterModule/filter.jsx | 1 + .../components/funding/wyreBankFunding.jsx | 1 + .../components/management/exportManager.jsx | 1 + .../management/newTransferManager.jsx | 6 +++++ app/client/components/navBar/page.tsx | 14 +++++----- app/client/components/pages/authPage.jsx | 5 +++- .../pages/businessVerificationPage.jsx | 1 + .../pages/transferAccountListPage.jsx | 1 + app/client/components/styledElements.js | 4 +-- app/client/components/theme.js | 2 +- .../transferAccount/transferAccountList.jsx | 16 ++++++++++- .../transferAccountManager.jsx | 9 ++++++- .../components/uploader/uploadButton.jsx | 7 ++++- .../components/uploader/uploadedTable.jsx | 9 +++++-- .../verification/businessBankDetails.jsx | 2 ++ .../verification/businessBankDocuments.jsx | 7 ++++- .../verification/businessBankLocation.jsx | 2 ++ .../verification/businessDetails.jsx | 1 + .../verification/businessDocuments.jsx | 7 ++++- app/client/nav.jsx | 27 ++++++++++++++++--- app/package.json | 2 +- app/server/static/css/styles.css | 4 +-- 40 files changed, 175 insertions(+), 40 deletions(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 5b929f393..4ddbf1512 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -6,5 +6,5 @@ A clear and concise description of the features implemented in the PR of bugs fi - [ ] Link relevant [trello card](https://trello.com/b/PA2LhlOh/sempo-dev) or [related issue](https://github.com/teamsempo/SempoBlockchain/issues) to the pull request - [ ] Request review from relevant @person or team -- [ ] QA your pull request. This includes running the app, login, testing changes etc. +- [ ] QA your pull request. This includes running the app, login, testing changes, checking [accessibility](https://www.a11yproject.com/checklist/) etc. - [ ] Bump [backend](https://github.com/teamsempo/SempoBlockchain/blob/e3eb0f480e86d5a8ef2c1814127b70ff018671c4/config.py#L7) and/or [frontend](https://github.com/teamsempo/SempoBlockchain/blob/e3eb0f480e86d5a8ef2c1814127b70ff018671c4/app/package.json#L3) versions following Semver diff --git a/app/client/components/AsyncButton.jsx b/app/client/components/AsyncButton.jsx index 52fb4e37c..d52f18c1b 100644 --- a/app/client/components/AsyncButton.jsx +++ b/app/client/components/AsyncButton.jsx @@ -16,6 +16,7 @@ export default class AsyncButton extends React.Component { alignItems: "center", justifyContent: "center" }} + label={this.props.label} >
@@ -36,6 +37,7 @@ export default class AsyncButton extends React.Component { alignItems: "center", justifyContent: "center" }} + label={this.props.label} > Success @@ -52,6 +54,7 @@ export default class AsyncButton extends React.Component { alignItems: "center", justifyContent: "center" }} + label={this.props.label} > {this.props.buttonText} diff --git a/app/client/components/NoDataMessage.jsx b/app/client/components/NoDataMessage.jsx index 887c95d1c..1e41eb215 100644 --- a/app/client/components/NoDataMessage.jsx +++ b/app/client/components/NoDataMessage.jsx @@ -12,7 +12,10 @@ export default class NoDataMessage extends React.Component { - +

There is no data available. Please upload a spreadsheet.

} diff --git a/app/client/components/adminUser/InviteForm.jsx b/app/client/components/adminUser/InviteForm.jsx index 7d8808604..5c182c7a0 100644 --- a/app/client/components/adminUser/InviteForm.jsx +++ b/app/client/components/adminUser/InviteForm.jsx @@ -95,6 +95,7 @@ const InviteForm = function(props) { id="EmailField" onKeyUp={props.onEmailFieldKeyPress} placeholder="Email" + aria-label="Email" /> @@ -107,6 +108,7 @@ const InviteForm = function(props) { value={tier} checked={props.tier === tier} onChange={props.handleToggle} + aria-label={`Tier ${tier}`} /> @@ -122,6 +124,7 @@ const InviteForm = function(props) { isLoading={props.isLoggingIn} buttonStyle={{ width: "calc(100% - 1em)", display: "flex" }} buttonText={Invite} + label={"Invite"} />
); diff --git a/app/client/components/adminUser/adminUserList.jsx b/app/client/components/adminUser/adminUserList.jsx index 16f8bd95d..8b048aa12 100644 --- a/app/client/components/adminUser/adminUserList.jsx +++ b/app/client/components/adminUser/adminUserList.jsx @@ -209,7 +209,10 @@ class AdminUserList extends React.Component { }) } > - + @@ -263,6 +266,7 @@ class AdminUserList extends React.Component { lineHeight: "25px", height: "25px" }} + label={"Add New Admin"} > + New Admin diff --git a/app/client/components/auth/TFAForm.jsx b/app/client/components/auth/TFAForm.jsx index 8a4b4ca55..97fe15529 100755 --- a/app/client/components/auth/TFAForm.jsx +++ b/app/client/components/auth/TFAForm.jsx @@ -30,6 +30,9 @@ export default class TFAForm extends React.Component { onClick={() => this.handleNextBack()} buttonStyle={{ width: "calc(100% - 1em)", display: "flex" }} buttonText={Next} + label={ + "Go to the next page once two factor authentication QR code is scanned" + } />
); @@ -64,6 +67,7 @@ const TFAQr = props => ( Android @@ -71,6 +75,7 @@ const TFAQr = props => ( iPhone diff --git a/app/client/components/auth/TFAValidator.jsx b/app/client/components/auth/TFAValidator.jsx index c6384f39d..50ba41159 100755 --- a/app/client/components/auth/TFAValidator.jsx +++ b/app/client/components/auth/TFAValidator.jsx @@ -90,7 +90,8 @@ export class TFAValidator extends React.Component { onChange={e => this.onCodeKeyPress(e)} onKeyUp={e => this.onKeyup(e)} style={{ letterSpacing: "0.2em" }} - placeholder="Your Code" + placeholder="Your TFA Code" + aria-label="Your TFA Code" /> @@ -108,6 +110,7 @@ export class TFAValidator extends React.Component { isLoading={this.props.validateState.isRequesting} buttonStyle={{ width: "calc(100% - 1em)", display: "flex" }} buttonText={Verify} + label={"Verify my application"} />
diff --git a/app/client/components/auth/authModule.jsx b/app/client/components/auth/authModule.jsx index a5981fb26..eff62359f 100755 --- a/app/client/components/auth/authModule.jsx +++ b/app/client/components/auth/authModule.jsx @@ -44,16 +44,24 @@ class authModuleContainer extends React.Component { if (this.props.loggedIn) { var button = (
- Logout Admin + + Logout Admin +
); } else { var button = (
- this.setActiveForm("LOGIN")}> + this.setActiveForm("LOGIN")} + label={"Go to Login Page"} + > Login - this.setActiveForm("REGISTER")}> + this.setActiveForm("REGISTER")} + label={"Go to Register Page"} + > Register
diff --git a/app/client/components/auth/loginForm.jsx b/app/client/components/auth/loginForm.jsx index e0ac4c01f..1b822c2da 100755 --- a/app/client/components/auth/loginForm.jsx +++ b/app/client/components/auth/loginForm.jsx @@ -6,7 +6,7 @@ import { Link } from "react-router-dom"; import AsyncButton from "./../AsyncButton.jsx"; import { LoginAction } from "../../reducers/auth/actions"; -import { Input, StyledButton, ErrorMessage } from "./../styledElements"; +import { Input, ErrorMessage } from "./../styledElements"; import { Footer, FooterLink, FooterText } from "../pages/authPage.jsx"; import TFAForm from "./TFAForm.jsx"; @@ -124,6 +124,7 @@ const LoginForm = function(props) { id="UserField" onKeyUp={props.onUserFieldKeyPress} placeholder="Email" + aria-label="Email" /> @@ -143,6 +145,7 @@ const LoginForm = function(props) { isLoading={props.isLoggingIn} buttonStyle={{ width: "calc(100% - 1em)", display: "flex" }} buttonText={LOGIN} + label={"Login"} />
diff --git a/app/client/components/auth/registerForm.jsx b/app/client/components/auth/registerForm.jsx index a2a56e40c..d0ad683a3 100755 --- a/app/client/components/auth/registerForm.jsx +++ b/app/client/components/auth/registerForm.jsx @@ -212,6 +212,7 @@ const RegisterForm = function(props) { placeholder="Email" disabled={props.state.invite ? "disabled" : ""} style={props.state.invite ? { display: "none" } : null} + aria-label="Email" /> {error_message} @@ -236,6 +238,7 @@ const RegisterForm = function(props) { isLoading={props.isRegistering} buttonStyle={{ width: "calc(100% - 1em)", display: "flex" }} buttonText={REGISTER} + label={"Register for Sempo Dashboard"} />
); diff --git a/app/client/components/auth/requestResetEmailForm.jsx b/app/client/components/auth/requestResetEmailForm.jsx index deda2f5d7..08601b1f8 100755 --- a/app/client/components/auth/requestResetEmailForm.jsx +++ b/app/client/components/auth/requestResetEmailForm.jsx @@ -83,6 +83,7 @@ const RequestResetEmailForm = function(props) { id="UserField" onKeyUp={props.onEmailFieldKeyPress} placeholder="Email" + aria-label="Email" /> Reset Password} + label={"Reset Password on Sempo Dashboard"} /> {error_message}
diff --git a/app/client/components/auth/resetPasswordForm.jsx b/app/client/components/auth/resetPasswordForm.jsx index 75f61d0d7..7896813e1 100755 --- a/app/client/components/auth/resetPasswordForm.jsx +++ b/app/client/components/auth/resetPasswordForm.jsx @@ -8,7 +8,7 @@ import { ResetPasswordAction } from "../../reducers/auth/actions"; import AsyncButton from "./../AsyncButton.jsx"; -import { Input, ErrorMessage, StyledButton } from "./../styledElements"; +import { Input, ErrorMessage } from "./../styledElements"; import { parseQuery } from "../../utils"; @@ -158,6 +158,7 @@ const ResetPasswordForm = function(props) { type="password" onKeyUp={props.onOldPasswordFieldKeyPress} placeholder="Password" + aria-label="Password" /> ); @@ -180,6 +181,7 @@ const ResetPasswordForm = function(props) { type="password" onKeyUp={props.onReenterPasswordFieldKeyPress} placeholder="Retype Password" + aria-label="Retype Password" /> {error_message} Change Password} + label={"Change Password"} /> ); diff --git a/app/client/components/creditTransfer/creditTransferList.jsx b/app/client/components/creditTransfer/creditTransferList.jsx index 6b01f369d..d85006760 100644 --- a/app/client/components/creditTransfer/creditTransferList.jsx +++ b/app/client/components/creditTransfer/creditTransferList.jsx @@ -311,6 +311,7 @@ class CreditTransferList extends React.Component { }} isLoading={this.props.isApproving} buttonText={NEXT} + label={"Next action"} /> { - + @@ -234,7 +237,10 @@ class BeneficiaryLiveFeed extends React.Component { } else if (transfer.transfer_type === "PAYMENT") { return ( - + - + @@ -299,6 +308,7 @@ class BeneficiaryLiveFeed extends React.Component { diff --git a/app/client/components/datasetList/compareListsView.jsx b/app/client/components/datasetList/compareListsView.jsx index 592a616ea..5f9af7743 100644 --- a/app/client/components/datasetList/compareListsView.jsx +++ b/app/client/components/datasetList/compareListsView.jsx @@ -4,8 +4,6 @@ import styled from "styled-components"; import { SpreadsheetAction } from "../../reducers/spreadsheet/actions"; -import { Input, StyledButton, ErrorMessage } from "./../styledElements"; - import DatasetFeed from "./datasetFeed.jsx"; import LoadingSpinner from "../loadingSpinner.jsx"; diff --git a/app/client/components/datasetList/datasetFeed.jsx b/app/client/components/datasetList/datasetFeed.jsx index 7121d9711..8b0ebb3b8 100644 --- a/app/client/components/datasetList/datasetFeed.jsx +++ b/app/client/components/datasetList/datasetFeed.jsx @@ -2,8 +2,6 @@ import React from "react"; import { connect } from "react-redux"; import styled from "styled-components"; -import { Input, StyledButton, ErrorMessage } from "./../styledElements"; - const mapStateToProps = state => { return {}; }; diff --git a/app/client/components/filterModule/SearchBoxWithFilter.jsx b/app/client/components/filterModule/SearchBoxWithFilter.jsx index 631b4fe6b..ce4feccc3 100644 --- a/app/client/components/filterModule/SearchBoxWithFilter.jsx +++ b/app/client/components/filterModule/SearchBoxWithFilter.jsx @@ -279,6 +279,7 @@ class SearchBoxWithFilter extends React.Component { )} {saveFilterDropdown ? "Cancel" : "Save Filter"} @@ -294,6 +295,7 @@ class SearchBoxWithFilter extends React.Component { value={this.state.filterName} placeholder="Filter name..." onChange={this.handleChange} + aria-label="Filter name" /> View Saved Filters @@ -368,6 +371,7 @@ class SearchBoxWithFilter extends React.Component { value={phrase} placeholder="Search..." onChange={this.handleChange} + aria-label="Search" /> diff --git a/app/client/components/filterModule/filter.jsx b/app/client/components/filterModule/filter.jsx index e37ec7588..666b37e36 100644 --- a/app/client/components/filterModule/filter.jsx +++ b/app/client/components/filterModule/filter.jsx @@ -420,6 +420,7 @@ class Filter extends React.Component { ? "/static/media/closePrimary.svg" : "/static/media/closeAlt.svg" } + alt={"Remove filter " + attributeProperties.name} /> ); diff --git a/app/client/components/funding/wyreBankFunding.jsx b/app/client/components/funding/wyreBankFunding.jsx index e6cabbe98..812b766f4 100644 --- a/app/client/components/funding/wyreBankFunding.jsx +++ b/app/client/components/funding/wyreBankFunding.jsx @@ -206,6 +206,7 @@ class WyreBankFunding extends React.Component { } buttonText={FUND ACCOUNT} isLoading={isLoading} + label={"Fund Account"} /> diff --git a/app/client/components/management/exportManager.jsx b/app/client/components/management/exportManager.jsx index 27f732c68..51dd79114 100644 --- a/app/client/components/management/exportManager.jsx +++ b/app/client/components/management/exportManager.jsx @@ -163,6 +163,7 @@ class ExportManager extends React.Component { isLoading={this.props.export.isRequesting} buttonStyle={{ display: "flex" }} buttonText={Export} + label={"Export"} /> {this.props.export.error} {this.props.activeOrganisation.token.symbol} {convertedBitcoin} @@ -231,6 +232,11 @@ class NewTransferManager extends React.Component { {this.state.create_transfer_type} } + label={ + (this.state.create_transfer_type === "BALANCE" + ? "SET " + : "CREATE ") + this.state.create_transfer_type + } /> diff --git a/app/client/components/navBar/page.tsx b/app/client/components/navBar/page.tsx index 6fa2e817b..e7af691ab 100644 --- a/app/client/components/navBar/page.tsx +++ b/app/client/components/navBar/page.tsx @@ -9,8 +9,7 @@ import IntercomSetup from "../intercom/IntercomSetup"; import ErrorBoundary from "../ErrorBoundary"; import LoadingSpinner from "../loadingSpinner"; -const { Header, Content, Footer } = Layout; -const { Title } = Typography; +const { Content, Footer } = Layout; interface OuterProps { noNav?: boolean; @@ -42,6 +41,12 @@ const Page: React.FunctionComponent = props => { } }, []); + React.useEffect(() => { + if (title) { + document.title = `Sempo | ${title}`; + } + }, [title]); + let onCollapse = (collapsed: boolean) => { setCollapsed(collapsed); localStorage.setItem("sideBarCollapsed", collapsed.toString()); @@ -91,11 +96,6 @@ const Page: React.FunctionComponent = props => { : { marginLeft: "200px" } } > - {title ? ( -
- {title} -
- ) : null}
- +
Get Started} onClick={this.props.nextStep} + label={"Get Started"} /> diff --git a/app/client/components/pages/transferAccountListPage.jsx b/app/client/components/pages/transferAccountListPage.jsx index 579e096bd..0f2c0d26c 100644 --- a/app/client/components/pages/transferAccountListPage.jsx +++ b/app/client/components/pages/transferAccountListPage.jsx @@ -90,6 +90,7 @@ class TransferAccountListPage extends React.Component { "/create?type=" + browserHistory.location.pathname.slice(1) ) } + label={"Add Single User"} > Add Single User diff --git a/app/client/components/styledElements.js b/app/client/components/styledElements.js index cd2581f46..b740043b3 100755 --- a/app/client/components/styledElements.js +++ b/app/client/components/styledElements.js @@ -137,7 +137,7 @@ export const ErrorMessage = styled.div` margin: 0.5em; `; -export const ModuleHeader = styled.h4` +export const ModuleHeader = styled.h1` text-transform: uppercase; position: relative; margin: 0; @@ -147,7 +147,7 @@ export const ModuleHeader = styled.h4` width: auto; background-color: #fff; font-size: 15px; - color: #6a7680; + color: #4e575e; font-weight: 600; letter-spacing: 1px; `; diff --git a/app/client/components/theme.js b/app/client/components/theme.js index ade7622f9..5500ef590 100644 --- a/app/client/components/theme.js +++ b/app/client/components/theme.js @@ -2,7 +2,7 @@ export const LightTheme = { background: "#f7fafc", backgroundColor: "#fcfeff", - color: "#6a7680" + color: "#4e575e" }; export const DefaultTheme = { diff --git a/app/client/components/transferAccount/transferAccountList.jsx b/app/client/components/transferAccount/transferAccountList.jsx index 1d73bb688..2d901f9cc 100644 --- a/app/client/components/transferAccount/transferAccountList.jsx +++ b/app/client/components/transferAccount/transferAccountList.jsx @@ -114,6 +114,7 @@ class TransferAccountList extends React.Component { type="checkbox" checked={checked} onChange={() => this.toggleSelectedTransferAccount(id)} + aria-label={`Select Account ${id}`} /> ); } @@ -188,16 +189,22 @@ class TransferAccountList extends React.Component { _customIcon(transferAccount) { let url = "/static/media/user.svg"; + let alt = "User "; if (transferAccount.is_beneficiary) { url = "/static/media/user.svg"; + alt = "User "; } else if (transferAccount.is_vendor) { url = "/static/media/store.svg"; + alt = "Vendor "; } else if (transferAccount.is_groupaccount) { url = "/static/media/groupaccount.svg"; + alt = "Group Account "; } else if (transferAccount.is_tokenagent) { url = "/static/media/tokenagent.svg"; + alt = "Token Agent "; } - return ; + alt = alt + this._customName(transferAccount); + return ; } render() { @@ -248,6 +255,7 @@ class TransferAccountList extends React.Component { lineHeight: "25px", height: "25px" }} + label={"New Transfer"} > NEW TRANSFER @@ -260,6 +268,7 @@ class TransferAccountList extends React.Component { lineHeight: "25px", height: "25px" }} + label={"Approve"} > APPROVE @@ -272,6 +281,7 @@ class TransferAccountList extends React.Component { lineHeight: "25px", height: "25px" }} + label={"Unapprove"} > UNAPPROVE @@ -285,6 +295,7 @@ class TransferAccountList extends React.Component { lineHeight: "25px", height: "25px" }} + label={"Export"} > Export @@ -333,6 +344,7 @@ class TransferAccountList extends React.Component { lineHeight: "25px", height: "25px" }} + label={"Add New"} > + Add New @@ -346,6 +358,7 @@ class TransferAccountList extends React.Component { lineHeight: "25px", height: "25px" }} + label={"Export"} > Export @@ -462,6 +475,7 @@ class TransferAccountList extends React.Component { onChange={() => this.checkAllTransferAccounts(filteredData) } + aria-label={"Select all accounts"} /> ), accessor: "id", diff --git a/app/client/components/transferAccount/transferAccountManager.jsx b/app/client/components/transferAccount/transferAccountManager.jsx index d55d24846..9c2fb4076 100644 --- a/app/client/components/transferAccount/transferAccountManager.jsx +++ b/app/client/components/transferAccount/transferAccountManager.jsx @@ -165,6 +165,7 @@ class TransferAccountManager extends React.Component { } = this.state; let accountTypeName; let icon; + let alt; if (this.state.newTransfer) { var newTransfer = ( @@ -202,22 +203,26 @@ class TransferAccountManager extends React.Component { accountTypeName = TransferAccountTypes.BENEFICIARY || window.BENEFICIARY_TERM; icon = "/static/media/user.svg"; + alt = "User Icon"; } else if (is_vendor) { accountTypeName = TransferAccountTypes.VENDOR; icon = "/static/media/store.svg"; + alt = "Vendor Icon"; } else if (is_groupaccount) { accountTypeName = TransferAccountTypes.GROUP_ACCOUNT; icon = "/static/media/groupaccount.svg"; + alt = "Group Account Icon"; } else if (is_tokenagent) { accountTypeName = TransferAccountTypes.TOKEN_AGENT; icon = "/static/media/tokenagent.svg"; + alt = "Token Agent Icon"; } var summaryBox = ( - +

{accountTypeName}

@@ -275,6 +280,7 @@ class TransferAccountManager extends React.Component { lineHeight: "25px", height: "25px" }} + label={"New Transfer"} > NEW TRANSFER @@ -291,6 +297,7 @@ class TransferAccountManager extends React.Component { this.props.transferAccounts.editStatus.isRequesting } buttonText={SAVE} + label={"Save"} /> diff --git a/app/client/components/uploader/uploadButton.jsx b/app/client/components/uploader/uploadButton.jsx index 95c174eaa..2562a2134 100644 --- a/app/client/components/uploader/uploadButton.jsx +++ b/app/client/components/uploader/uploadButton.jsx @@ -46,7 +46,11 @@ class UploadButton extends React.Component { return ( {this.props.uploadButtonText} - this.handleFileChange(e)} /> + this.handleFileChange(e)} + aria-label="Upload File" + /> ); } @@ -59,6 +63,7 @@ class UploadButton extends React.Component { this.handleFileChange(e)} + aria-label="Upload File" /> diff --git a/app/client/components/uploader/uploadedTable.jsx b/app/client/components/uploader/uploadedTable.jsx index ea814af4c..a54d31a91 100644 --- a/app/client/components/uploader/uploadedTable.jsx +++ b/app/client/components/uploader/uploadedTable.jsx @@ -408,6 +408,8 @@ class uploadedTable extends React.Component { ); } + let nextText = this.currentStepNormalisedIndex() === 2 ? "Save" : "Next"; + return ( Prev @@ -444,8 +447,9 @@ class uploadedTable extends React.Component { ? { opacity: 0, pointerEvents: "None" } : {} } + label={nextText} > - {this.currentStepNormalisedIndex() === 2 ? "Save" : "Next"} + {nextText} @@ -504,7 +508,7 @@ const CustomColumnFields = function(props) { value={props.customAttribute} onCustomAttributeKeyPress={e => props.onCustomAttributeKeyPress(e)} /> - props.handleAddClick()}> + props.handleAddClick()} label={"Add"}> {" "} Add{" "} @@ -537,6 +541,7 @@ class CustomInput extends React.Component { innerRef={input => { this.nameInput = input; }} + aria-label={this.props.value} /> ); } diff --git a/app/client/components/verification/businessBankDetails.jsx b/app/client/components/verification/businessBankDetails.jsx index abc77dacd..28364456f 100644 --- a/app/client/components/verification/businessBankDetails.jsx +++ b/app/client/components/verification/businessBankDetails.jsx @@ -220,9 +220,11 @@ class BusinessBankDetails extends React.Component { Back} onClick={this.props.backStep} + label={"Back"} /> Next} + label={"Next"} onClick={this.isValidated} isLoading={ this.props.editBankAccountStatus.isRequesting || diff --git a/app/client/components/verification/businessBankDocuments.jsx b/app/client/components/verification/businessBankDocuments.jsx index 5d3f8b1d1..8de71e0f4 100644 --- a/app/client/components/verification/businessBankDocuments.jsx +++ b/app/client/components/verification/businessBankDocuments.jsx @@ -93,7 +93,10 @@ class BusinessBankDocuments extends React.Component { .map((document, idx) => { return ( - +
{document.user_filename} @@ -214,11 +217,13 @@ class BusinessBankDocuments extends React.Component { Back} onClick={this.props.backStep} + label={"Back"} /> COMPLETE} onClick={this.isValidated} isLoading={this.props.editStatus.isRequesting} + label={"Complete"} />
diff --git a/app/client/components/verification/businessBankLocation.jsx b/app/client/components/verification/businessBankLocation.jsx index bc19982b8..ba5a5ac51 100644 --- a/app/client/components/verification/businessBankLocation.jsx +++ b/app/client/components/verification/businessBankLocation.jsx @@ -187,10 +187,12 @@ class BusinessBankLocation extends React.Component { Back} onClick={this.props.backStep} + label={"Back"} /> Next} onClick={this.isValidated} + label={"Next"} /> diff --git a/app/client/components/verification/businessDetails.jsx b/app/client/components/verification/businessDetails.jsx index 657841450..3bc3740d4 100644 --- a/app/client/components/verification/businessDetails.jsx +++ b/app/client/components/verification/businessDetails.jsx @@ -560,6 +560,7 @@ class BusinessDetails extends React.Component { onClick={this.isValidated} isLoading={this.props.editStatus.isRequesting} buttonStyle={{ display: "flex" }} + label={"Next"} /> diff --git a/app/client/components/verification/businessDocuments.jsx b/app/client/components/verification/businessDocuments.jsx index 46ff3a1f6..404cb471f 100644 --- a/app/client/components/verification/businessDocuments.jsx +++ b/app/client/components/verification/businessDocuments.jsx @@ -94,7 +94,10 @@ class BusinessDocuments extends React.Component { .map((document, idx) => { return ( - +
{document.user_filename} @@ -241,6 +244,7 @@ class BusinessDocuments extends React.Component { Back} onClick={this.props.backStep} + label={"Back"} />
diff --git a/app/client/nav.jsx b/app/client/nav.jsx index ff849fcca..2db92c19c 100644 --- a/app/client/nav.jsx +++ b/app/client/nav.jsx @@ -79,6 +79,7 @@ class Nav extends React.Component { isLoggedIn={isLoggedIn} isReAuthing={isReAuthing} isAntDesign={true} + title={"Dashboard"} /> {/* PUBLIC PAGES */} - - - + + +
diff --git a/app/package.json b/app/package.json index 4efbaeb55..b864c2d0d 100644 --- a/app/package.json +++ b/app/package.json @@ -1,6 +1,6 @@ { "name": "SempoBlockchain", - "version": "1.2.49", + "version": "1.2.50", "description": "Sempo blockchain web app using Python-Flask and React", "main": "index.js", "homepage": "./", diff --git a/app/server/static/css/styles.css b/app/server/static/css/styles.css index 6d127a0e7..3668a2b4f 100644 --- a/app/server/static/css/styles.css +++ b/app/server/static/css/styles.css @@ -1,7 +1,7 @@ body { font-family: "Open Sans", sans-serif; font-weight: 200; - color: #555; + color: #4e575e; background-color: #f7f7f7; margin: 0; overflow-y: scroll; @@ -127,7 +127,7 @@ body { padding: 5px 1em !important; text-align: left; font-size: 15px; - color: #6a7680; + color: #4e575e; font-weight: 600; letter-spacing: 1px; text-transform: uppercase;