From c6b9fac0f5b8bc079307018f48241f7912c3a41f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Cl=C3=A9rice?= Date: Wed, 30 Oct 2019 10:00:25 +0100 Subject: [PATCH 1/3] (Bookmark) Added Bookmark model, link, interface --- app/cli.py | 18 ++++++++++ app/main/views/corpus.py | 32 ++++++++++++++--- app/main/views/tokens.py | 3 +- app/models/__init__.py | 2 +- app/models/corpus.py | 46 ++++++++++++++++++++++++- app/statics/css/site.css | 24 ++++++++++++- app/templates/macros/nav_macros.html | 4 +-- app/templates/macros/tokens_macros.html | 5 +-- app/templates/main/tokens_correct.html | 2 +- 9 files changed, 123 insertions(+), 13 deletions(-) diff --git a/app/cli.py b/app/cli.py index 53393f31..28f93db0 100644 --- a/app/cli.py +++ b/app/cli.py @@ -238,9 +238,27 @@ def corpus_dump(corpus, path): )) click.echo("--- Allowed POS Values dumped") + @click.command("db-add", help="Small tool to add new table instead of migrating") + @click.argument("model") + def db_add_table(model): + import app.models as tables + Model = getattr(tables, model, None) + if Model: + with app.app_context(): + Model.__table__.create(db.session.bind, checkfirst=True) + else: + click.echo("Model not found.") + click.echo( + "Model available: " + ", ".join(sorted([ + x for x in dir(tables) + if x[0] != "_" and x[0].isupper() + ])) + ) + cli.add_command(db_create) cli.add_command(db_fixtures) cli.add_command(db_recreate) + cli.add_command(db_add_table) cli.add_command(run) cli.add_command(corpus_ingest) cli.add_command(corpus_import) diff --git a/app/main/views/corpus.py b/app/main/views/corpus.py index 7d1c374a..d4e9459a 100644 --- a/app/main/views/corpus.py +++ b/app/main/views/corpus.py @@ -5,7 +5,7 @@ from app import db -from app.models import CorpusUser, ControlLists, WordToken, ChangeRecord +from app.models import CorpusUser, ControlLists, WordToken, ChangeRecord, Bookmark from .utils import render_template_with_nav_info from app.utils.forms import create_input_format_convertion, read_input_tokens from .. import main @@ -13,7 +13,7 @@ from ...models import Corpus from ...utils.response import format_api_like_reply from ...errors import MissingTokenColumnValue, NoTokensInput -from .utils import requires_corpus_admin_access +from .utils import requires_corpus_admin_access, requires_corpus_access from ..forms import Delete AUTOCOMPLETE_LIMIT = 20 @@ -109,14 +109,13 @@ def error(): @main.route('/corpus/get/') @login_required +@requires_corpus_access("corpus_id") def corpus_get(corpus_id): """ Main page about the corpus :param corpus_id: ID of the corpus """ corpus = Corpus.query.get_or_404(corpus_id) - if not corpus.has_access(current_user): - abort(403) limit_corr = request.args.get("limit", 10) if isinstance(limit_corr, str): @@ -166,6 +165,31 @@ def corpus_get(corpus_id): lemma_cor=lemma_cor, pos_cor=pos_cor, morph_cor=morph_cor) +@main.route("/corpus//bookmark") +@login_required +@requires_corpus_access("corpus_id") +def corpus_bookmark(corpus_id): + token = request.args.get("token_id", None) + page = request.args.get("page", None) + if token and page: + bm = Bookmark.mark(corpus_id, current_user.id, token, page) + link = "{uri}#token_{token}_row".format( + uri=url_for("main.tokens_correct", corpus_id=corpus_id, page=bm.page), + token=bm.token_id + ) + else: + bm = Corpus.query.get_or_404(corpus_id).get_bookmark(current_user) + if bm: + link = "{uri}#token_{token}_row".format( + uri=url_for("main.tokens_correct", corpus_id=corpus_id, page=bm.page), + token=bm.token_id + ) + else: + flash("No bookmark found for this corpus on your account", category="warning") + link = url_for("main.tokens_correct", corpus_id=corpus_id) + return redirect(link) + + @main.route('/corpus//delete', methods=["GET", "POST"]) @requires_corpus_admin_access("corpus_id") def corpus_delete(corpus_id: int): diff --git a/app/main/views/tokens.py b/app/main/views/tokens.py index 06020509..4f997f07 100644 --- a/app/main/views/tokens.py +++ b/app/main/views/tokens.py @@ -6,7 +6,7 @@ from .utils import render_template_with_nav_info, request_wants_json, requires_corpus_access from .. import main -from ...models import WordToken, Corpus, ChangeRecord, TokenHistory +from ...models import WordToken, Corpus, ChangeRecord, TokenHistory, Bookmark from ...utils.forms import string_to_none, strip_or_none, column_search_filter, prepare_search_string from ...utils.pagination import int_or from ...utils.tsv import TSV_CONFIG @@ -22,6 +22,7 @@ def tokens_correct(corpus_id): :param corpus_id: Id of the corpus """ corpus = Corpus.query.filter_by(**{"id": corpus_id}).first() + current_user.bookmark: Bookmark = corpus.get_bookmark(current_user) tokens = corpus\ .get_tokens()\ diff --git a/app/models/__init__.py b/app/models/__init__.py index 9a2fd719..6443399e 100644 --- a/app/models/__init__.py +++ b/app/models/__init__.py @@ -1,3 +1,3 @@ -from .corpus import WordToken, ChangeRecord, Corpus, CorpusUser, TokenHistory +from .corpus import WordToken, ChangeRecord, Corpus, CorpusUser, TokenHistory, Bookmark from .user import User, AnonymousUser, Permission, Role from .control_lists import AllowedLemma, AllowedMorph, AllowedPOS, ControlListsUser, ControlLists, PublicationStatus diff --git a/app/models/corpus.py b/app/models/corpus.py index 2f8a170d..08d5fd03 100644 --- a/app/models/corpus.py +++ b/app/models/corpus.py @@ -2,7 +2,7 @@ import csv import io import enum -from typing import Iterable +from typing import Iterable, Optional # PIP Packages import unidecode from sqlalchemy.ext.associationproxy import association_proxy @@ -357,6 +357,14 @@ def update_allowed_values(self, allowed_type, allowed_values): cls.add_batch(allowed_values, self.id, _commit=True) return data + def get_bookmark(self, user: User) -> Optional["Bookmark"]: + """ Retrieve a bookmark for a given user. None if not found + """ + return Bookmark.query.filter(db.and_( + Bookmark.user_id == user.id, + Bookmark.corpus_id == self.id + )).first() + class WordToken(db.Model): """ A word token is a word from a corpus with primary annotation @@ -1133,3 +1141,39 @@ def apply_changes_to(self, user_id, token_ids): WordToken.update(**apply) changed.append(token) return changed + + +class Bookmark(db.Model): + """ + Association proxy that link users to corpora + :param corpus_id: a corpus ID + :param user_id: a user ID + """ + corpus_id = db.Column(db.Integer, db.ForeignKey("corpus.id", ondelete='CASCADE'), primary_key=True) + user_id = db.Column(db.Integer, db.ForeignKey(User.id), primary_key=True) + token_id = db.Column(db.Integer, db.ForeignKey(WordToken.id, ondelete="CASCADE"), primary_key=True) + page = db.Column(db.Integer, nullable=False) + + @staticmethod + def mark(corpus: int, user: int, token_id: int, page: int): + bm = Bookmark( + corpus_id=corpus, + user_id=user, + token_id=token_id, + page=page + ) + Bookmark.clear(corpus, user, _commit=False) + db.session.add(bm) + db.session.commit() + return bm + + @staticmethod + def clear(corpus: int, user: int, _commit: bool = False): + bm = Bookmark.query.filter(db.and_( + Bookmark.corpus_id == corpus, + Bookmark.user_id == user + )).first() + if bm: + db.session.delete(bm) + if _commit: + db.session.commit() diff --git a/app/statics/css/site.css b/app/statics/css/site.css index 41e5f467..1043df53 100644 --- a/app/statics/css/site.css +++ b/app/statics/css/site.css @@ -103,6 +103,27 @@ tr.history .badge { td.dd, th.dd { text-align: center; } + + tr.bookmark { + background-color: #accbf7; + position: relative; + transform: scale(1); + } + tr.bookmark td:first-child::before { + content: "\f02e"; + color:black; + left:-20px; + position:absolute; + top:0; + margin-right: .5em; + margin-top:0.5em; + font-family: FontAwesome; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + transform: translate(0, 0); + font-size: 1.2em; + line-height: 100%; + } /** https://codepen.io/sergiopedercini/pen/jmKdbj/?source=post_page--------------------------- 1 */ @@ -182,4 +203,5 @@ https://codepen.io/sergiopedercini/pen/jmKdbj/?source=post_page----------------- } #statistics > div { margin-bottom: 1em !important; - } \ No newline at end of file + } + diff --git a/app/templates/macros/nav_macros.html b/app/templates/macros/nav_macros.html index f99794c7..31fcd589 100644 --- a/app/templates/macros/nav_macros.html +++ b/app/templates/macros/nav_macros.html @@ -176,8 +176,8 @@ - diff --git a/app/templates/macros/tokens_macros.html b/app/templates/macros/tokens_macros.html index c9a22350..1f3fb172 100644 --- a/app/templates/macros/tokens_macros.html +++ b/app/templates/macros/tokens_macros.html @@ -3,7 +3,7 @@ {{ token.left_context }} <{{tag}}>{{ token.form }} {{ token.right_context }} {% endmacro -%} -{% macro table(tokens, corpus, changed, editable=False, tracking=False, checkbox=False, record=None, similar=False) %} +{% macro table(tokens, corpus, changed, editable=False, tracking=False, checkbox=False, record=None, similar=False, current_user=False)%} {% if editable %} {%- if checkbox %}
@@ -27,7 +27,7 @@

Save checked similar lemma

{% for token in tokens.items %} - + {{ token.order_id }} {{token.form}} {{token.lemma}} @@ -49,6 +49,7 @@

Save checked similar lemma

Edit the form Delete the row Add a token after this one + Set as bookmark
{%- endif -%} diff --git a/app/templates/main/tokens_correct.html b/app/templates/main/tokens_correct.html index eba856c6..decaba20 100644 --- a/app/templates/main/tokens_correct.html +++ b/app/templates/main/tokens_correct.html @@ -10,7 +10,7 @@

Corpus {{corpus.name}} - List of tokens

{{ nav.render_pagination(pagination=tokens, corpus_id=corpus.id, endpoint="main.tokens_correct") }} -{{ tokens_macros.table(tokens, corpus=corpus, changed=changed, editable=True, similar=True) }} +{{ tokens_macros.table(tokens, corpus=corpus, changed=changed, editable=True, similar=True, current_user=current_user) }} {{ nav.render_pagination(pagination=tokens, corpus_id=corpus.id, endpoint="main.tokens_correct") }} From eddad17846c204550a358e28d3d8471c47b8d02b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Cl=C3=A9rice?= Date: Wed, 30 Oct 2019 10:32:39 +0100 Subject: [PATCH 2/3] Python 3.6 --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index a54192cc..a7968f41 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,6 @@ language: python python: - - "3.5" + - "3.6" addons: chrome: stable apt: From a9bf75deabfab89ffbc98be7c89a4d0e4de64da6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Cl=C3=A9rice?= Date: Wed, 30 Oct 2019 11:08:25 +0100 Subject: [PATCH 3/3] (Bookmark) Added tests ! --- app/templates/macros/nav_macros.html | 2 +- tests/test_selenium/test_bookmark.py | 83 ++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+), 1 deletion(-) create mode 100644 tests/test_selenium/test_bookmark.py diff --git a/app/templates/macros/nav_macros.html b/app/templates/macros/nav_macros.html index 31fcd589..18585f6c 100644 --- a/app/templates/macros/nav_macros.html +++ b/app/templates/macros/nav_macros.html @@ -176,7 +176,7 @@ -