Skip to content
GitLab
Projects
Groups
Snippets
/
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
Menu
Open sidebar
antiloop222
kresus
Commits
012f8970
Commit
012f8970
authored
Aug 18, 2019
by
Nicolas Frandeboeuf
Committed by
Benjamin Bouvier
Aug 18, 2019
Browse files
Implement demo mode; fixes #743
parent
48f755e5
Changes
24
Hide whitespace changes
Inline
Side-by-side
client/components/header/demo-button.js
0 → 100644
View file @
012f8970
import
React
from
'
react
'
;
import
{
connect
}
from
'
react-redux
'
;
import
{
get
,
actions
}
from
'
../../store
'
;
import
{
translate
as
$t
}
from
'
../../helpers
'
;
import
{
MODAL_SLUG
}
from
'
./disable-demo-modal
'
;
const
DemoButton
=
connect
(
state
=>
{
return
{
isDemoMode
:
get
.
isDemoMode
(
state
)
};
},
dispatch
=>
{
return
{
handleClick
()
{
actions
.
showModal
(
dispatch
,
MODAL_SLUG
);
}
};
}
)(
props
=>
{
if
(
!
props
.
isDemoMode
)
{
return
null
;
}
return
(
<
button
type
=
"
reset
"
className
=
"
btn warning disable-demo-mode
"
onClick
=
{
props
.
handleClick
}
>
{
$t
(
'
client.demo.disable
'
)}
<
/button
>
);
});
export
default
DemoButton
;
client/components/header/disable-demo-modal.js
0 → 100644
View file @
012f8970
import
React
from
'
react
'
;
import
{
connect
}
from
'
react-redux
'
;
import
{
actions
}
from
'
../../store
'
;
import
{
translate
as
$t
}
from
'
../../helpers
'
;
import
{
registerModal
}
from
'
../ui/modal
'
;
import
ModalContent
from
'
../ui/modal/content
'
;
import
CancelAndWarn
from
'
../ui/modal/cancel-and-warn-buttons
'
;
export
const
MODAL_SLUG
=
'
disable-demo-params
'
;
const
DisableDemoModal
=
connect
(
null
,
dispatch
=>
{
return
{
handleDisable
()
{
actions
.
disableDemoMode
(
dispatch
);
actions
.
hideModal
(
dispatch
);
}
};
}
)(
props
=>
{
const
body
=
<
p
>
{
$t
(
'
client.demo.disable_warning
'
)}
<
/p>
;
const
footer
=
(
<
CancelAndWarn
onConfirm
=
{
props
.
handleDisable
}
warningLabel
=
{
$t
(
'
client.demo.disable
'
)}
/
>
);
return
<
ModalContent
title
=
{
$t
(
'
client.demo.title
'
)}
body
=
{
body
}
footer
=
{
footer
}
/>
;
});
registerModal
(
MODAL_SLUG
,
()
=>
<
DisableDemoModal
/>
);
client/components/init/account-wizard.js
View file @
012f8970
...
...
@@ -8,6 +8,7 @@ import NewBankForm from '../settings/bank-accesses/form';
import
ImportModule
from
'
../settings/backup/import
'
;
import
LocaleSelector
from
'
../settings/customization/locale-selector
'
;
import
Demo
from
'
./demo
'
;
import
WeboobInstallReadme
from
'
./weboob-readme
'
;
const
BASE_PATH
=
URL
.
initialize
.
url
();
...
...
@@ -16,7 +17,6 @@ const IMPORT_PATH = URL.initialize.url('import');
const
DEMO_PATH
=
URL
.
initialize
.
url
(
'
demo-mode
'
);
export
default
class
AccountWizard
extends
React
.
Component
{
// TODO: Demo mode should be implemented
renderMenu
=
()
=>
(
<
div
>
<
header
>
...
...
@@ -58,7 +58,7 @@ export default class AccountWizard extends React.Component {
<
/p
>
<
/Link
>
<
Link
to
=
{
DEMO_PATH
}
className
=
"
disabled
"
>
<
Link
to
=
{
DEMO_PATH
}
>
<
h3
>
<
i
className
=
"
fa fa-laptop small-only
"
/>
{
$t
(
'
client.accountwizard.menu.demo_title
'
)}
...
...
@@ -105,19 +105,7 @@ export default class AccountWizard extends React.Component {
<
/div
>
);
renderDemoMode
=
()
=>
(
<
div
>
<
header
>
<
h1
>
{
$t
(
'
client.accountwizard.demomode
'
)}
<
/h1
>
<
/header
>
<
p
>
{
$t
(
'
client.accountwizard.demomode_description
'
)}
<
/p
>
<
Link
className
=
"
btn danger
"
to
=
{
BASE_PATH
}
>
{
$t
(
'
client.general.cancel
'
)}
<
/Link
>
<
/div
>
);
renderDemoMode
=
()
=>
<
Demo
/>
;
renderWeboobReadme
=
()
=>
<
WeboobInstallReadme
/>
;
...
...
client/components/init/demo.js
0 → 100644
View file @
012f8970
import
React
from
'
react
'
;
import
{
connect
}
from
'
react-redux
'
;
import
{
Link
}
from
'
react-router-dom
'
;
import
{
actions
}
from
'
../../store
'
;
import
{
translate
as
$t
}
from
'
../../helpers
'
;
import
URL
from
'
../../urls
'
;
const
BASE_PATH
=
URL
.
initialize
.
url
();
const
Demo
=
connect
(
null
,
dispatch
=>
{
return
{
handleEnableDemoMode
()
{
actions
.
enableDemoMode
(
dispatch
);
}
};
}
)(
props
=>
{
return
(
<
div
>
<
header
>
<
h1
>
{
$t
(
'
client.demo.title
'
)}
<
/h1
>
<
/header
>
<
p
>
{
$t
(
'
client.demo.description
'
)}
<
/p
>
<
p
className
=
"
buttons-toolbar
"
>
<
Link
className
=
"
btn danger
"
to
=
{
BASE_PATH
}
>
{
$t
(
'
client.general.cancel
'
)}
<
/Link
>
<
button
type
=
"
button
"
className
=
"
btn primary
"
onClick
=
{
props
.
handleEnableDemoMode
}
>
{
$t
(
'
client.general.continue
'
)}
<
/button
>
<
/p
>
<
/div
>
);
});
export
default
Demo
;
client/css/layout.css
View file @
012f8970
...
...
@@ -129,6 +129,10 @@ header .section-title {
text-overflow
:
ellipsis
;
}
header
.disable-demo-mode
{
margin-right
:
2ex
;
}
header
.settings-dropdown
{
margin-right
:
2ex
;
}
...
...
client/css/sections/onboarding.css
View file @
012f8970
...
...
@@ -51,11 +51,6 @@
padding
:
1em
;
}
/* TODO: Remove once demo mode is implemented. */
.init-panes
.disabled
{
pointer-events
:
none
;
}
.init-panes
>
a
{
flex
:
auto
;
width
:
30%
;
...
...
@@ -118,6 +113,15 @@
margin-bottom
:
2em
;
}
.wizard-content
.buttons-toolbar
{
display
:
flex
;
justify-content
:
flex-end
;
}
.wizard-content
.buttons-toolbar
>
*
:not
(
:last-child
)
{
margin-right
:
2ex
;
}
@media
(
max-width
:
768px
)
{
.wizard
header
{
height
:
auto
;
...
...
client/main.js
View file @
012f8970
...
...
@@ -27,6 +27,8 @@ import AccountWizard from './components/init/account-wizard';
import
Menu
from
'
./components/menu
'
;
import
DropdownMenu
from
'
./components/menu/dropdown
'
;
import
DemoButton
from
'
./components/header/demo-button
'
;
import
{
LoadingMessage
,
LoadingOverlay
}
from
'
./components/ui/loading
'
;
import
ThemeLoaderTag
from
'
./components/ui/theme-link
'
;
import
ErrorReporter
from
'
./components/ui/error-reporter
'
;
...
...
@@ -135,6 +137,9 @@ class BaseApp extends React.Component {
<
Link
to
=
"
/
"
>
{
$t
(
'
client.KRESUS
'
)}
<
/Link
>
<
/h1
>
<
Route
path
=
{
URL
.
sections
.
pattern
}
render
=
{
this
.
makeSectionTitle
}
/
>
<
DemoButton
/>
<
DropdownMenu
currentAccountId
=
{
currentAccountId
}
/
>
<
/header
>
...
...
client/store/actions.js
View file @
012f8970
...
...
@@ -50,3 +50,7 @@ export const UPDATE_WEBOOB = 'UPDATE_WEBOOB';
export
const
GET_WEBOOB_VERSION
=
'
GET_WEBOOB_VERSION
'
;
export
const
GET_LOGS
=
'
GET_LOGS
'
;
export
const
CLEAR_LOGS
=
'
CLEAR_LOGS
'
;
// Demo
export
const
ENABLE_DEMO_MODE
=
'
ENABLE_DEMO_MODE
'
;
export
const
DISABLE_DEMO_MODE
=
'
DISABLE_DEMO_MODE
'
;
client/store/backend.js
View file @
012f8970
...
...
@@ -381,3 +381,15 @@ export function clearLogs() {
method
:
'
DELETE
'
});
}
export
function
enableDemoMode
()
{
return
buildFetchPromise
(
`api/
${
API_VERSION
}
/demo`
,
{
method
:
'
POST
'
});
}
export
function
disableDemoMode
()
{
return
buildFetchPromise
(
`api/
${
API_VERSION
}
/demo`
,
{
method
:
'
DELETE
'
});
}
client/store/index.js
View file @
012f8970
...
...
@@ -14,7 +14,7 @@ import DefaultSettings from '../../shared/default-settings';
import
{
FAIL
,
SUCCESS
,
fillOutcomeHandlers
}
from
'
./helpers
'
;
import
{
IMPORT_INSTANCE
}
from
'
./actions
'
;
import
{
IMPORT_INSTANCE
,
ENABLE_DEMO_MODE
,
DISABLE_DEMO_MODE
}
from
'
./actions
'
;
import
{
assert
,
assertHas
,
assertDefined
,
debug
}
from
'
../helpers
'
;
...
...
@@ -22,14 +22,17 @@ import * as backend from './backend';
import
{
genericErrorHandler
}
from
'
../errors
'
;
const
actionsWithStateReset
=
[
IMPORT_INSTANCE
,
ENABLE_DEMO_MODE
,
DISABLE_DEMO_MODE
];
// Augment basic reducers so that they can handle state reset:
// - if the event is a state reset (IMPORT_INSTANCE), pass the new sub-state to the reducer.
// - otherwise, apply to the actual reducer.
function
augmentReducer
(
reducer
,
field
)
{
return
(
state
,
action
)
=>
{
if
(
action
.
type
===
IMPORT_INSTANCE
&&
action
.
status
===
SUCCESS
)
{
if
(
action
sWithStateReset
.
includes
(
action
.
type
)
&&
action
.
status
===
SUCCESS
)
{
return
reducer
(
action
.
state
[
field
],
action
);
}
return
reducer
(
state
,
action
);
};
}
...
...
@@ -228,6 +231,11 @@ export const get = {
return
Ui
.
isMenuHidden
(
state
.
ui
);
},
isDemoMode
(
state
)
{
assertDefined
(
state
);
return
Ui
.
isDemoMode
(
state
.
ui
);
},
// *** Categories *********************************************************
// Categories
categories
(
state
)
{
...
...
@@ -456,6 +464,16 @@ export const actions = {
dispatch
(
Ui
.
toggleMenu
(
hideMenu
));
},
enableDemoMode
(
dispatch
)
{
assertDefined
(
dispatch
);
dispatch
(
enableDemo
());
},
disableDemoMode
(
dispatch
)
{
assertDefined
(
dispatch
);
dispatch
(
disableDemo
());
},
// *** Settings ***********************************************************
updateWeboob
(
dispatch
)
{
assertDefined
(
dispatch
);
...
...
@@ -649,7 +667,7 @@ export function init() {
state
.
themes
=
world
.
themes
;
// The UI must be computed at the end.
state
.
ui
=
Ui
.
initialState
();
state
.
ui
=
Ui
.
initialState
(
get
.
setting
(
state
,
'
demo-mode
'
)
);
return
new
Promise
(
accept
=>
{
accept
(
state
);
...
...
@@ -669,6 +687,20 @@ const basic = {
data
,
state
};
},
enableDemo
(
state
)
{
return
{
type
:
ENABLE_DEMO_MODE
,
state
};
},
disableDemo
(
state
)
{
return
{
type
:
DISABLE_DEMO_MODE
,
state
};
}
};
...
...
@@ -693,3 +725,37 @@ function importInstance(data, maybePassword) {
});
};
}
function
enableDemo
()
{
return
dispatch
=>
{
dispatch
(
basic
.
enableDemo
());
return
backend
.
enableDemoMode
()
.
then
(()
=>
{
return
init
();
})
.
then
(
newState
=>
{
dispatch
(
success
.
enableDemo
(
newState
));
})
.
catch
(
err
=>
{
dispatch
(
fail
.
enableDemo
(
err
));
});
};
}
function
disableDemo
()
{
return
dispatch
=>
{
dispatch
(
basic
.
disableDemo
());
return
backend
.
disableDemoMode
()
.
then
(()
=>
{
return
init
();
})
.
then
(
newState
=>
{
dispatch
(
success
.
disableDemo
(
newState
));
})
.
catch
(
err
=>
{
dispatch
(
fail
.
disableDemo
(
err
));
});
};
}
client/store/ui.js
View file @
012f8970
...
...
@@ -10,7 +10,9 @@ import {
TOGGLE_SEARCH_DETAILS
,
LOAD_THEME
,
UPDATE_MODAL
,
TOGGLE_MENU
TOGGLE_MENU
,
ENABLE_DEMO_MODE
,
DISABLE_DEMO_MODE
}
from
'
./actions
'
;
import
{
translate
as
$t
,
computeIsSmallScreen
,
notify
}
from
'
../helpers
'
;
...
...
@@ -81,6 +83,18 @@ const basic = {
type
:
TOGGLE_MENU
,
hideMenu
};
},
enableDemo
()
{
return
{
type
:
ENABLE_DEMO_MODE
};
},
disableDemo
()
{
return
{
type
:
DISABLE_DEMO_MODE
};
}
};
...
...
@@ -306,7 +320,9 @@ const reducers = {
UPDATE_WEBOOB
:
reduceUpdateWeboob
,
EXPORT_INSTANCE
:
reduceExportInstance
,
SET_IS_SMALL_SCREEN
:
reduceSetIsSmallScreen
,
TOGGLE_MENU
:
reduceToggleMenu
TOGGLE_MENU
:
reduceToggleMenu
,
ENABLE_DEMO_MODE
:
makeProcessingReasonReducer
(
'
client.demo.enabling
'
),
DISABLE_DEMO_MODE
:
makeProcessingReasonReducer
(
'
client.demo.disabling
'
)
};
const
uiState
=
u
({
...
...
@@ -314,7 +330,8 @@ const uiState = u({
displaySearchDetails
:
false
,
processingReason
:
'
client.general.loading_assets
'
,
updatingWeboob
:
false
,
sendingTestEmail
:
false
sendingTestEmail
:
false
,
isDemoMode
:
false
});
export
const
reducer
=
createReducerFromMap
(
uiState
,
reducers
);
...
...
@@ -332,7 +349,7 @@ function initialSearch() {
};
}
export
function
initialState
()
{
export
function
initialState
(
isDemoEnabled
)
{
let
search
=
initialSearch
();
return
u
(
{
...
...
@@ -341,6 +358,7 @@ export function initialState() {
processingReason
:
'
client.general.loading_assets
'
,
updatingWeboob
:
false
,
sendingTestEmail
:
false
,
isDemoMode
:
isDemoEnabled
,
isExporting
:
false
,
isSmallScreen
:
computeIsSmallScreen
(),
modal
:
{
...
...
@@ -402,3 +420,7 @@ export function getModal(state) {
export
function
isMenuHidden
(
state
)
{
return
state
.
isMenuHidden
;
}
export
function
isDemoMode
(
state
)
{
return
state
.
isDemoMode
;
}
client/themes/default/sections/onboarding.css
View file @
012f8970
...
...
@@ -12,15 +12,6 @@
color
:
inherit
;
}
/* TODO: Remove once demo mode is implemented. */
.init-panes
.disabled
{
background-color
:
rgba
(
0
,
0
,
0
,
0.3
);
}
.init-panes
.disabled
>
*
{
opacity
:
0.5
;
}
.init-panes
>
a
{
border-color
:
#C5C5C5
;
border-radius
:
2px
;
...
...
scripts/banks_list_generator.py
View file @
012f8970
...
...
@@ -25,7 +25,7 @@ from weboob.tools.value import Value, ValueBackendPassword
class
MockModule
(
object
):
def
__init__
(
self
,
name
,
description
,
config
,
backend
=
'mock'
):
def
__init__
(
self
,
name
,
description
,
config
,
backend
=
'mock
-with-errors
'
):
self
.
name
=
name
self
.
description
=
description
self
.
config
=
config
...
...
@@ -52,6 +52,8 @@ MANUAL_MODULES = [MockModule('manual', 'Manual Bank', BackendConfig(
Value
(
'login'
),
ValueBackendPassword
(
'password'
)),
backend
=
'manual'
)]
MOCK_MODULES
=
[
MockModule
(
'demo'
,
'Demo bank'
,
BackendConfig
(
Value
(
'login'
),
ValueBackendPassword
(
'password'
)),
backend
=
'mock'
),
MockModule
(
'fakebank1'
,
'Fake Bank 1'
,
BackendConfig
(
Value
(
'login'
),
ValueBackendPassword
(
'password'
))),
MockModule
(
'fakebank2'
,
'Fake Bank 2'
,
BackendConfig
(
...
...
server/controllers/v1/accesses.js
View file @
012f8970
...
...
@@ -27,51 +27,48 @@ export async function preloadAccess(req, res, next, accessId) {
}
}
// Destroy a given access, including accounts, alerts and operations.
export
async
function
destroy
(
req
,
res
)
{
try
{
let
{
id
:
userId
}
=
req
.
user
;
let
access
=
req
.
preloaded
.
access
;
log
.
info
(
`Removing access
${
access
.
id
}
for bank
${
access
.
vendorId
}
...`
);
export
async
function
destroyWithData
(
userId
,
access
)
{
log
.
info
(
`Removing access
${
access
.
id
}
for bank
${
access
.
vendorId
}
...`
);
// TODO arguably, this should be done in the access model.
let
accounts
=
await
Accounts
.
byAccess
(
userId
,
access
);
for
(
let
account
of
accounts
)
{
await
AccountController
.
destroyWithOperations
(
userId
,
account
);
}
// TODO arguably, this should be done in the access model.
let
accounts
=
await
Accounts
.
byAccess
(
userId
,
access
);
for
(
let
account
of
accounts
)
{
await
AccountController
.
destroyWithOperations
(
userId
,
account
);
}
// The access should have been destroyed by the last account deletion.
let
stillThere
=
await
Accesses
.
exists
(
userId
,
access
.
id
);
if
(
stillThere
)
{
log
.
error
(
'
Access should have been deleted! Manually deleting.
'
);
await
Accesses
.
destroy
(
userId
,
access
.
id
);
}
// The access should have been destroyed by the last account deletion.
let
stillThere
=
await
Accesses
.
exists
(
userId
,
access
.
id
);
if
(
stillThere
)
{
log
.
error
(
'
Access should have been deleted! Manually deleting.
'
);
await
Accesses
.
destroy
(
userId
,
access
.
id
);
}
log
.
info
(
'
Done!
'
);
}
log
.
info
(
'
Done!
'
);
// Destroy a given access, including accounts, alerts and operations.
export
async
function
destroy
(
req
,
res
)
{
try
{
await
destroyWithData
(
req
.
user
.
id
,
req
.
preloaded
.
access
);
res
.
status
(
204
).
end
();
}
catch
(
err
)
{
return
asyncErr
(
res
,
err
,
'
when destroying an access
'
);
}
}
// Creates a new bank access (expecting at least (vendorId / login /
// password)), and retrieves its accounts and operations.
export
async
function
create
(
req
,
res
)
{
let
access
;
export
async
function
createAndRetrieveData
(
userId
,
params
)
{
let
error
=
checkHasAllFields
(
params
,
[
'
vendorId
'
,
'
login
'
,
'
password
'
])
||
checkAllowedFields
(
params
,
[
'
vendorId
'
,
'
login
'
,
'
password
'
,
'
fields
'
,
'
customLabel
'
]);
if
(
error
)
{
throw
new
KError
(
`when creating a new access:
${
error
}
`
,
400
);
}
let
access
=
null
;
let
createdAccess
=
false
;
let
retrievedAccounts
=
false
;
let
{
id
:
userId
}
=
req
.
user
;
try
{
let
params
=
req
.
body
;
let
error
=
checkHasAllFields
(
params
,
[
'
vendorId
'
,
'
login
'
,
'
password
'
])
||
checkAllowedFields
(
params
,
[
'
vendorId
'
,
'
login
'
,
'
password
'
,
'
fields
'
,
'
customLabel
'
]);
if
(
error
)
{
throw
new
KError
(
`when creating a new access:
${
error
}
`
,
400
);
}
access
=
await
Accesses
.
create
(
userId
,
params
);
createdAccess
=
true
;
...
...
@@ -83,11 +80,11 @@ export async function create(req, res) {
access
);
re
s
.
status
(
201
).
json
(
{
re
turn
{
accessId
:
access
.
id
,
accounts
,
newOperations
}
)
;
};
}
catch
(
err
)
{
log
.
error
(
'
The access process creation failed, cleaning up...
'
);
...
...
@@ -106,6 +103,18 @@ export async function create(req, res) {
await
Accesses
.
destroy
(
userId
,
access
.
id
);
}
// Rethrow the error
throw
err
;
}
}
// Creates a new bank access (expecting at least (vendorId / login /
// password)), and retrieves its accounts and operations.
export
async
function
create
(
req
,
res
)
{
try
{
const
data
=
await
createAndRetrieveData
(
req
.
user
.
id
,
req
.
body
);
res
.
status
(
201
).
json
(
data
);
}
catch
(
err
)
{
return
asyncErr
(
res
,
err
,
'
when creating a bank access
'
);
}
}
...
...
server/controllers/v1/demo.js
0 → 100644
View file @
012f8970
import
Accesses
from
'
../../models/accesses
'
;
import
Categories
from
'
../../models/categories
'
;
import
Settings
from
'
../../models/settings
'
;
import
{
asyncErr
,
KError
,
translate
as
$t
}
from
'
../../helpers
'
;
import
{
createAndRetrieveData
as
createAndRetrieveAccessData
,
destroyWithData
as
destroyAccessWithData
}
from
'
./accesses
'
;
import
DefaultCategories
from
'
../../shared/default-categories.json
'
;
export
async
function
isEnabled
(
userId
)
{
return
await
Settings
.
findOrCreateDefaultBooleanValue
(
userId
,
'
demo-mode
'
);
}
export
async
function
enable
(
req
,
res
)
{
try
{
let
{
id
:
userId
}
=
req
.
user
;
let
isDemoEnabled
=
await
isEnabled
(
userId
);
if
(
isDemoEnabled
)
{