Skip to content

Commit

Permalink
Merge pull request #159 from hipster-philology/bookmark
Browse files Browse the repository at this point in the history
Bookmark
  • Loading branch information
MrGecko authored Apr 23, 2020
2 parents bff0ed7 + a9bf75d commit 424d57f
Show file tree
Hide file tree
Showing 11 changed files with 207 additions and 14 deletions.
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
language: python
python:
- "3.5"
- "3.6"
addons:
chrome: stable
apt:
Expand Down
18 changes: 18 additions & 0 deletions app/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
32 changes: 28 additions & 4 deletions app/main/views/corpus.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,15 @@


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
from ...utils.forms import strip_or_none
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
Expand Down Expand Up @@ -109,14 +109,13 @@ def error():

@main.route('/corpus/get/<int:corpus_id>')
@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):
Expand Down Expand Up @@ -166,6 +165,31 @@ def corpus_get(corpus_id):
lemma_cor=lemma_cor, pos_cor=pos_cor, morph_cor=morph_cor)


@main.route("/corpus/<int:corpus_id>/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/<int:corpus_id>/delete', methods=["GET", "POST"])
@requires_corpus_admin_access("corpus_id")
def corpus_delete(corpus_id: int):
Expand Down
3 changes: 2 additions & 1 deletion app/main/views/tokens.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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()\
Expand Down
2 changes: 1 addition & 1 deletion app/models/__init__.py
Original file line number Diff line number Diff line change
@@ -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
46 changes: 45 additions & 1 deletion app/models/corpus.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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()
24 changes: 23 additions & 1 deletion app/statics/css/site.css
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down Expand Up @@ -182,4 +203,5 @@ https://codepen.io/sergiopedercini/pen/jmKdbj/?source=post_page-----------------
}
#statistics > div {
margin-bottom: 1em !important;
}
}

