Exemplo n.º 1
0
Arquivo: main.py Projeto: devolt5/ntg
def leitzeile_json(passage_or_id):
    """Endpoint.  Serve the leitzeile for the verse containing passage_or_id. """

    auth()

    with current_app.config.dba.engine.begin() as conn:
        passage = Passage(conn, passage_or_id)
        verse_start = (passage.start // 1000) * 1000
        verse_end = verse_start + 999

        res = execute(
            conn, """
        SELECT l.begadr, l.endadr, l.lemma, ARRAY_AGG (p.pass_id)
        FROM nestle l
          LEFT JOIN passages p ON (p.passage @> l.passage)
        WHERE int4range (:start, :end + 1) @> l.passage
        GROUP BY l.begadr, l.endadr, l.lemma

        UNION -- get the insertions

        SELECT p.begadr, p.endadr, '', ARRAY_AGG (p.pass_id)
        FROM passages_view p
        WHERE int4range (:start, :end + 1) @> p.passage AND (begadr % 2) = 1
        GROUP BY p.begadr, p.endadr

        ORDER BY begadr, endadr DESC
        """, dict(parameters, start=verse_start, end=verse_end))

        Leitzeile = collections.namedtuple('Leitzeile',
                                           'begadr, endadr, lemma, pass_ids')
        leitzeile = [Leitzeile._make(r)._asdict() for r in res]

        return make_json_response(leitzeile)
Exemplo n.º 2
0
def stemma(passage_or_id):
    """Serve a local stemma in dot format.

    A local stemma is a DAG (directed acyclic graph).  The layout of the DAG is
    precomputed on the server using GraphViz.  GraphViz adds a precomputed
    position to each node and a precomputed bezier path to each edge.

    N.B. I also considered client-side layout of DAGs, but found only 2 viable
    libraries:

    - dagre.  Javascript clone of GraphViz.  Unmaintained.  Buggy.  Does not
      work well with require.js.

    - viz.js.  GraphViz cross-compiled to Javascript with Emscripten.  Huge.
      Promising but still early days.

    Both libraries have their drawbacks so the easiest way out was to precompute
    the layout on the server.

    """

    width = float(request.args.get('width') or 0.0)
    fontsize = float(request.args.get('fontsize') or 10.0)

    with current_app.config.dba.engine.begin() as conn:
        passage = Passage(conn, passage_or_id)
        graph = db_tools.local_stemma_to_nx(conn, passage.pass_id,
                                            user_can_write(current_app))
        dot = helpers.nx_to_dot(graph, width, fontsize, nodesep=0.2)
        return dot
Exemplo n.º 3
0
def notes_txt(passage_or_id):
    """Read or write the editor notes for a passage

    """

    private_auth()

    with current_app.config.dba.engine.begin() as conn:
        passage = Passage(conn, passage_or_id)

        if request.method == 'PUT':

            edit_auth()
            json = request.get_json()

            res = execute(
                conn, """
            SET LOCAL ntg.user_id = :user_id;
            """, dict(parameters, user_id=flask_login.current_user.id))

            # check for edit conflicts
            res = execute(
                conn, """
            SELECT * FROM notes
            WHERE pass_id = :pass_id AND note != :old_note
            """,
                dict(parameters,
                     pass_id=passage.pass_id,
                     old_note=json['original']))
            for row in res:
                return make_json_response(
                    status=409,
                    message='Cannot save. The note was edited by another user.'
                )

            # save
            res = execute(
                conn, """
            INSERT INTO notes AS n (pass_id, note)
            VALUES (:pass_id, :note)
            ON CONFLICT (pass_id) DO
            UPDATE
            SET note = :note
            WHERE n.pass_id = EXCLUDED.pass_id
            """, dict(parameters, pass_id=passage.pass_id, note=json['note']))

            return make_json_response(message='Note saved.')

        res = execute(
            conn, """
        SELECT note
        FROM notes
        WHERE pass_id = :pass_id
        """, dict(parameters, pass_id=passage.pass_id))

        if res.rowcount > 0:
            return make_text_response(res.fetchone()[0])
        return make_text_response('')
Exemplo n.º 4
0
Arquivo: main.py Projeto: devolt5/ntg
def cliques_json(passage_or_id):
    """ Endpoint.  Serve all cliques found in a passage.

    :param string passage_or_id: The passage id.

    """

    auth()

    with current_app.config.dba.engine.begin() as conn:
        passage = Passage(conn, passage_or_id)
        return make_json_response(passage.cliques())
Exemplo n.º 5
0
Arquivo: main.py Projeto: devolt5/ntg
def passage_json(passage_or_id=None):
    """Endpoint.  Serve information about a passage.

    Return information about a passage or navigate to it.

    :param string passage_or_id: The passage id.
    :param string siglum:        The siglum of the book to navigate to.
    :param string chapter:       The chapter to navigate to.
    :param string verse:         The verse to navigate to.
    :param string word:          The word (range) to navigate to.
    :param string button:        The button pressed.

    """

    auth()

    passage_or_id = request.args.get('pass_id') or passage_or_id or '0'

    siglum = request.args.get('siglum')
    chapter = request.args.get('chapter')
    verse = request.args.get('verse')
    word = request.args.get('word')
    button = request.args.get('button')

    with current_app.config.dba.engine.begin() as conn:
        if siglum and chapter and verse and word and button == 'Go':
            parsed_passage = Passage.parse("%s %s:%s/%s" %
                                           (siglum, chapter, verse, word))
            passage = Passage(conn, parsed_passage)
            return make_json_response(passage.to_json())

        if button in ('-1', '1'):
            passage = Passage(conn, passage_or_id)
            passage = Passage(conn, int(passage.pass_id) + int(button))
            return make_json_response(passage.to_json())

        passage = Passage(conn, passage_or_id)
        return cache(make_json_response(passage.to_json()))
Exemplo n.º 6
0
Arquivo: main.py Projeto: devolt5/ntg
def attesting_csv(passage_or_id, labez):
    """ Serve all relatives of all mss. attesting labez at passage. """

    auth()

    with current_app.config.dba.engine.begin() as conn:
        passage = Passage(conn, passage_or_id)

        res = execute(
            conn, """
        SELECT ms_id, hs, hsnr
        FROM apparatus_view
        WHERE pass_id = :pass_id AND labez = :labez
        ORDER BY hsnr
        """, dict(parameters, pass_id=passage.pass_id, labez=labez))

        Attesting = collections.namedtuple('Attesting', 'ms_id hs hsnr')

        return csvify(Attesting._fields, list(map(Attesting._make, res)))
Exemplo n.º 7
0
Arquivo: main.py Projeto: devolt5/ntg
def apparatus_json(passage_or_id):
    """ The contents of the apparatus table. """

    auth()

    with current_app.config.dba.engine.begin() as conn:
        passage = Passage(conn, passage_or_id)

        # list of labez => lesart
        res = execute(
            conn, """
        SELECT labez, reading (labez, lesart)
        FROM readings
        WHERE pass_id = :pass_id
        ORDER BY labez
        """, dict(parameters, pass_id=passage.pass_id))

        Readings = collections.namedtuple('Readings', 'labez lesart')
        readings = [Readings._make(r)._asdict() for r in res]

        # list of labez_clique => manuscripts
        res = execute(
            conn, """
        SELECT labez, clique, labez_clique, labezsuf, reading (labez, lesart), ms_id, hs, hsnr, certainty
        FROM apparatus_view_agg
        WHERE pass_id = :pass_id
        ORDER BY hsnr, labez, clique
        """, dict(parameters, pass_id=passage.pass_id))

        Manuscripts = collections.namedtuple(
            'Manuscripts',
            'labez clique labez_clique labezsuf lesart ms_id hs hsnr certainty'
        )
        manuscripts = [Manuscripts._make(r)._asdict() for r in res]

        return make_json_response({
            'readings': readings,
            'manuscripts': manuscripts,
        })

    return 'Error'
Exemplo n.º 8
0
Arquivo: main.py Projeto: devolt5/ntg
def attestation_json(passage_or_id):
    """Answer with a list of the attestations of all manuscripts at one specified
    passage."""

    auth()

    with current_app.config.dba.engine.begin() as conn:
        passage = Passage(conn, passage_or_id)

        res = execute(
            conn, """
        SELECT ms_id, labez
        FROM apparatus
        WHERE pass_id = :pass_id
        ORDER BY ms_id
        """, dict(parameters, pass_id=passage.pass_id))

        attestations = {}
        for row in res:
            ms_id, labez = row
            attestations[str(ms_id)] = labez

        return make_json_response({'attestations': attestations})
Exemplo n.º 9
0
Arquivo: main.py Projeto: devolt5/ntg
def relatives_csv(passage_or_id, hs_hsnr_id):
    """Output a table of the nearest relatives of a manuscript.

    Output a table of the nearest relatives/ancestors/descendants of a
    manuscript and what they attest.

    """

    auth()

    type_ = request.args.get('type') or 'rel'
    limit = int(request.args.get('limit') or 0)
    labez = request.args.get('labez') or 'all'
    mode = request.args.get('mode') or 'sim'
    include = request.args.getlist('include[]') or []
    fragments = request.args.getlist('fragments[]') or []

    view = 'affinity_view' if mode == 'rec' else 'affinity_p_view'

    where = ''
    if type_ == 'anc':
        where = ' AND older < newer'
    if type_ == 'des':
        where = ' AND older >= newer'

    if labez == 'all':
        where += " AND labez !~ '^z'"
    elif labez == 'all+lac':
        pass
    else:
        where += " AND labez = '%s'" % labez

    if 'fragments' in fragments:
        frag_where = ''
    else:
        frag_where = 'AND aff.common > aff.ms1_length / 2'

    limit = '' if limit == 0 else ' LIMIT %d' % limit

    with current_app.config.dba.engine.begin() as conn:

        passage = Passage(conn, passage_or_id)
        ms = Manuscript(conn, hs_hsnr_id)
        rg_id = passage.request_rg_id(request)

        exclude = get_excluded_ms_ids(conn, include)

        # Get the X most similar manuscripts and their attestations
        res = execute(
            conn, """
        /* get the LIMIT closest ancestors for this node */
        WITH ranks AS (
          SELECT ms_id1, ms_id2,
            rank () OVER (ORDER BY affinity DESC, common, older, newer DESC, ms_id2) AS rank,
            affinity
          FROM {view} aff
          WHERE ms_id1 = :ms_id1 AND aff.rg_id = :rg_id AND ms_id2 NOT IN :exclude
            AND newer > older {frag_where}
          ORDER BY affinity DESC
        )

        SELECT r.rank,
               aff.ms_id2 as ms_id,
               ms.hs,
               ms.hsnr,
               aff.ms2_length,
               aff.common,
               aff.equal,
               aff.older,
               aff.newer,
               aff.unclear,
               aff.common - aff.equal - aff.older - aff.newer - aff.unclear as norel,
               CASE WHEN aff.newer < aff.older THEN ''
                    WHEN aff.newer = aff.older THEN '-'
                    ELSE '>'
               END as direction,
               aff.affinity,
               a.labez,
               a.certainty
        FROM
          {view} aff
        JOIN apparatus_view_agg a
          ON aff.ms_id2 = a.ms_id
        JOIN manuscripts ms
          ON aff.ms_id2 = ms.ms_id
        LEFT JOIN ranks r
          ON r.ms_id2 = aff.ms_id2
        WHERE aff.ms_id2 NOT IN :exclude AND aff.ms_id1 = :ms_id1
              AND aff.rg_id = :rg_id AND aff.common > 0
              AND a.pass_id = :pass_id {where} {frag_where}
        ORDER BY affinity DESC, r.rank, newer DESC, older DESC, hsnr
        {limit}
        """,
            dict(parameters,
                 where=where,
                 frag_where=frag_where,
                 ms_id1=ms.ms_id,
                 hsnr=ms.hsnr,
                 pass_id=passage.pass_id,
                 rg_id=rg_id,
                 limit=limit,
                 view=view,
                 exclude=exclude))

        Relatives = collections.namedtuple(
            'Relatives',
            'rank ms_id hs hsnr length common equal older newer unclear norel direction affinity labez certainty'
        )
        return csvify(Relatives._fields, list(map(Relatives._make, res)))
Exemplo n.º 10
0
Arquivo: main.py Projeto: devolt5/ntg
def manuscript_full_json(passage_or_id, hs_hsnr_id):
    """Endpoint.  Serve information about a manuscript.

    :param string hs_hsnr_id: The hs, hsnr or id of the manuscript.

    """

    auth()

    hs_hsnr_id = request.args.get('ms_id') or hs_hsnr_id

    with current_app.config.dba.engine.begin() as conn:
        passage = Passage(conn, passage_or_id)
        ms = Manuscript(conn, hs_hsnr_id)
        rg_id = passage.request_rg_id(request)

        if ms.ms_id is None:
            return cache(
                make_json_response(None, 400,
                                   'Bad request: No such manuscript.'))

        json = ms.to_json()
        json['length'] = ms.get_length(rg_id)

        # Get the attestation(s) of the manuscript (may be uncertain eg. a/b/c)
        res = execute(
            conn, """
        SELECT labez, clique, labez_clique, certainty
        FROM apparatus_view_agg
        WHERE ms_id = :ms_id AND pass_id = :pass_id
        """, dict(parameters, ms_id=ms.ms_id, pass_id=passage.pass_id))

        row = res.fetchone()
        if row is not None:
            json['labez'], json['clique'], json['labez_clique'], json[
                'certainty'] = row

        # Get the affinity of the manuscript to all manuscripts
        res = execute(
            conn, """
        SELECT avg (a.affinity) as aa,
        percentile_cont(0.5) WITHIN GROUP (ORDER BY a.affinity) as ma
        FROM affinity a
        WHERE a.ms_id1 = :ms_id1 AND a.rg_id = :rg_id
        """, dict(parameters, ms_id1=ms.ms_id, rg_id=rg_id))

        json['aa'], json['ma'] = 0.0, 0.0
        row = res.fetchone()
        if row is not None:
            json['aa'], json['ma'] = row

        # Get the affinity of the manuscript to MT
        #
        # For a description of mt and mtp see the comment in
        # ActsMsListValPh3.pl and
        # http://intf.uni-muenster.de/cbgm/actsPh3/guide_en.html#Ancestors

        res = execute(
            conn, """
        SELECT a.affinity as mt, a.equal::float / c.length as mtp
        FROM affinity a
        JOIN ms_ranges c
          ON (a.ms_id1, a.rg_id) = (c.ms_id, c.rg_id)
        WHERE a.ms_id1 = :ms_id1 AND a.ms_id2 = 2 AND a.rg_id = :rg_id
        """, dict(parameters, ms_id1=ms.ms_id, rg_id=rg_id))

        json['mt'], json['mtp'] = 0.0, 0.0
        row = res.fetchone()
        if row is not None:
            json['mt'], json['mtp'] = row

        return cache(make_json_response(json))
Exemplo n.º 11
0
def stemma_edit(passage_or_id):
    """Edit a local stemma.

    Called from local-stemma.js (split, merge, move) and textflow.js (move-manuscripts).

    """

    edit_auth()

    args = request.get_json()

    action = args.get('action')

    if action not in ('add', 'del', 'split', 'merge', 'move',
                      'move-manuscripts'):
        raise EditError('Bad request')

    params = {}
    for n in 'labez_old labez_new source_labez'.split():
        if n in args:
            params[n] = args.get(n)
            if not RE_VALID_LABEZ.match(params[n]):
                raise EditError('Bad request')
    for n in 'clique_old clique_new source_clique'.split():
        if n in args:
            params[n] = args.get(n)
            if not RE_VALID_CLIQUE.match(params[n]):
                raise EditError('Bad request')

    def integrity_error(e):
        if 'ix_locstem_unique_original' in str(e):
            raise EditError(
                '''Only one original reading allowed. If you want to change the original
                reading, first remove the old original reading.<br/><br/>''' +
                str(e))
        if 'locstem_pkey' in str(e):
            raise EditError(
                '''This readings already dependes on that reading.<br/><br/>'''
                + str(e))
        if 'same_source' in str(e):
            raise EditError(
                '''A reading cannot be derived from the same reading.
                If you want to <b>merge two readings</b>, use shift + drag.''')
        raise EditError(str(e))

    with current_app.config.dba.engine.begin() as conn:
        passage = Passage(conn, passage_or_id)
        params['pass_id'] = passage.pass_id
        params['user_id'] = flask_login.current_user.id

        res = execute(
            conn, """
        SET LOCAL ntg.user_id = :user_id;
        """, dict(parameters, **params))

        if action == 'move':
            # reassign a source reading
            # there may be multiple existent assignments, there'll be only one left
            try:
                res = execute(
                    conn, """
                DELETE FROM locstem
                WHERE (pass_id, labez, clique) = (:pass_id, :labez_old, :clique_old);
                INSERT INTO locstem (pass_id, labez, clique, source_labez, source_clique)
                VALUES (:pass_id, :labez_old, :clique_old, :labez_new, :clique_new)
                """, dict(parameters, **params))
            except sqlalchemy.exc.IntegrityError as e:
                integrity_error(e)
            except sqlalchemy.exc.DatabaseError as e:
                raise EditError(str(e))

        if action == 'del':
            # remove a source reading
            try:
                # check if we are asked to remove the only link,
                # in that case reassign to 'unknown'
                res = execute(
                    conn, """
                SELECT pass_id
                FROM locstem
                WHERE (pass_id, labez, clique) = (:pass_id, :labez_old, :clique_old);
                """, dict(parameters, **params))

                tools.log(logging.INFO, 'Deleting: ' + str(params))

                if res.rowcount > 1:
                    res = execute(
                        conn, """
                    DELETE FROM locstem
                    WHERE (pass_id, labez, clique) = (:pass_id, :labez_old, :clique_old)
                      AND (source_labez, source_clique) = (:source_labez, :source_clique)
                    """, dict(parameters, **params))
                else:
                    res = execute(
                        conn, """
                    UPDATE locstem
                    SET (source_labez, source_clique) = ('?', '1')
                    WHERE (pass_id, labez, clique) = (:pass_id, :labez_old, :clique_old);
                    """, dict(parameters, **params))
            except sqlalchemy.exc.IntegrityError as e:
                integrity_error(e)
            except sqlalchemy.exc.DatabaseError as e:
                raise EditError(str(e))

        if action == 'add':
            # add a source reading
            try:
                res = execute(
                    conn, """
                INSERT INTO locstem (pass_id, labez, clique, source_labez, source_clique)
                VALUES (:pass_id, :labez_old, :clique_old, :labez_new, :clique_new)
                """, dict(parameters, **params))
            except sqlalchemy.exc.IntegrityError as e:
                integrity_error(e)
            except sqlalchemy.exc.DatabaseError as e:
                raise EditError(str(e))

        if action in ('add', 'del', 'move'):
            # test the still uncommitted changes

            graph = db_tools.local_stemma_to_nx(conn, passage.pass_id)

            # test: not a DAG
            if not nx.is_directed_acyclic_graph(graph):
                raise EditError('The new graph contains cycles.')
            # test: not connected
            graph.add_edge('*', '?')
            if not nx.is_weakly_connected(graph):
                raise EditError('The new graph is not connected.')

        elif action == 'split':
            # Get the lowest free integer for the new clique. See: #122
            res = execute(
                conn, """
            SELECT clique
            FROM  cliques
            WHERE pass_id = :pass_id AND labez = :labez_old
            """, dict(parameters, **params))

            taken = set([int(r[0]) for r in res])
            n = 1
            while n in taken:
                n += 1
            params['clique_next'] = str(n)

            # insert into cliques table
            res = execute(
                conn, """
            INSERT INTO cliques (pass_id, labez, clique)
            VALUES (:pass_id, :labez_old, :clique_next)
            """, dict(parameters, **params))

            # insert into locstem table with source = '?'
            res = execute(
                conn, """
            INSERT INTO locstem (pass_id, labez, clique, source_labez, source_clique)
            VALUES (:pass_id, :labez_old, :clique_next, '?', '1')
            """, dict(parameters, **params))

        elif action == 'merge':
            # merge two cliques (eg. b1, b2) into one clique (eg. b1)
            #
            # reassign manuscripts to merged clique
            res = execute(
                conn, """
            UPDATE ms_cliques
            SET clique = :clique_new
            WHERE (pass_id, labez, clique) = (:pass_id, :labez_old, :clique_old)
            """, dict(parameters, **params))

            # reassign sources to merged clique
            res = execute(
                conn, """
            UPDATE locstem
            SET source_clique = :clique_new
            WHERE (pass_id, source_labez, source_clique) = (:pass_id, :labez_old, :clique_old)
            """, dict(parameters, **params))

            # remove clique from locstem
            res = execute(
                conn, """
            DELETE FROM locstem
            WHERE (pass_id, labez, clique) = (:pass_id, :labez_old, :clique_old)
            """, dict(parameters, **params))

            # remove clique from cliques
            res = execute(
                conn, """
            DELETE FROM cliques
            WHERE (pass_id, labez, clique) = (:pass_id, :labez_old, :clique_old)
            """, dict(parameters, **params))

        elif action == 'move-manuscripts':
            # reassign a set of manuscripts to a new clique
            ms_ids = set(args.get('ms_ids') or [])

            res = execute(
                conn, """
            UPDATE apparatus_cliques_view
            SET clique = :clique_new
            WHERE (pass_id, labez, clique) = (:pass_id, :labez_old, :clique_old)
              AND ms_id IN :ms_ids
            """, dict(parameters, ms_ids=tuple(ms_ids), **params))

            tools.log(logging.INFO, 'Moved ms_ids: ' + str(ms_ids))

        # return the changed passage
        passage = Passage(conn, passage_or_id)
        return make_json_response(passage.to_json())

    raise EditError('Could not edit local stemma.')
Exemplo n.º 12
0
def textflow(passage_or_id):
    """ Output a stemma of manuscripts. """

    labez = request.args.get('labez') or ''
    hyp_a = request.args.get('hyp_a') or 'A'
    connectivity = int(request.args.get('connectivity') or 10)
    width = float(request.args.get('width') or 0.0)
    fontsize = float(request.args.get('fontsize') or 10.0)
    mode = request.args.get('mode') or 'sim'

    include = request.args.getlist('include[]') or []
    fragments = request.args.getlist('fragments[]') or []
    checks = request.args.getlist('checks[]') or []
    var_only = request.args.getlist('var_only[]') or []
    cliques = request.args.getlist('cliques[]') or []

    fragments = 'fragments' in fragments
    checks = 'checks' in checks
    var_only = 'var_only' in var_only  # Panel: Coherence at Variant Passages (GraphViz)
    cliques = 'cliques' in cliques  # consider or ignore cliques
    leaf_z = 'Z' in include  # show leaf z nodes in global textflow?

    view = 'affinity_view' if mode == 'rec' else 'affinity_p_view'

    global_textflow = not ((labez != '') or var_only)
    rank_z = False  # include z nodes in ranking?

    if global_textflow:
        connectivity = 1
        rank_z = True
    if connectivity == 21:
        connectivity = 9999

    labez_where = ''
    frag_where = ''
    z_where = ''

    if labez != '':
        labez_where = 'AND app.cbgm AND app.labez = :labez'
        if hyp_a != 'A':
            labez_where = 'AND app.cbgm AND (app.labez = :labez OR (app.ms_id = 1 AND :hyp_a = :labez))'

    if not fragments:
        frag_where = 'AND a.common > a.ms1_length / 2'

    if not rank_z:
        z_where = "AND app.labez !~ '^z' AND app.certainty = 1.0"

    group_field = 'labez_clique' if cliques else 'labez'

    with current_app.config.dba.engine.begin() as conn:
        passage = Passage(conn, passage_or_id)
        rg_id = passage.request_rg_id(request)

        exclude = get_excluded_ms_ids(conn, include)

        # nodes query
        #
        # get all nodes or all nodes (hypothetically) attesting labez

        res = execute(
            conn, """
        SELECT ms_id
        FROM apparatus app
        WHERE pass_id = :pass_id AND ms_id NOT IN :exclude {labez_where} {z_where}
        """,
            dict(parameters,
                 exclude=exclude,
                 pass_id=passage.pass_id,
                 labez=labez,
                 hyp_a=hyp_a,
                 labez_where=labez_where,
                 z_where=z_where))

        nodes = {row[0] for row in res}
        if not nodes:
            nodes = {-1}  # avoid SQL syntax error

        # rank query
        #
        # query to get the closest ancestors for every node with rank <= connectivity

        query = """
        SELECT ms_id1, ms_id2, rank
        FROM (
          SELECT ms_id1, ms_id2, rank () OVER (PARTITION BY ms_id1
             ORDER BY affinity DESC, common, older, newer DESC, ms_id2) AS rank
          FROM {view} a
          WHERE ms_id1 IN :nodes AND a.rg_id = :rg_id AND ms_id2 NOT IN :exclude
            AND newer > older {frag_where}
        ) AS r
        WHERE rank <= :connectivity
        ORDER BY rank
        """

        res = execute(
            conn, query,
            dict(parameters,
                 nodes=tuple(nodes),
                 exclude=exclude,
                 rg_id=rg_id,
                 pass_id=passage.pass_id,
                 view=view,
                 labez=labez,
                 connectivity=connectivity,
                 frag_where=frag_where,
                 hyp_a=hyp_a))

        Ranks = collections.namedtuple('Ranks', 'ms_id1 ms_id2 rank')
        ranks = list(map(Ranks._make, res))

        # Initially build an unconnected graph with one node for each
        # manuscript.  We will connect the nodes later.  Finally we will remove
        # unconnected nodes.

        graph = nx.DiGraph()

        dest_nodes = {r.ms_id1 for r in ranks}
        src_nodes = {r.ms_id2 for r in ranks}

        res = execute(
            conn, """
        SELECT ms.ms_id, ms.hs, ms.hsnr, a.labez, a.clique, a.labez_clique, a.certainty
        FROM apparatus_view_agg a
        JOIN manuscripts ms USING (ms_id)
        WHERE pass_id = :pass_id AND ms_id IN :ms_ids
        """,
            dict(parameters,
                 ms_ids=tuple(src_nodes | dest_nodes | nodes),
                 pass_id=passage.pass_id))

        Mss = collections.namedtuple(
            'Mss', 'ms_id hs hsnr labez clique labez_clique certainty')
        mss = list(map(Mss._make, res))

        for ms in mss:
            attrs = {}
            attrs['hs'] = ms.hs
            attrs['hsnr'] = ms.hsnr
            attrs[
                'labez'] = ms.labez if ms.certainty == 1.0 else 'zw ' + ms.labez
            attrs['clique'] = ms.clique
            attrs[
                'labez_clique'] = ms.labez_clique if ms.certainty == 1.0 else 'zw ' + ms.labez_clique
            attrs['ms_id'] = ms.ms_id
            attrs['label'] = ms.hs
            attrs['certainty'] = ms.certainty
            attrs['clickable'] = '1'
            if ms.ms_id == 1 and hyp_a != 'A':
                attrs['labez'] = hyp_a[0]
                attrs['clique'] = ''
                attrs['labez_clique'] = hyp_a[0]
            # FIXME: attrs['shape'] = SHAPES.get (attrs['labez'], SHAPES['a'])
            graph.add_node(ms.ms_id, **attrs)

        # Connect the nodes
        #
        # Step 1: If the node has internal parents, keep only the top-ranked
        # internal parent.
        #
        # Step 2: If the node has no internal parents, keep the top-ranked
        # parents for each external attestation.
        #
        # Assumption: ranks are sorted top-ranked first

        def is_z_node(n):
            labez = n['labez']
            cert = n['certainty']
            return (labez[0] == 'z') or (cert < 1.0)

        tags = set()
        for step in (1, 2):
            for r in ranks:
                a1 = graph.nodes[r.ms_id1]
                if not r.ms_id2 in graph.nodes:
                    continue
                a2 = graph.nodes[r.ms_id2]
                if not (global_textflow) and is_z_node(a2):
                    # disregard zz / zw
                    continue
                if step == 1 and a1[group_field] != a2[group_field]:
                    # differing attestations are handled in step 2
                    continue
                if r.ms_id1 in tags:
                    # an ancestor of this node that lays within the node's
                    # attestation was already seen.  we need not look into other
                    # attestations
                    continue
                if str(r.ms_id1) + a2[group_field] in tags:
                    # an ancestor of this node that lays within this attestation
                    # was already seen.  we need not look into further nodes
                    continue
                # add a new parent
                if r.rank > 1:
                    graph.add_edge(r.ms_id2,
                                   r.ms_id1,
                                   rank=r.rank,
                                   headlabel=r.rank)
                else:
                    graph.add_edge(r.ms_id2, r.ms_id1)

                if a1[group_field] == a2[group_field]:
                    # tag: has ancestor node within the same attestation
                    tags.add(r.ms_id1)
                else:
                    # tag: has ancestor node with this other attestation
                    tags.add(str(r.ms_id1) + a2[group_field])

        if not leaf_z:
            remove_z_leaves(graph)

        # the if clause fixes #83
        graph.remove_nodes_from([
            n for n in nx.isolates(graph) if graph.nodes[n]['labez'] != labez
        ])

        if var_only:
            # Panel: Coherence at Variant Passages (GraphViz)
            #
            # if one predecessor is within the same attestation then remove all
            # other predecessors that are not within the same attestation
            for n in graph:
                within = False
                attestation_n = graph.nodes[n][group_field]
                for p in graph.predecessors(n):
                    if graph.nodes[p][group_field] == attestation_n:
                        within = True
                        break
                if within:
                    for p in graph.predecessors(n):
                        if graph.nodes[p][group_field] != attestation_n:
                            graph.remove_edge(p, n)

            # remove edges between nodes within the same attestation
            for u, v in list(graph.edges()):
                if graph.nodes[u][group_field] == graph.nodes[v][group_field]:
                    graph.remove_edge(u, v)

            # remove now isolated nodes
            graph.remove_nodes_from(list(nx.isolates(graph)))

            # unconstrain backward edges (yields a better GraphViz layout)
            for u, v in graph.edges():
                if graph.nodes[u][group_field] > graph.nodes[v][group_field]:
                    graph.adj[u][v]['constraint'] = 'false'

        else:
            for n in graph:
                # Use a different label if the parent's labez_clique differs from this
                # node's labez_clique.
                pred = list(graph.predecessors(n))
                attrs = graph.nodes[n]
                if not pred:
                    attrs['label'] = "%s: %s" % (attrs['labez_clique'],
                                                 attrs['hs'])
                for p in pred:
                    if attrs['labez_clique'] != graph.nodes[p]['labez_clique']:
                        attrs['label'] = "%s: %s" % (attrs['labez_clique'],
                                                     attrs['hs'])
                        graph.adj[p][n]['style'] = 'dashed'

        if checks:
            for rank in congruence(conn, passage):
                try:
                    graph.adj[rank.ms_id1][rank.ms_id2]['style'] = 'bold'
                except KeyError:
                    pass

    if var_only:
        dot = helpers.nx_to_dot_subgraphs(graph, group_field, width, fontsize)
    else:
        dot = helpers.nx_to_dot(graph, width, fontsize)
    return dot
Exemplo n.º 13
0
def congruence_list_json(range_id):
    """ Endpoint: check the congruence """

    with current_app.config.dba.engine.begin() as conn:
        passage = Passage(conn, 1)
        return make_json_response(congruence_list(conn, passage, range_id))