def oset(self, obj, key, value, cvar=None):
    setattr(obj, (cvar or self.cvar)[key], value) def dset(self, collection, key, value, cvar=None):
    collection[(cvar or self.cvar)[key]] = value def oget(self, obj, key, default=None, cvar=None):
    return getattr(obj, (cvar or self.cvar)[key], default) def consume(info, key):
    """Consumes key from dict

    Arguments:

    * `info` -- dict
    * `key` -- key

    Doctest:

    .. doctest::

        >>> info = {'abc': 1, 'def': 2}
        >>> consume(info, 'abc')
        1
        >>> info
        {'def': 2}
        >>> consume(info, 'abc') is None
        True
    """
    if key not in info:
        return None
    result = info[key]
    del info[key]
    return result def display_cell(text):
    """Remove cells that start with "# Temp" and add a new one

    Arguments:

    * `text` -- new cell content

    """
    encoded_code = base64.b64encode(quote(text.encode()).encode()).decode()
    display(Javascript("""
        $('span:contains("# Temp")').closest('.cell').remove();
        var code = IPython.notebook.insert_cell_{0}('code');
        code.set_text(decodeURIComponent(window.atob("{1}")));
    """.format("below", encoded_code))) def bibtex_to_info(citation, rules=None):
    """Convert BibTeX dict from bibtexparse to info dict for adding a db entry
    
    Doctest:

    .. doctest::

        >>> bibtex_to_info({'title': 'a', 'author': 'Pim, J'})
        {'place1': '', 'year': 0, 'name': 'a', 'authors': 'Pim, J', 'display': 'pim', 'pyref': 'pim0a'}
        >>> bibtex_to_info({'title': 'a', 'author': 'Pim, J', 'year': '2017'})
        {'place1': '', 'year': 2017, 'name': 'a', 'authors': 'Pim, J', 'display': 'pim', 'pyref': 'pim2017a'}
        >>> bibtex_to_info({'title': 'a', 'author': 'Pim, J', 'year': '2017 [in press]'})
        {'place1': '', 'year': 2017, 'name': 'a', 'authors': 'Pim, J', 'note': 'in press', 'display': 'pim', 'pyref': 'pim2017a'}
        >>> bibtex_to_info({'title': 'a', 'author': 'Pim, J', 'pages': '1--5'})
        {'place1': '', 'year': 0, 'name': 'a', 'authors': 'Pim, J', 'pp': '1--5', 'display': 'pim', 'pyref': 'pim0a'}
        >>> bibtex_to_info({'title': 'a', 'author': 'Pim, J', 'journal': 'CiSE'})
        {'place1': 'CiSE', 'year': 0, 'name': 'a', 'authors': 'Pim, J', 'place': 'CiSE', 'display': 'pim', 'pyref': 'pim0a'}
        >>> bibtex_to_info({'title': 'a', 'author': 'Pim, J', 'ENTRYTYPE': 'article'})
        {'place1': '', 'year': 0, 'name': 'a', 'authors': 'Pim, J', 'entrytype': 'article', 'display': 'pim', 'pyref': 'pim0a'}
        >>> bibtex_to_info({'title': 'a', 'author': 'Pim, J', 'other': 'a'})
        {'place1': '', 'year': 0, 'name': 'a', 'authors': 'Pim, J', 'display': 'pim', 'pyref': 'pim0a', 'other': 'a'}
   
    """
    rules = rules or config.BIBTEX_TO_INFO
    return ConvertDict(rules).run(citation) def reload(work_func=None):
    """Reload all the database

    Doctest:

    ..doctest::

        >>> reload()
        >>> from snowballing.example.database.work.y2014 import murta2014a
        >>> murta2014a.metakey
        'murta2014a'
        >>> from snowballing.example.database.work.y2015 import murta2014a as alias
        >>> alias is murta2014a
        True
    """
    
    _clear_db()
    if config.MODULES["places"]:
        importlib.reload(config.MODULES["places"])
    _reload_work()
    import_submodules(config.MODULES["citations"])
    import_submodules(config.MODULES["groups"])
    if getattr(config, "CHECK_DEPRECATION", True):
        check_config_deprecation()

    for key, work in load_work_map_all_years():
        oset(work, "metakey", key)
        if work_func:
            work_func(work, key)
        for alias in config.get_work_aliases(work):
            year = config.get_alias_year(work, alias)
            module = "y{}.py".format(year) if isinstance(year, int) else year
            if module not in WORK_CACHE:
                module = "y9999.py"
            setattr(WORK_CACHE[module], key, work) def citation_text(workref, cited, ref="", backward=False):
    """Create code for citation

    Arguments:

    * `workref` -- work varname that is cited (by default)

    * `cited` -- work info dict that cites the work (by default)

    Keyword arguments:

    * `ref` -- citation number

    * `backward` -- invert citation: `workref` cites `cited`


    Doctest:

    .. doctest::

        >>> print(citation_text('freire2008a', {'pyref': 'murta2014a'}))
        <BLANKLINE>
        DB(Citation(
            murta2014a, freire2008a, ref="",
            contexts=[
        <BLANKLINE>
            ],
        ))
        <BLANKLINE>

        >>> print(citation_text('pimentel2015a', {'pyref': 'murta2014a'}, backward=True, ref="[8]"))
        <BLANKLINE>
        DB(Citation(
            pimentel2015a, murta2014a, ref="[8]",
            contexts=[
        <BLANKLINE>
            ],
        ))
        <BLANKLINE>
    """
    pyref = dget(cited, "pyref")
    thepyref = pyref
    if backward:
        pyref, workref = workref, pyref
    return textwrap.dedent("""
    DB(Citation(
        {pyref}, {workref}, ref="{ref}",
        contexts=[

        ],
    ))
    """.format(**locals())) def work_by_varname(varname, year=None):
    """Load work by varname

    Doctest:

    .. doctest::

        >>> reload()
        >>> work = work_by_varname('murta2014a')
        >>> work.year
        2014
    """
    if year is None:
        year = int(parse_varname(varname, 2) or -1)
    module = "y{}.py".format(year) if isinstance(year, int) else year
    if module not in WORK_CACHE:
        return
    worklist = WORK_CACHE[module]
    return getattr(worklist, varname, None) def extract_info(article, rules=None):
    """Extract info from google scholar article

    Doctest:

    .. doctest::

        Mock:

        >>> class Article: pass
        >>> article = Article()
        >>> article.as_citation = lambda: '''
        ... @inproceedings{murta2014noworkflow,
        ...   title={noWorkflow: capturing and analyzing provenance of scripts},
        ...   author={Murta, Leonardo and Braganholo, Vanessa and Chirigati, Fernando and Koop, David and Freire, Juliana},
        ...   booktitle={International Provenance and Annotation Workshop},
        ...   pages={71--83},
        ...   year={2014},
        ...   organization={Springer}
        ... }'''
        >>> article.attrs = {
        ...   'excerpt': ['Abstract'],
        ...   'cluster_id': ['5458343950729529273'],
        ...   'url_citations': ['http://scholar.google.com/scholar?cites=5458343950729529273&as_sdt=2005&sciodt=0,5&hl=en'],
        ... }
        >>> article.div = None

        Test:

        >>> reload()  # Deterministic name
        >>> extract_info(article)
        {'place1': 'International Provenance and Annotation Workshop', 'year': 2014, 'pp': '71--83', 'authors': 'Murta, Leonardo and Braganholo, Vanessa and Chirigati, Fernando and Koop, David and Freire, Juliana', 'name': 'noWorkflow: capturing and analyzing provenance of scripts', 'entrytype': 'inproceedings', 'place': 'IPAW', 'display': 'murta', 'pyref': 'murta2014b', 'organization': 'Springer', 'ID': 'murta2014noworkflow', 'excerpt': 'Abstract', 'cluster_id': '5458343950729529273', 'scholar': 'http://scholar.google.com/scholar?cites=5458343950729529273&as_sdt=2005&sciodt=0,5&hl=en'}
    """
    rules = rules or config.BIBTEX_TO_INFO
    as_citation = article.as_citation()
    if not isinstance(as_citation, str):
        as_citation = as_citation.decode("utf-8")
    citation = parse_bibtex(as_citation)[0]
    converter = ConvertDict(rules)
    return converter.run(citation, article=article) def create_info_code(nwork, info, citation_var, citation_file, should_add, ref=""):
    """Create insertion code with both code and citation"""
    citations = ""
    text = "insert('''"
    if nwork is None:
        text += info_to_code(info) + "\n"
    if should_add["citation"] and citation_var:
        text += citation_text(
            citation_var, info,
            ref=ref, backward=should_add["backward"]
        ) + "\n"
        citations = ", citations='{}'".format(citation_file)
    text += "'''{});".format(citations)
    if text == "insert('''''');":
        text = ""

    if nwork and should_add["set"] and "(" not in dget(info, "pyref"):
        text += "\n" + changes_dict_to_set_attribute(dget(info, "pyref"), should_add["set"])

    return {
        "code": text.strip(),
        "extra": config.check_insertion(
            nwork, info, citation_var, citation_file, should_add, ref=""
        )
    } def should_add_info(
    info, citation, article=None, backward=False, citation_file=None,
    warning=lambda x: None, set_scholar=False,
    article_rules=None, bibtex_rules=None,
    add_citation=True
):
    """Check if there is anything to add for this info"""
    convert = ConvertDict(article_rules or config.ARTICLE_TO_INFO)
    info = convert.run(info, article=article)
    nwork = consume(info, "_nwork")
    should_add = {
        "add": False,
        "citation": citation,
        "set": {},
        "backward": backward,
    }

    if not nwork or (not citation and add_citation):
        should_add["add"] = True
        should_add["citation"] = citation
        return should_add, nwork, info
    
    changes = set_by_info(nwork, info, set_scholar=set_scholar, rules=bibtex_rules or config.BIBTEX_TO_INFO)
    should_add["set"] = changes["set"]

    if should_add["set"]:
        should_add["add"] = True

    if add_citation:
        local_citation = find_local_citation(
            nwork, citation, backward,
            citation_file=citation_file, warning=warning
        )
        if local_citation:
            should_add["citation"] = None
        else:
            should_add["add"] = True

    return should_add, nwork, info def set_by_info(work, info, set_scholar=True, rules=None):
    """Find attributes that should be modified in a work object to make it match an info object"""
    
    rules = rules or config.BIBTEX_TO_INFO
    rules.get("<set_before>", lambda x, y: None)(work, info)
    work_keys = {k for k in work.__dict__.keys() if not k.startswith("__")} - rules["<set_ignore_keys>"]
    meta_keys = info.keys() - rules.get("<set_ignore_keys>", set())
    show_result = OrderedDict(
        (key, None) for key in rules.get("<set_order>", [])
    )
    set_result = {}
    shared = meta_keys & work_keys
    for key in shared:
        value = info[key]
        add = False
        if key in rules.get("<set_ignore_but_show>", set()):
            add = True
        elif getattr(work, key) != value and key not in getattr(work, rules.get("<set_ignore_attr>", "ignoreattrs"), set()):
            add = True
            set_result[key] = (value, getattr(work, key))
        elif key in rules.get("<set_always_show>", set()):
            add = True
        if add:
            show_result[key] = (value, getattr(work, key))

    for key in meta_keys - work_keys:
        value = info[key]
        set_result[key] = (value, None)
        show_result[key] = (value, "")

    if set_scholar and rules.get("<scholar_ok>") and not hasattr(work, rules["<scholar_ok>"]):
        set_result[rules["<scholar_ok>"]] = (True, None)

    result = {
        "show": show_result,
        "set": set_result,
    }
    if "<pos_diff>" in rules:
        rules["<pos_diff>"](work, info, result)
    return result def changes_dict_to_set_attribute(metakey, changes_dict, end=";"):
    """Convert dictionart of changes to set_attribute instructions"""
    result = []
    for key, (value, old) in changes_dict.items():
        result.append("set_attribute({!r}, {!r}, {!r}, old={!r})".format(metakey, key, value, old))
    return "\n".join(result) + end class ScholarConf(object):
    """Helper class for global settings."""

    VERSION = '2.10'
    LOG_LEVEL = 3
    MAX_PAGE_RESULTS = 20 # Current maximum for per-page results
    SCHOLAR_SITE = 'http://scholar.google.com'

    # USER_AGENT = 'Mozilla/5.0 (X11; U; FreeBSD i386; en-US; rv:1.9.2.9) Gecko/20100913 Firefox/3.6.9'
    # Let's update at this point (3/14):
    USER_AGENT = 'Mozilla/5.0 (X11; Linux x86_64; rv:27.0) Gecko/20100101 Firefox/27.0'

    # If set, we will use this file to read/save cookies to enable
    # cookie use across sessions.
    COOKIE_JAR_FILE = None def old_form_to_new(show_deprecation=False):
    from . import config
    if show_deprecation:
        warnings.warn("""
            The following attributes have been deprecated:
                
            * config.FORM_BUTTONS
            
            * config.FORM_TEXT_FIELDS


            Please, consider upgrading to the new FORM definition format:

                config.FORM = {
                    'widgets': ...,
                    'order': ...,
                    'events': ...,
                }


            For obtaining an equivalent form definition, run:

                from snowballing.rules import old_form_to_new
                print(old_form_to_new())
        
        """)
    order = []
    widgets = []

    base = [
        [
            "dropdown", "Type", "work_type", config.DEFAULT_CLASS, 
            [tup[0] for tup in config.CLASSES]
        ],
        ["toggle", "File", "file_field", False],
        ["text", "Due", "due", ""],
        ["text", "Place", "place", ""],
        ["text", "Year", "year", ""],
        ["text", "Prefix Var", "prefix", ""],
        ["text", "PDFPage", "pdfpage", ""],
    ]

    events = [
        ["due", "observe", [
            ["if",
                ["and",
                    ["!=", ":due", ""],
                    ["==", ":work_type", "Work"]
                ],
                {
                    "work_type": "WorkUnrelated",
                },
                ["if",
                    ["and",
                        ["==", ":due", ""],
                        ["==", ":work_type", "WorkUnrelated"]
                    ],
                    {
                        "work_type": "Work"
                    },
                    []

                ]
            ]

        ]],
        ["place", "observe", [
            ["if",
                ["and",
                    ["==", ":place", "Lang"],
                    ["==", ":work_type", "Work"]
                ],
                {
                    "work_type": "WorkLang"
                },
                []
            ]
        ]],
    ]

    widgets = base[:]

    current_button = 0
    for row in config.FORM_BUTTONS:
        widget_row = []
        for button in row:
            current_button += 1
            button_id = "_b{}".format(current_button)
            widgets.append(["button", button[0], button_id])
            events.append([button_id, "click", {
                k.replace("_widget", ""): v for k, v in button[1].items()
            }])
            widget_row.append(button_id)
        order.append(widget_row)

    for tup in config.FORM_TEXT_FIELDS:
        new_tup = ["text", tup[0], tup[1], tup[2]]
        widgets.append(new_tup)
        base.append(new_tup)

    iterable = iter(base)
    for w1, w2 in zip_longest(iterable, iterable):
        order.append([w1[2]] + ([w2[2]] if w2 else []))

    return {
        "widgets": widgets,
        "order": order,
        "events": events,
    } def display_list(elements):
    """Display list of elements using IPython display"""
    for disp in elements or []:
        display(disp) def parse_bibtex(bib):
    """Parse BibTex and return list of entries"""
    parser = BibTexParser(common_strings=True)
    parser.customization = convert_to_unicode
    return bibtexparser.loads(bib, parser=parser).entries JOHN_SNOW_VERSION = "1.0.0"