4 changes: 2 additions & 2 deletions app/templates/macros/nav_macros.html
Original file line number Diff line number Diff line change
Expand Up @@ -176,8 +176,8 @@
<li class="nav-item"><a class="nav-link" href="{{ url_for("main.tokens_correct", corpus_id=corpus.id) }}">
<i class="fa fa-pencil"></i> Correct tokens
</a></li>
<li class="nav-item"> <a class="nav-link " href="{{ url_for("main.tokens_correct", corpus_id=corpus.id) }}" id="last-edit-link">
<i class="fa fa-history"></i> Last corrected tokens
<li class="nav-item"> <a class="nav-link" id="bookmark_link" href="{{ url_for("main.corpus_bookmark", corpus_id=corpus.id) }}">
<i class="fa fa-bookmark"></i> Bookmark
</a></li>
<li class="nav-item"><a class="nav-link" href="{{url_for("main.tokens_export", corpus_id=corpus.id)}}">
<i class="fa fa-download"></i> Export tokens</a></li>
Expand Down
5 changes: 3 additions & 2 deletions app/templates/macros/tokens_macros.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
{{ token.left_context }} <{{tag}}>{{ token.form }}</{{tag}}> {{ 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 %}
<div>
Expand All @@ -27,7 +27,7 @@ <h3>Save checked similar lemma</h3>
</thead>
<tbody>
{% for token in tokens.items %}
<tr class="editable{% if token.id in changed %} table-changed{% endif %} token-anchor" data-token-order="{{ token.order_id }}" id="token_{{token.id}}_row">
<tr class="editable{% if token.id in changed %} table-changed{% endif %} token-anchor {% if current_user and current_user.bookmark.token_id == token.id %}bookmark{% endif %}" data-token-order="{{ token.order_id }}" id="token_{{token.id}}_row">
<td class="tok-anc"><a tabindex="-1" href="#tok{{ token.order_id }}" id="tok{{ token.order_id }}">{{ token.order_id }}</a></td>
<td>{{token.form}}</td>
<td {% if not checkbox %}contenteditable="true" class="token_lemma typeahead"{% endif %}>{{token.lemma}}</td>
Expand All @@ -49,6 +49,7 @@ <h3>Save checked similar lemma</h3>
<a class="dropdown-item" href="{{ url_for('main.tokens_edit_form', corpus_id=corpus.id, token_id=token.id) }}">Edit the form</a>
<a class="dropdown-item" href="{{ url_for('main.tokens_del_row', corpus_id=corpus.id, token_id=token.id) }}">Delete the row</a>
<a class="dropdown-item" href="{{ url_for('main.tokens_add_row', corpus_id=corpus.id, token_id=token.id) }}">Add a token after this one</a>
<a class="dropdown-item bookmark" href="{{ url_for('main.corpus_bookmark', corpus_id=corpus.id, token_id=token.id, page=tokens.page) }}">Set as bookmark</a>
</div>
</td>
{%- endif -%}
Expand Down
2 changes: 1 addition & 1 deletion app/templates/main/tokens_correct.html
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ <h1>Corpus {{corpus.name}} - List of tokens </h1>

{{ 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") }}

Expand Down
83 changes: 83 additions & 0 deletions tests/test_selenium/test_bookmark.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
from tests.test_selenium.base import TestBase
from tests.db_fixtures.wauchier import WauchierTokens
from app.models import Bookmark, WordToken


class TestBookmark(TestBase):
""" Check that saving a bookmark and going to it works
"""

def set_bookmark(self, tok_id, page=None):
self.driver.get(self.url_for_with_port("main.tokens_correct", corpus_id="1", page=page))
self.driver.save_screenshot("token.png")
self.driver.find_element_by_id("dd_t"+str(tok_id)).click()
self.driver.implicitly_wait(2)
dd = self.driver.find_element_by_css_selector("*[aria-labelledby='dd_t{}']".format(tok_id))
dd.find_element_by_partial_link_text("Set as bookmark").click()
self.driver.implicitly_wait(2)

def test_create_bookmark(self):
""" [Bookmark] Check that we are able to create a bookmark """
self.addCorpus("wauchier")
# First edition
self.set_bookmark(110, 2)

with self.app.app_context():
self.assertIsNotNone(
Bookmark.query.filter(
self.db.and_(
Bookmark.corpus_id == 1,
Bookmark.user_id == 1,
Bookmark.token_id == 110,
Bookmark.page == 2
)
).first()
)

def test_edit_bookmark(self):
""" [Bookmark] Check that we are able to create a bookmark then recreate one"""
self.addCorpus("wauchier")
# Create the bokomark
self.set_bookmark(110, 2)
# Update it
self.set_bookmark(220, 3)
self.assertEqual(
self.driver.current_url,
self.url_for_with_port("main.tokens_correct", corpus_id="1", page=3)+"#token_220_row",
"No bookmark should go to first page"
)

with self.app.app_context():
bookmark = Bookmark.query.filter(
self.db.and_(
Bookmark.corpus_id == 1,
Bookmark.user_id == 1
)
).all()
self.assertIsNotNone(bookmark)
self.assertEqual(len(bookmark), 1)
self.assertEqual(bookmark[0].token_id, 220)
self.assertEqual(bookmark[0].page, 3)

def test_go_to_bookmark(self):
""" [Bookmark] Check that we are to go to a bookmark """
self.addCorpus("wauchier")
# Check first cases where there is nothing
self.driver.get(self.url_for_with_port("main.tokens_correct", corpus_id="1"))
self.driver.find_element_by_id("bookmark_link").click()
self.driver.implicitly_wait(1)
self.assertEqual(
self.driver.current_url, self.url_for_with_port("main.tokens_correct", corpus_id="1"),
"No bookmark should go to first page"
)
# Set the bookmark
self.set_bookmark(220, 3)
# Reset page
self.driver.get(self.url_for_with_port("main.tokens_correct", corpus_id="1"))
self.driver.find_element_by_id("bookmark_link").click()
self.driver.implicitly_wait(1)
self.assertEqual(
self.driver.current_url,
self.url_for_with_port("main.tokens_correct", corpus_id="1", page=3)+"#token_220_row",
"No bookmark should go to first page"
)

0 comments on commit 424d57f

Please sign in to comment.