import os 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 plotly.express as px 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=[ # Layer 0: For the Header and Help Function(s) dbc.Button( '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([ "Input: ", # 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, style={ "width": "400px"}, ), ]),size="lg", color="primary", type="border", fullscreen=True,), # Layer 2: For file input and recursion depths html.Div([ "References Depth: ", # Forward recursion. Values between 1 and 10 can be entered. dcc.Input(id='forward-depth',value='1',type='number',min='0',max='5', style={ "width": "50px"}, ), " Cited-by Depth: ", # Backward recursion. Values between 1 and 10 can be entered. dcc.Input(id='backward-depth',value='1',type='number',min='0',max='5', style={"width": "50px"}, ), # Upload box. Can be used via drag-and-drop or byclicking on it to open a file viewer. dbc.Spinner(dcc.Upload( id="file-input", children=html.Div( #Drag and drop or click to select a file to upload ["Drag and drop"]), style={ "width": "400px", "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 html.Div([ # 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'}), # Clear all selected elements. dbc.Button(id='clear-selected-button',children='Clear Selected', color="primary", className="me-1",style={'display': 'inline-block'}), # Starts the process that generates a graph. 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], value=[],labelStyle = dict(display= 'block')) ]), ], 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'), Output('input-err','children'), 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 input_value not in currValues: # 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) except Exception as err: 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(),'','' @app.callback( Output('collapse', 'is_open'), [Input('collapse-button', 'n_clicks')], [State('collapse', 'is_open')], ) def toggle_collapse(n, 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 ''' if n: return not is_open return is_open @app.callback( 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 if __name__ == '__main__': app.run_server(debug=False)