DATABASE_DIR = Path.home() / "database"
TEXT_EDITOR = None
LINE_PARAMS = None
WEB_DRIVER = lambda: webdriver.Chrome()
RUN_WIDGET = True 
PDF_EXTRACTOR = None
PDF_EXTRACTOR = "java -jar refExtractor.jar full {path}"
CLASSES = [
    ("Work", "work", "display", "#FFD86E", "black"),
    ("WorkSnowball", "snowball", "display", "#6DCE9E", "white"),
    ("WorkOk", "ok", "display", "#68BDF6", "white"),
    ("WorkUnrelated", "unrelated", "hide", "#DE9BF9", "white"),
    ("WorkNoFile", "nofile", "hide", "#A5ABB6", "white"),
    ("WorkLang", "lang", "hide", "#ff8040", "white"),
    ("Site", "site", "hide", "#000080", "white"),
    ("Email", "site", "hide", "#000080", "white"),
]
DEFAULT_CLASS = "Work"
SIMILARITY_RATIO = 0.8
CHECK_DEPRECATION = True
DEBUG_FIELDS = True
WORK_FIELDS = [
    "entrytype", "year", "name", "authors", "place",
    "booktitle", "bookauthors", "edition", "available",
    "volume", "number", "section", "pp", "article",
    "doi", "isbn",  "proceedings", "issn",
    "organization", "publisher", "school", "institution", "track",
    "ref", "local", "editors", "awards",
    "special", "website", "link", "scholar", "shorttitle", "address",
]
BIBTEX_IGNORE_FIELDS = [
    "excerpt", "month", "bookname", "url", "ID",

    # Tool
    "_.*", "force_.*", "file.*", "category", "alias", "aliases", "scholar_ok",
    "scholar", "cluster_id", "scholar_id", "display", "metakey", "due", "_tyear",
    "citation_file", "notes", "tracking", "snowball", "request", "_draw",
    "may_be_related_to", "note",
]
BIBTEX_TO_INFO = {
    "<before>": [
        ("place1", ""),
        ("year", lambda x: int(
            (x["year"][:4] or 0) if "[in press]" in x.get("year", "")
            else x.get("year", 0)
        )),
    ],
    "<middle>": [
        ("place", lambda old, new: _place_value(new["place1"])),
        ("display", lambda old, new: last_name_first_author(new["authors"])),
        ("pyref", lambda old, new: info_to_pyref(new)),
    ],
    "<after>": [
        ("name", lambda old, new, current: new["name"].replace("{", "").replace("}", ""))
    ],
    "title": ["name"],
    "author": ["authors"],
    "address": ["local"],
    "publisher": ["organization"],
    "pages": ["pp"],
    "ENTRYTYPE": ["entrytype"],
    "journal": ["place1"],
    "booktitle": ["place1"],
    "year": [
        ("note", lambda x: "in press" if "[in press]" in x.get("year", "") else None)
    ],
    "<article>": [
        ("excerpt", lambda article, new: article.attrs["excerpt"][0]),
        ("cluster_id", lambda article, new: article.attrs["cluster_id"][0]),
        ("scholar", lambda article, new: article.attrs["url_citations"][0]),
        ("div", lambda article, new: article.div),
    ],
    "<set_ignore_keys>": {"excerpt", "div", "pyref", "display", "place", "_work_type", "_ref"},
    "<set_ignore_but_show>": {"place1", "year"},
    "<set_always_show>": {"name", "authors", "entrytype", "place1", "year"},
    "<set_order>": ["name", "authors", "entrytype", "place1", "year"],
    "<scholar_ok>": "scholar_ok",
    "<set_before>": lambda work, info: [
        setattr(work, "entrytype", work.place.type)
            if not hasattr(work, "entrytype")
            and hasattr(work, "place") else 
            None,
        setattr(info, "place1", info["place"])
            if "place" in info
            and not "place1" in info else
            None,
        setattr(work, "place1", "{} ({})".format(work.place.name, work.place.acronym))
            if hasattr(work, "place") else
            None,
    ],
    "<pos_diff>": _pos_diff
}
BIBTEX_TO_INFO_WITH_TYPE = (
    ModifyRules(BIBTEX_TO_INFO, "with_type")
    .append("<before>", ("_work_type", "Work"))
).rules
BIBTEX_TO_INFO_IGNORE_SET_ID = (
    ModifyRules(BIBTEX_TO_INFO, "ignore_set")
    .add("<set_ignore_keys>", "ID")
).rules
ARTICLE_TO_INFO = {
    "<after>": [
        (None, lambda old, new, current:
            [
                setitem(new, "pyref", 'Site("{name}", "{url}")'.format(**new)),
                setitem(new, "_nwork", Site(new["name"], new["url"]))
            ] if new.get("_work_type") == "Site" else
            [
                setitem(new, "_nwork", work_by_varname(new["pyref"])),
                setitem(new, "name", new["_nwork"].name),
                setitem(new, "place", new["_nwork"].place.name),
            ] if new.get("_work_type") == "Ref" else 
            [
                setitem(new, "place", _place_value(new["place1"])) if "place" not in new else None,
                setitem(new, "display", last_name_first_author(new["authors"])) if "display" not in new else None,
                setitem(new, "pyref", info_to_pyref(new)) if "pyref" not in new else None,
                setitem(new, "_nwork", find_work_by_info(new, set())),
            ]
        
        )
    ],
    "div": [None],
    "citation_id": [None],
    "<article>": [
        (None, lambda article, new: 
            [
                setitem(article, "_show_site", True),
                setitem(article, "_ref", article.get("citation_id", "")),
            ] if new.get("_work_type") == "Site" else
            [
                setitem(article, "name", new.get("name")),
                setitem(article, "place", new.get("place")),
                setitem(article, "_ref", article.get("citation_id", "")),
            ] if new.get("_work_type") == "Ref" else [
                setitem(article, "_ref", article.get("citation_id", "")),
            ]
           
        ),
    ]
}
INFO_TO_INSERT = {
    "<before>": [
        ("work_type", "Work"), ("due", ""), ("related", ""),
        ("display", ""), ("authors", ""), ("the_place", ""), ("other", ""),
    ],
    "<middle>": [
        ("the_place", sequence([
            var_item("place", obj="new"),
            str_item("place1", obj="new"), 
        ])),
        ("other", lambda old, new, current: [
            "{}={!r},".format(key, consume(current, key)) # .replace('"', r'\"')
            for key in copy(current)
        ]),
        ("attributes", lambda old, new: "\n            ".join(remove_empty([
            new["due"], new["related"], new["display"],
            new["authors"], new["the_place"]
        ] + new["other"]))),
    ],
    "<result>": lambda old, new, current: textwrap.dedent("""
        {pyref} = DB({work_type}(
            {year}, "{name}",
            {attributes}
        ))""".format(**new)),
    "_work_type": ["work_type"],
    "year": ["year"],
    "name": ["name"],
    "authors": [("authors", str_item("authors"))],
    "display": [("display", str_item("display"))],
    "pyref": ["pyref"],
    "place": ["place"],
    "place1": ["place1"],
    "may_be_related_to": [("related", str_list("may_be_related_to"))],
    "due": [("due", str_item("due"))] ,
    "excerpt": [None],
    "_ref": [None],
}
FIND_INFO_WORK = {
    "<skip>": [
        ("_work_type", "Site"),
    ],
    "<ignore>": ["_year", "_work", "_key"],
    "<before>": [
        ("place", lambda old: 
            None if "entrytype" not in old else
            "Thesis" if old["entrytype"] in ("phdthesis", "mastersthesis") else
            "TechReport" if old["entrytype"] == "techreport" else
            "Book" if old["entrytype"] == "book" else
            None
        ), 
        ("_work", lambda old: work_by_varname(old["ID"]) if "ID" in old else None),
        ("_key", lambda old: old["ID"] if "ID" in old else None),
    ],
    "year": ["year", "_year"],
    "school": ["local"],
}
WORK_TO_BIBTEX = {
    "<before>": [
        ("ID", lambda work, new, current: (
            new["_name"] if new.get("_name", None) else
            work.metakey if hasattr(work, "metakey") else
            (lambda split: (split[0] if split else "a") + str(work.year))(work.authors.split())
        )),
    ],
    "<middle>": [
        (None, _work_to_bibtex_middle),
    ],
    "<ignore>": lambda: BIBTEX_IGNORE_FIELDS + ["place", "place1"],
    "<use>": lambda: WORK_FIELDS,
    "<result>": lambda work, new, current: (
        [
            consume(new, "_name"),
            consume(new, "_acronym"),
            homogenize_latex_encoding(new) if consume(new, "_homogeneize") else new,
        ][-1]
    ), 
    "name": ["title"],
    "authors": ["author"],
    "local": ["address"],
    "organization": ["publisher"],
    "pp": ["pages"],
    "entrytype": ["ENTRYTYPE"],
}
FORM = {
    # List of widgets
    #   Each widget has at least 3 fields
    #     Type (text, dropdown, toggle, button)
    #     Label
    #     Variable
    #   Some widget requ-ire extra fields
    #     text, dropdown, and toggle: 4th field indicates the value
    #     dropdown: 5th field is a list with options
    "widgets": [
        # Base
        [
            "dropdown", "Type", "work_type", DEFAULT_CLASS, 
            [tup[0] for tup in CLASSES]
        ],
        ["toggle", "File", "file_field", False],
        ["text", "Due", "due", ""],
        ["text", "Place", "place", ""],
        ["text", "Year", "year", ""],
        ["text", "Prefix Var", "prefix", ""],
        ["text", "PDFPage", "pdfpage", ""],
        ["text", "Related", "may_be_related_to", None],
        ["text", "Display", "display", None],
        ["text", "Link", "link", None],
        # Buttons
        ["button", "Ok", "_b1"],
        
    ],
    # List of events
    #   Each event has 3 fields
    #     Widget variable
    #     Listener (observe, click)
    #     Action. It can be:
    #       A dictionary indicating which values to chance
    #       A list with the action (first item is a str) and its parameters (other actions)
    #       A list of actions to run in sequence
    "events": [
        # Base
        ["due", "observe", [
            ["if",
                ["and",
                    ["!=", ":due", ""],
                    ["==", ":work_type", "Work"]
                ],
                {
                    "work_type": "WorkUnrelated",
                },
                ["if",
                    ["and",
                        ["==", ":due", ""],
                        ["==", ":work_type", "WorkUnrelated"]
                    ],
                    {
                        "work_type": "Work"
                    },
                    []

                ]
            ]

        ]],
        ["place", "observe", [
            ["if",
                ["and",
                    ["==", ":place", "Lang"],
                    ["==", ":work_type", "Work"]
                ],
                {
                    "work_type": "WorkLang"
                },
                []
            ]
        ]],
        # Buttons
        ["_b1", "click", [
            {
                "work_type": "WorkOk",
                "file_field": True,
            },
            ["reload"],
        ]],
    ],
    # Exhibition of the widges.
    #   Each list indicates a row with other widgets
    #   I suggest using no more than 4 buttons in a row 
    #   and no more than 2 inputs in a row
    "order": [
        ["_b1"],
        ["work_type", "file_field"],
        ["due", "place"],
        ["year", "prefix"],
        ["may_be_related_to", "display"],
        ["summary", "link"],
    ],
    # Show operation
    #   Use the same "DSL" as the events to update info object.
    "show": [
        ["if", ["==", ".place", "Lang"],
            {"work_type": "WorkLang"},
            []
        ],
        ["update_info", "due", ":due", None, ""],
        ["update_info", "place", ":place", None, ""],
        ["update_info", "_work_type", ":work_type", None, "Work"],
        ["if", 
            ["or",
                ["update_info", "year", ":year", None, ""],
                ["update_info", "display", ":prefix", None, ""],
            ],
            ["pyref"],
            []
        ],
        ["update_info", "file", ":file_field", ["+", ".pyref", r"\.pdf"], False],
        ["update_info", "file", ":pdfpage", ["+", ".file", "#page=", ":pdfpage"], ""],
        ["update_info", "may_be_related_to", ":may_be_related_to", None, ""],
        ["update_info", "display", ":display", None, ""],
        ["update_info", "link", ":link", None, ""],
    ]
}
APPROACH_FORCE_PREFIX = "force_"
APPROACH_RELATED_CATEGORY = "Related"
MODULES = {
    "places": None,
    "work": None,
    "citations": None,
    "groups": None,
}
ATTR = {
    # Work
    "category": "category",
    "metakey": "metakey", 
    "pyref": "pyref", 
    "year": "year", 
    "name": "name",
    "site_link": "link",
    "email_authors": "authors",
    "citation_file": "citation_file",


    # Approach
    "approach_dont_cite": "dont_cite",
    "approach_work": "work",
}
SCHOLAR_MAP = {
    "scholar_id": "scholar_id",
    "cluster_id": "cluster_id",
    "scholar": "scholar",
    "scholar_ok": "scholar_ok",
}
def _place_value(place1):
def _pos_diff(work, info, result):
    def delkey(key, from_set=True):
