Skip to content
Snippets Groups Projects
Select Git revision
  • 916612e8fba48315c5270fc4d898419e474af90a
  • main default protected
2 results

citation_parser_ui.py

Blame
  • Code owners
    Assign users and groups as approvers for specific file changes. Learn more.
    citation_parser_ui.py 10.58 KiB
    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
    
    app = dash.Dash(__name__)
    
    # 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([
        # Layer 0: For the Header and Help Function(s)
        html.Div([
            html.Button(id='show-info',children='Show Info',n_clicks=0),
            html.Div(id='info-box')
        ]),
        # Layer 1: For all mandatory Inputs
        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='input-string', value='', type='text',debounce=True),
            # Forward recursion. Values between 1 and 10 can be entered.
            dcc.Input(id='forward-depth',value='1',type='number',min='1',max='10'),
            # Backward recursion. Values between 1 and 10 can be entered.
            dcc.Input(id='backward-depth',value='1',type='number',min='1',max='10'),
            # Upload box. Can be used via drag-and-drop or byclicking on it to open a file viewer.
            dcc.Upload(
                id="upload-data",
                children=html.Div(
                    ["Drag and drop or click to select a file to upload."]),
                style={
                    "width": "30%",
                    "height": "60px",
                    "lineHeight": "60px",
                    "borderWidth": "1px",
                    "borderStyle": "dashed",
                    "borderRadius": "5px",
                    "textAlign": "center",
                    "margin": "10px",
                })
        ]),
        # Layer 2: For the checklist, Remove-/Start-Buttons and input-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.
            html.Button(id='clear-all-button',children='Clear All'),
            # Clear all selected elements.
            html.Button(id='clear-selected-button',children='Clear Selected'),
            # Starts the process that generates a graph.
            html.Button(id='start-button',children='Generate Graph')
        ]),
        # Layer 3: For additional Options (e.g. Topological Sort)
        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=[])
        ]),
        # Layer 4: For the Graph
        html.Div([
            html.Div(id='test-output')
        ])
    ])
    
    @app.callback(
        Output('input-checklist','options'),
        Output('input-checklist','value'),
        Output('input-string','value'),
        Output('input-err','children'),
        Input('input-string','value'),
        Input('clear-all-button','n_clicks'),
        Input('clear-selected-button','n_clicks'),
        Input('upload-data','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.
        input-string 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:
            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 'input-string' 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 options,selected_inputs,'','{}'.format(err)
                    # Creates a more readable string to display in the checklist
                    rep_str = pub.contributors[0] + ',' + pub.journal + \
                            ',' + pub.publication_date
                    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 'upload-data.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
                            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('info-box','children'),
        Input('show-info','n_clicks')
    )
    def show_hide_info_box(n_clicks):
        '''
        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_clicks % 2 == 0:
            return ''
        else:
            return html.Div(boxcontent, style={'whiteSpace': 'pre-line'})
    
    @app.callback(
        Output('test-output','children'),
        Input('start-button','n_clicks'),
        Input('input-checklist','options'),
        Input('input-checklist','value'),
        Input('forward-depth','value'),
        Input('backward-depth','value'),
        State('additional-options','value')
    )
    def generate_output(n_clicks,all_inputs,selected_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 selected_inputs: values of all checked elements
        :type selected_inputs: list of strings
        :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: 
            s = ''
            for i in range(len(all_inputs)):
                x = all_inputs[i]['value']
                if x in selected_inputs:
                    s += x*(abs(int(forward_depth)-int(backward_depth)))
                else:
                    s += x*(int(forward_depth)+int(backward_depth))
            return s
        else:
            raise PreventUpdate
    
    if __name__ == '__main__':
        app.run_server(debug=True)