Newer
Older
import base64
import re
import dash
from dash import dcc
from dash import html
from dash import callback_context
from dash.dependencies import Input, Output, State
from dash.exceptions import PreventUpdate
from input.interface import InputInterface
import input.publication
from verarbeitung.process_main import Processing
from dash.dependencies import Input, Output, State
import dash_bootstrap_components as dbc # pip install dash-bootstrap-components
app = dash.Dash(__name__, external_stylesheets=[dbc.themes.SPACELAB]) #SPACELAB https://bootswatch.com/default/ for more themes)
# List of options when inputting data and generating the graph
additional_options = ['Update Automatically','Smart Input']
# Reads the contents of info_box.txt.
# They can later be displayed by pressing the corresponding button.
f = open('info_box.txt', 'r')
boxcontent = f.read()
f.close()
app.layout = html.Div([
html.Div(children=[
'Show Info',
id='collapse-button',
className="me-1",
color="primary",
n_clicks=0,
),
dbc.Collapse(
dbc.Card(dbc.CardBody(html.Div(boxcontent, style={'whiteSpace': 'pre-line'}))),
id='collapse',
is_open=False,
),
# Layer 1: For the string input
dbc.Spinner(html.Div([
# A simple box for inputting a string.
# Value is transmitted upon pressing return or clicking out of the box.
dcc.Input(id='string-input', value='', type='text',debounce=True,
]),size="lg", color="primary", type="border", fullscreen=True,),
# Layer 2: For file input and recursion depths
html.Div([
# Forward recursion. Values between 1 and 10 can be entered.
dcc.Input(id='forward-depth',value='1',type='number',min='0',max='5',
# Backward recursion. Values between 1 and 10 can be entered.
dcc.Input(id='backward-depth',value='1',type='number',min='0',max='5',
# Upload box. Can be used via drag-and-drop or byclicking on it to open a file viewer.
dbc.Spinner(dcc.Upload(
#Drag and drop or click to select a file to upload
"height": "60px",
"lineHeight": "60px",
"borderWidth": "1px",
"borderStyle": "dashed",
"borderRadius": "5px",
"textAlign": "center",
"margin": "10px",
}),size="lg", color="primary", type="border", fullscreen=True,),
# Layer 3: For the checklist, Remove-/Start-Buttons and error message
# All input DOIs are collected in this checklist.
# It is initialized to avoid error messages.
dcc.Checklist(id='input-checklist',options=[],
labelStyle = dict(display='block'),value=[]),
# Displays error message if 'Smart Input' is active.
html.Div(id='input-err',style={'color':'red'}),
# Clears the entire list.
dbc.Button(id='clear-all-button',children='Clear All', color="primary", className="me-1",style={'display': 'inline-block'}),
dbc.Button(id='clear-selected-button',children='Clear Selected', color="primary", className="me-1",style={'display': 'inline-block'}),
dbc.Button(id='start-button',children='Generate Graph', color="primary", className="me-1",style={'display': 'inline-block'})
# Layer 4: For additional Options
html.Div([
html.H4('Additional Options'),
# A checklist of all additional options that are listed above.
dcc.Checklist(id='additional-options',
options=[{'label':k,'value':k} for k in additional_options],
], style={'padding': 10, 'flex': 0.8}),
html.Div(children=[
# Layer 5: For the Graph and corresponding error messages
dbc.Spinner(html.Div([
html.Div(id='generate-graph-error',style={'color':'red'}),
html.Iframe(
src="assets/index.html",
style={"height": "650px", "width": "100%"},
]),size="lg", color="primary", type="border", fullscreen=True,),
], style={'padding': 10, 'flex': 1.2})
], style={'display': 'flex', 'flex-direction': 'row'})
@app.callback(
Output('input-checklist','options'),
Output('input-checklist','value'),
Output('string-input','value'),
Input('string-input','value'),
Input('clear-all-button','n_clicks'),
Input('clear-selected-button','n_clicks'),
Input('file-input','contents'),
State('input-checklist','options'),
State('input-checklist','value'),
State('additional-options','value')
)
def update_input_checklist(input_value,btn1,btn2,filecontents,all_inputs,
selected_inputs,additional_options):
'''
Most important callback function. Updates the checklist that holds all inputs.
State of the checklist as input is needed so that previews entries are readded.
string-input is required as Output to clear the input box after each input.
Different actions are performed depending on which input triggered the callback.
The value-attribute of input-checklist must be updates so that the values
of deleted elements no longer appear in the list of selected elements.
:param input_value: given by dcc.Input
:type input_value: string
:param btn1: signals pressing of clear-all-button
:type btn1: int
:param btn2: signals pressing of clear-selected-button
:type btn2: int
:param filecontents: the contents of an uploaded file
:type filecontents: bit-string
:param all_inputs: all labels and values from the checklist,
regardless if they have been checked or not
:type all_inputs: list of dictionaries with 2 entries each
:param selected_inputs: values of all checked elements
:type selected_inputs: list of strings
:param addtitional_options: all checked additional options
:type additional_options: list of strings
'''
# changed_id is used to determine which Input has triggered the callback
changed_id = [p['prop_id'] for p in callback_context.triggered][0]
# if clear-all-button was pressed:
if 'clear-all-button' in changed_id:
os.remove('assets/json_text.json')
return list(),list(),'',''
# if clear-selected-button was pressed:
if 'clear-selected-button' in changed_id:
all_inputs = [i for i in all_inputs if i['value'] not in selected_inputs]
return all_inputs,list(),'',''
# when a new element is added via dcc.Input
if 'string-input' in changed_id:
# Creates a list of previously added inputs to make sure nothing is added twice
currValues = [x['value'] for x in all_inputs]
# if 'Smart Input' is selected, the input will be checked for validity
# and a more readable string will be returned
if 'Smart Input' in additional_options:
try:
# Attempts to call get_publication. If unsuccesful,
# the DOI is not added and an error message is returned
i = InputInterface()
pub = i.get_pub_light(input_value)
return all_inputs,selected_inputs,'','{}'.format(err)
# Creates a more readable string to display in the checklist
rep_str = pub.contributors[0] + ',' + pub.journal + \
',' + pub.publication_date
# Makes sure not to add the same article with different links
currLabels = [x['label'] for x in all_inputs]
if rep_str not in currLabels:
all_inputs.append({'label':rep_str, 'value':input_value})
# if 'Smart Input' is not selected, the input value is added as is,
# without checking for validity.
else:
all_inputs.append({'label':input_value,'value':input_value})
return all_inputs,selected_inputs,'',''
# when a txt-file is uploaded
if 'file-input.contents' in changed_id:
if filecontents:
# Skips the info portion that is added when a file is uploaded
found = base64.b64decode(re.search(',(.+?)$', filecontents).group(1))
# Returns the binary string into a proper text
text = found.decode('utf-8')
# Creates a list of inputs by splitting the lines
list_of_inputs = (text.strip().split('\n'))
CurrValues = [x['value'] for x in all_inputs]
# For every line the same actions as for a single input are performed
for input_value in list_of_inputs:
if input_value not in CurrValues:
if 'Smart Input' in additional_options:
try:
i = InputInterface()
pub = i.get_pub_light(input_value)
except Exception as err:
return all_inputs,selected_inputs,'','{}'.format(err)
rep_str = pub.contributors[0] + ',' + pub.journal + \
',' + pub.publication_date
currLabels = [x['label'] for x in all_inputs]
if rep_str not in currLabels:
all_inputs.append({'label':rep_str, 'value':input_value})
else:
all_inputs.append({'label':input_value,'value':input_value})
return all_inputs,selected_inputs,'',''
# when the programm is first started:
# if this is not done, the input_checklist will be generated
# with one element that contains an empty string
if input_value == '':
return list(),list(),'',''
Output('collapse', 'is_open'),
[Input('collapse-button', 'n_clicks')],
[State('collapse', 'is_open')],
This callback shows and hides the (first) info-box by, checking how# often
the button has been pressed. The text was loaded at the top.
:param n_clicks: number of times show-info has been clicked.
'type n_clicks: int
'''
Output('generate-graph-error','children'),
Input('start-button','n_clicks'),
Input('input-checklist','options'),
Input('forward-depth','value'),
Input('backward-depth','value'),
State('additional-options','value')
)
def generate_output(n_clicks,all_inputs,forward_depth,backward_depth,additional_options):
Basic structure for a callback that generates an output. This is only a
proof of concept and has noting to do with the intended output yet.
:param n_clicks: how often has Generate Graph been clicked
:type n_clicks: int
:param all_inputs: all labels and values from the checklist,
regardless if they have been checked or not
:type all_inputs: list of dictionaries with 2 entries each
:param forward_depth: forward recursion depth
:type forward_depth: unsigned int
:param backward_depth: backward recursion depth
:type backward_depth: unsigned int
:param additional_options: value of all selected additional options
:type additional_options: list of strings
'''
changed_id = [p['prop_id'] for p in callback_context.triggered][0]
if n_clicks is None:
raise PreventUpdate
elif 'Update Automatically' in additional_options \
or 'start-button' in changed_id:
input_links = [x['value'] for x in all_inputs]
errors = Processing(input_links,int(forward_depth),int(backward_depth),'assets/json_text.json')
if errors:
message = ['The following inputs are invalid and were not used:']
for error in errors:
message.append(html.Br())
message.append(error)
message = html.P(message)
#message = [html.P(error) for error in errors]
return message
app.run_server(debug=False)