diff --git a/app/__init__.py b/app/__init__.py index ace587d0..ac3a4548 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -49,6 +49,7 @@ def _set_sqlite_case_insensitive_pragma(dbapi_con, connection_record): """ This ensures that SQLite is not case-insensitive when using LIKEs""" if isinstance(dbapi_con, SQLite3Connection): dbapi_con.execute("PRAGMA case_sensitive_like=ON;") + dbapi_con.execute("PRAGMA foreign_keys=ON;") app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False diff --git a/app/main/filters.py b/app/main/filters.py index f5deb90d..f2f42ef5 100644 --- a/app/main/filters.py +++ b/app/main/filters.py @@ -1,8 +1,10 @@ -from flask import request, url_for -from json import dumps import math - import locale +from typing import Optional +from json import dumps + +from flask import request, url_for + from . import main from ..models import WordToken @@ -27,7 +29,9 @@ def json(obj): @main.app_template_filter("get_token_uri") -def get_token_uri(token: WordToken): +def get_token_uri(token: Optional[WordToken]): + if token is None: + return "#" per_page = request.args.get("per_page", 100, int) page = int(math.ceil(token.order_id / 100)) return url_for( diff --git a/app/models/corpus.py b/app/models/corpus.py index b1e45d94..d9cfcd22 100644 --- a/app/models/corpus.py +++ b/app/models/corpus.py @@ -633,7 +633,7 @@ class WordToken(db.Model): left_context = db.Column(db.String(512)) right_context = db.Column(db.String(512)) - _changes = db.relationship("ChangeRecord", cascade="all,delete") + _changes = db.relationship("ChangeRecord") CONTEXT_LEFT = 3 CONTEXT_RIGHT = 3 @@ -742,7 +742,8 @@ def edit_form(self, form, corpus, user): old=self.form, action_type=TokenHistory.TYPES.Edition, user_id=user.id, - word_token_id=self.id + word_token_id=self.id, + order_id = self.order_id )) self.form = form db.session.add(self) @@ -782,7 +783,8 @@ def add_form(self, form, corpus, user): new=form, action_type=TokenHistory.TYPES.Addition, user_id=user.id, - word_token_id=new_token.id + word_token_id=new_token.id, + order_id = new_token.order_id )) # Update the contexts @@ -809,6 +811,7 @@ def del_form(self, corpus, user): WordToken.order_id > self.order_id )).update({WordToken.order_id: WordToken.order_id - 1}) + # Update # Record the change db.session.add(TokenHistory( corpus=corpus.id, @@ -816,9 +819,11 @@ def del_form(self, corpus, user): old=self.form, action_type=TokenHistory.TYPES.Deletion, user_id=user.id, - word_token_id=self.id + #word_token_id=self.id, + order_id = self.order_id )) + # Update the contexts self.update_context_around(corpus, delete=self.id) @@ -1348,11 +1353,12 @@ class TYPES(enum.Enum): id = db.Column(db.Integer, primary_key=True, autoincrement=True) corpus = db.Column(db.Integer, db.ForeignKey('corpus.id', ondelete="CASCADE")) - word_token_id = db.Column(db.Integer, db.ForeignKey('word_token.id')) + word_token_id = db.Column(db.Integer, db.ForeignKey('word_token.id', ondelete='SET NULL'), nullable=True) user_id = db.Column(db.Integer, db.ForeignKey(User.id)) action_type = db.Column(db.Enum(TYPES), nullable=False) new = db.Column(db.String(100), nullable=True) old = db.Column(db.String(100), nullable=True) + order_id = db.Column(db.Integer, nullable=True) created_on = db.Column(db.DateTime, server_default=db.func.now()) user = db.relationship(User, lazy='select') @@ -1464,8 +1470,9 @@ def get_like(corpus_id, form, group_by, category="lemma"): class ChangeRecord(db.Model): """ A change record keep track of lemma, POS or morph that have been changed for a particular form""" id = db.Column(db.Integer, primary_key=True, autoincrement=True) - corpus = db.Column(db.Integer, db.ForeignKey('corpus.id')) - word_token_id = db.Column(db.Integer, db.ForeignKey('word_token.id')) + corpus = db.Column(db.Integer, db.ForeignKey('corpus.id', ondelete="CASCADE")) + word_token_id = db.Column(db.Integer, db.ForeignKey('word_token.id', ondelete="SET NULL"), nullable=True, + default=None) user_id = db.Column(db.Integer, db.ForeignKey(User.id)) form = db.Column(db.String(128)) lemma = db.Column(db.String(128)) diff --git a/tests/test_selenium/base.py b/tests/test_selenium/base.py index 7e06c19e..f0b2d006 100644 --- a/tests/test_selenium/base.py +++ b/tests/test_selenium/base.py @@ -37,19 +37,18 @@ def get_chrome(): # This ensures compatibility with nektos/act - if os.path.isfile('/usr/bin/chromium-browser'): - return '/usr/bin/chromium-browser' - elif os.path.isfile('/usr/bin/chromium'): - return '/usr/bin/chromium' - elif os.path.isfile('/usr/bin/chrome'): + if os.path.isfile('/usr/bin/chrome'): return '/usr/bin/chrome' elif os.path.isfile('/usr/bin/google-chrome'): return '/usr/bin/google-chrome' + elif os.path.isfile('/usr/bin/chromium-browser'): + return '/usr/bin/chromium-browser' + elif os.path.isfile('/usr/bin/chromium'): + return '/usr/bin/chromium' else: return None - class _element_has_count(object): """ An expectation for checking that an element has a particular css class. @@ -414,6 +413,14 @@ def url_for_with_port(self, *args, **kwargs): url = url.replace('://localhost/', '://localhost:%d/' % (self.app.config["LIVESERVER_PORT"])) return url + def token_dropdown_link(self, tok_id, link): + self.driver.get(self.url_for_with_port("main.tokens_correct", corpus_id="1")) + 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)) + self.element_find_element_by_partial_link_text(dd, link).click() + self.driver.implicitly_wait(2) + class TokenCorrectBase(TestBase): """ Base class with helpers to test token edition page """ diff --git a/tests/test_selenium/test_token_correct.py b/tests/test_selenium/test_token_correct.py index 4771521e..878314cd 100644 --- a/tests/test_selenium/test_token_correct.py +++ b/tests/test_selenium/test_token_correct.py @@ -1,5 +1,6 @@ from tests.test_selenium.base import TokenCorrectBase, TokenCorrect2CorporaBase import selenium +from sqlalchemy import text class TestTokenCorrectWauchierCorpus(TokenCorrectBase): @@ -223,3 +224,37 @@ def test_edit_token_lemma_with_typeahead_click(self): "s", id_row=str(self.first_token_id(2)+1), corpus_id="2", autocomplete_selector=".autocomplete-suggestion[data-val='saint']" ) + + def test_correct_delete(self): + """ [TokenCorrectDelete] Check that we are able to edit then delete the form of a token """ + self.addCorpus(with_token=True, with_allowed_lemma=True, tokens_up_to=24) + # We edit the lemma of the first token + token, status_text, row = self.edith_nth_row_value("saint", id_row="1") + # We checked it was saved + self.assert_saved(row) + self.assertEqual( + len(self.db.session.execute(text("SELECT * FROM change_record")).all()), 1, + "There should be one record in the history" + ) + # We delete the token + self.token_dropdown_link(1, "Delete") + # Confirm the token's form on delete form + inp = self.driver_find_element_by_css_selector("input[name='form']") + inp.clear() + inp.send_keys("De") + # Delete + self.driver_find_element_by_css_selector("button[type='submit']").click() + # Check that it was removed + row = self.driver_find_elements_by_css_selector("tr.token-anchor[data-token-order='1']")[0] + self.assertEqual( + self.element_find_elements_by_tag_name(row, "td")[1].text, "seint", + "The token has been removed" + ) + self.driver_find_element_by_link_text("History").click() + self.assertEqual( + len(self.db.session.execute(text("SELECT * FROM change_record")).all()), 1, + "There should be one record in the history" + ) + history_row = self.driver_find_elements_by_css_selector("tbody tr.history") + self.assertEqual(len(history_row), 1, "There should be one record in the history") + diff --git a/tests/test_selenium/test_tokens_edit.py b/tests/test_selenium/test_tokens_edit.py index 7fe69267..d25bf1a5 100644 --- a/tests/test_selenium/test_tokens_edit.py +++ b/tests/test_selenium/test_tokens_edit.py @@ -6,14 +6,6 @@ class TestTokenEdit(TestBase): """ Check token form edition, token update, token delete """ - def dropdown_link(self, tok_id, link): - self.driver.get(self.url_for_with_port("main.tokens_correct", corpus_id="1")) - 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)) - self.element_find_element_by_partial_link_text(dd, link).click() - self.driver.implicitly_wait(2) - def change_form_value(self, value): inp = self.driver_find_element_by_css_selector("input[name='form']") inp.clear() @@ -46,7 +38,7 @@ def test_edition(self): """ [TokenEdit] Check that we are able to edit the form of a token """ self.addCorpus("wauchier") # First edition - self.dropdown_link(5, "Edit") + self.token_dropdown_link(5, "Edit") self.change_form_value("oulala") self.driver_find_element_by_css_selector("button[type='submit']").click() self.driver.implicitly_wait(5) @@ -69,7 +61,7 @@ def test_edition(self): "History should be saved" ) # Second edition - self.dropdown_link(8, "Edit") + self.token_dropdown_link(8, "Edit") self.change_form_value("Oulipo") self.driver_find_element_by_css_selector("button[type='submit']").click() self.driver.implicitly_wait(5) @@ -96,7 +88,7 @@ def test_addition(self): """ [TokenEdit] Check that we are able to add tokens""" self.addCorpus("wauchier") # First edition - self.dropdown_link(5, "Add") + self.token_dropdown_link(5, "Add") self.change_form_value("oulala") self.driver_find_element_by_css_selector("button[type='submit']").click() self.driver.implicitly_wait(5) @@ -120,7 +112,7 @@ def test_addition(self): "History should be saved" ) # Second edition - self.dropdown_link(8, "Add") + self.token_dropdown_link(8, "Add") self.change_form_value("Oulipo") self.driver_find_element_by_css_selector("button[type='submit']").click() self.driver.implicitly_wait(5) @@ -143,8 +135,8 @@ def test_addition(self): "History should be saved" ) - def test_delete(self): - """ [TokenEdit] Check that we are able to edit the form of a token """ + def test_edit_delete(self): + """ [TokenEdit] Check that we are able to edit then delete the form of a token """ self.addCorpus("wauchier") # Get the original value @@ -152,7 +144,7 @@ def test_delete(self): original_set = self.select_context_around(5) # First we add a token - self.dropdown_link(5, "Add") + self.token_dropdown_link(5, "Add") self.change_form_value("oulala") self.driver_find_element_by_css_selector("button[type='submit']").click() self.driver.implicitly_wait(5) @@ -177,11 +169,10 @@ def test_delete(self): ) # Then we remove it - self.dropdown_link(6, "Delete") + self.token_dropdown_link(6, "Delete") self.change_form_value("oulala") self.driver_find_element_by_css_selector("button[type='submit']").click() self.driver.implicitly_wait(5) - self.assertEqual( self.select_context_around(5), original_set,