Skip to content
Snippets Groups Projects
citation_parser_main.py 12.5 KiB
Newer Older
David, Sebastian's avatar
David, Sebastian committed
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
Suireen's avatar
Suireen committed
import plotly.express as px
import dash_bootstrap_components as dbc # pip install dash-bootstrap-components
Suireen's avatar
Suireen committed


app = dash.Dash(__name__, external_stylesheets=[dbc.themes.SPACELAB])  #SPACELAB https://bootswatch.com/default/ for more themes)
David, Sebastian's avatar
David, Sebastian committed

# 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.
David, Sebastian's avatar
David, Sebastian committed
f = open('info_box.txt', 'r')
boxcontent = f.read()
f.close()

app.layout = html.Div([
David, Sebastian's avatar
David, Sebastian committed
    # Layer 0: For the Header and Help Function(s)
Suireen's avatar
Suireen committed
    dbc.Button(
Suireen's avatar
Suireen committed
            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
David, Sebastian's avatar
David, Sebastian committed
        "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,
Suireen's avatar
Suireen committed
        style={ "width": "400px"},
        ),
        
    ]),size="lg", color="primary", type="border", fullscreen=True,),
        # Layer 2: For file input and recursion depths
        "References Depth: ",
David, Sebastian's avatar
David, Sebastian committed
        # Forward recursion. Values between 1 and 10 can be entered.
        dcc.Input(id='forward-depth',value='1',type='number',min='0',max='5',
Suireen's avatar
Suireen committed
        style={ "width": "50px"},
        ),
        " Cited-by Depth: ",
David, Sebastian's avatar
David, Sebastian committed
        # Backward recursion. Values between 1 and 10 can be entered.
        dcc.Input(id='backward-depth',value='1',type='number',min='0',max='5',
Suireen's avatar
Suireen committed
        style={"width": "50px"},
        ),
        # Upload box. Can be used via drag-and-drop or byclicking on it to open a file viewer.
David, Sebastian's avatar
David, Sebastian committed
            children=html.Div(
Suireen's avatar
Suireen committed
            #Drag and drop or click to select a file to upload
Suireen's avatar
Suireen committed
                ["Drag and drop"]),
David, Sebastian's avatar
David, Sebastian committed
            style={
David, Sebastian's avatar
David, Sebastian committed
                "height": "60px",
                "lineHeight": "60px",
                "borderWidth": "1px",
                "borderStyle": "dashed",
                "borderRadius": "5px",
                "textAlign": "center",
                "margin": "10px",
            }),size="lg", color="primary", type="border", fullscreen=True,),
David, Sebastian's avatar
David, Sebastian committed
    ]),
    # Layer 3: For the checklist, Remove-/Start-Buttons and error message
David, Sebastian's avatar
David, Sebastian committed
    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=[]),
David, Sebastian's avatar
David, Sebastian committed
        # 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'}),
David, Sebastian's avatar
David, Sebastian committed
        # Clear all selected elements.
        dbc.Button(id='clear-selected-button',children='Clear Selected', color="primary", className="me-1",style={'display': 'inline-block'}),
David, Sebastian's avatar
David, Sebastian committed
        # Starts the process that generates a graph.
        dbc.Button(id='start-button',children='Generate Graph', color="primary", className="me-1",style={'display': 'inline-block'})
David, Sebastian's avatar
David, Sebastian committed
    ]),
    # Layer 4: For additional Options
David, Sebastian's avatar
David, Sebastian committed
    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],
Suireen's avatar
Suireen committed
            value=[],labelStyle = dict(display= 'block'))
Suireen's avatar
Suireen committed
        ]),
        
    ], style={'padding': 10, 'flex': 0.8}),

 html.Div(children=[
    # Layer 5: For the Graph and corresponding error messages
        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'})
David, Sebastian's avatar
David, Sebastian committed

@app.callback(
    Output('input-checklist','options'),
    Output('input-checklist','value'),
    Output('string-input','value'),
David, Sebastian's avatar
David, Sebastian committed
    Output('input-err','children'),
    Input('string-input','value'),
David, Sebastian's avatar
David, Sebastian committed
    Input('clear-all-button','n_clicks'),
    Input('clear-selected-button','n_clicks'),
    Input('file-input','contents'),
David, Sebastian's avatar
David, Sebastian committed
    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):
David, Sebastian's avatar
David, Sebastian committed
    '''
    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.
David, Sebastian's avatar
David, Sebastian committed
    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')
David, Sebastian's avatar
David, Sebastian committed
        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:
David, Sebastian's avatar
David, Sebastian committed
        # Creates a list of previously added inputs to make sure nothing is added twice
        currValues = [x['value'] for x in all_inputs]
David, Sebastian's avatar
David, Sebastian committed
        if input_value not in currValues:

            # if 'Smart Input' is selected, the input will be checked for validity
David, Sebastian's avatar
David, Sebastian committed
            # and a more readable string will be returned
            if 'Smart Input' in additional_options:
                try:
                    # Attempts to call get_publication. If unsuccesful,
David, Sebastian's avatar
David, Sebastian committed
                    # the DOI is not added and an error message is returned
                    i = InputInterface()
                    pub = i.get_pub_light(input_value)
David, Sebastian's avatar
David, Sebastian committed
                except Exception as err:
                    return all_inputs,selected_inputs,'','{}'.format(err)
David, Sebastian's avatar
David, Sebastian committed
                # 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})
David, Sebastian's avatar
David, Sebastian committed
            
            # 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:
David, Sebastian's avatar
David, Sebastian committed
        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)
David, Sebastian's avatar
David, Sebastian committed
                        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})
David, Sebastian's avatar
David, Sebastian committed
                    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
David, Sebastian's avatar
David, Sebastian committed
    # with one element that contains an empty string
    if input_value == '':
        return list(),list(),'',''

David, Sebastian's avatar
David, Sebastian committed
@app.callback(
Suireen's avatar
Suireen committed
    Output('collapse', 'is_open'),
    [Input('collapse-button', 'n_clicks')],
    [State('collapse', 'is_open')],
Suireen's avatar
Suireen committed
def toggle_collapse(n, is_open):
David, Sebastian's avatar
David, Sebastian committed
    '''
Suireen's avatar
Suireen committed
    This callback shows and hides the (first) info-box by, checking how# often
David, Sebastian's avatar
David, Sebastian committed
    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
    '''
Suireen's avatar
Suireen committed
    if n:
        return not is_open
    return is_open

David, Sebastian's avatar
David, Sebastian committed

@app.callback(
    Output('generate-graph-error','children'),
David, Sebastian's avatar
David, Sebastian committed
    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):
David, Sebastian's avatar
David, Sebastian committed
    '''
    Basic structure for a callback that generates an output. This is only a
David, Sebastian's avatar
David, Sebastian committed
    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
David, Sebastian's avatar
David, Sebastian committed

if __name__ == '__main__':
    app.run_server(debug=False)