def _work_to_bibtex_middle(work, new, current):
def convert_citation_text_lines_to_info(text):
def info_to_pyref(info):
def set_info_letter(info, letter):
def view_func(work):
def view_autoopen(work):
def check_insertion(nwork, info, citation_var, citation_file, should_add, ref=""):
def check_load(work, varname, warning=lambda x: print(x)):
def display_article(article):
def query_str(work):
def work_post_init(work):
def work_eq(first, second):
def work_hash(work):
def info_work_match(info, work):
def work_display(work):
def get_work_aliases(work):
def get_alias_year(work, alias):
def graph_place_text(work):
def graph_place_tooltip(work):
def work_link(work):
def work_tooltip(work):
def citation_tooltip(citation):
def approach_ids_from_work(approach, works):
def approach_display(approach):
import re
import json
import traceback
import sys
import os
import subprocess
from copy import copy
from urllib.parse import urlparse, parse_qs
from IPython.display import clear_output, display, HTML, Javascript
from ipywidgets import HBox, VBox, IntSlider, ToggleButton, Text, Layout
from ipywidgets import Dropdown, Button, Tab, Label, Textarea, Output, FileUpload
from .collection_helpers import oset, dset, oget, consume
from .jupyter_utils import display_cell
from .operations import bibtex_to_info, reload, citation_text, work_by_varname
from .operations import extract_info, create_info_code, should_add_info
from .operations import set_by_info, changes_dict_to_set_attribute
from .scholar import ScholarConf
from .rules import old_form_to_new
from .utils import display_list, parse_bibtex
from . import config
        from ipywidgets import Button
        from .selenium_scholar import URLQuery
            from .selenium_scholar import SearchScholarQuery
        from snowballing.selenium_scholar import URLQuery
        from .selenium_scholar import SearchScholarQuery
            min(self.selector_widget.value + 1, len(self.articles)),
            len(self.articles)
        )
        for key, widget in self.widgets.items():
            if key in self.widgets_empty:
                if isinstance(widget, ToggleButton):
                    widget.value = self.widgets_empty[key] or False 
                else:   
                    widget.value = self.widgets_empty[key] or ""

    def next_article(self, b=None):
        """Next article click event"""
        self.selector_widget.value = min(self.selector_widget.value + 1, self.selector_widget.max)
        self.erase_article_form()
        self.show(clear=True)

    def previous_article(self, b=None):
        """Previous article click event"""
        self.selector_widget.value = max(self.selector_widget.value - 1, self.selector_widget.min)
        self.erase_article_form()
        self.show(clear=True)

    def valid_articles(self, articles, show=False):
        """Generate valid articles"""
        if not articles:
            return
        for article in articles:
            should, nwork, info = should_add_info(
                article, self.work, article=article,
                backward=self.backward,
                citation_file=self.citation_file if self.force_citation_file else None,
                warning=lambda x: self.to_display.append(x)
            )
            if should["add"]:
                yield article, nwork, info, should

    def clear(self):
        """Clear cell and output"""
        if self.disable_show:
            return
        self.to_display = []
        self.runner_widget.clear()
        self.output_widget.clear_output()

    def update_info(self, info, field, widget, value=None, default=""):
        """Update info according to widget"""
        if widget.value != default:
            info[field] = widget.value if value is None else value
        return bool(widget.value)

    def show_article(self, article, nwork, info, should):
        """Display article"""
        result = create_info_code(
            nwork, info,
            self.citation_var, self.citation_file, should,
            ref=article.get("_ref", "")
        )
        self.runner_widget.set_code(result["code"])
        self.output_widget.clear_output()
        with self.output_widget:
            if self.to_display:
                display("\n".join(self.to_display))
            display_list(config.display_article(article))
            for key, value in result["extra"].items():
                display(HTML("<label>{}</label><input value='{}' style='width: 100%'></input>".format(key, value)))
        self.to_display = []

    def show(self, b=None, clear=True):
        """Generic display"""
        _up = self.update_info
        reload()
        self.next_article_widget.disabled = self.selector_widget.value == self.selector_widget.max
        self.previous_article_widget.disabled = self.selector_widget.value == 0
        if clear:
            self.clear()
        if self.disable_show or not self.articles:
            return
        article, _, _, _ = self.articles[self.selector_widget.value]
        with self.output_widget:
            display_list(config.display_article(article))
        for article, nwork, info, should in self.valid_articles([article], show=True):
            runner = EventRunner(self, info=info)
            runner.execute(self.show_event)
            self.show_article(article, nwork, info, should)

    def browser(self):
        """Widget visualization"""
        with self.output_widget:
            print("Press 'Reload Article'")
        return self.view

    def _ipython_display_(self):
        """ Displays widget """
        with self.output_widget:
            print("Press 'Reload Article'")
        display(self.view)


class BackwardSnowballing(ArticleNavigator):
    """Navigate on article list for insertion with backward citation"""

    def __init__(self, citation_var, citation_file=None, articles=None, force_citation_file=True):
        work = work_by_varname(citation_var)
        citation_file = citation_file or oget(work, "citation_file", citation_var)
        super(BackwardSnowballing, self).__init__(
            citation_var=citation_var,
            citation_file=citation_file,
            articles=articles,
            backward=True,
            force_citation_file=force_citation_file
        )


class ForwardSnowballing:
    """Navigate on article list for insertion with forward citation"""

    def __init__(self, querier, citation_var, citation_file=None, debug=False, start=None, load=True):
        reload()
        self.querier = querier
        work = work_by_varname(citation_var)

        citation_file = citation_file or oget(work, "citation_file", citation_var)