From 1d3e8bbdb84c96a22b4ee3faf368305ddf5c79a8 Mon Sep 17 00:00:00 2001 From: Aga Date: Thu, 8 Jun 2023 10:40:08 +0100 Subject: [PATCH 01/56] add check --- portality/bll/services/article.py | 4 ++++ portality/ui/messages.py | 1 + 2 files changed, 5 insertions(+) diff --git a/portality/bll/services/article.py b/portality/bll/services/article.py index 7b55894d24..1e0e2ac76d 100644 --- a/portality/bll/services/article.py +++ b/portality/bll/services/article.py @@ -170,6 +170,10 @@ def _validate_issns(article_bibjson: models.ArticleBibJSON): if pissn == eissn: raise exceptions.ArticleNotAcceptable(message=Messages.EXCEPTION_IDENTICAL_PISSN_AND_EISSN) + journals = models.Journal.find_by_issn([pissn,eissn], True) + if journals is None or len(journals) > 1: + raise exceptions.ArticleNotAcceptable(message=Messages.EXCEPTION_MISMATCHED_ISSNS) + def create_article(self, article, account, duplicate_check=True, merge_duplicate=True, limit_to_account=True, add_journal_info=False, dry_run=False, update_article_id=None): diff --git a/portality/ui/messages.py b/portality/ui/messages.py index 094751c97c..cf8092e212 100644 --- a/portality/ui/messages.py +++ b/portality/ui/messages.py @@ -61,6 +61,7 @@ class Messages(object): EXCEPTION_NO_CONTRIBUTORS_EXPLANATION = "DOAJ requires at least one author for each article." EXCEPTION_TOO_MANY_ISSNS = "Too many ISSNs. Only 2 ISSNs are allowed: one Print ISSN and one Online ISSN." + EXCEPTION_MISMATCHED_ISSNS = "Issns provided don't match any journal." EXCEPTION_ISSNS_OF_THE_SAME_TYPE = "Both ISSNs have the same type: {type}" EXCEPTION_IDENTICAL_PISSN_AND_EISSN = "The Print and Online ISSNs supplied are identical. If you supply 2 ISSNs they must be different." EXCEPTION_NO_ISSNS = "Neither Print ISSN nor Online ISSN has been supplied. DOAJ requires at least one ISSN." From 83b6fded639bba06090b38c8569f81f5da819e00 Mon Sep 17 00:00:00 2001 From: Aga Date: Thu, 8 Jun 2023 14:01:38 +0100 Subject: [PATCH 02/56] add check and unit tests --- ...issn_validation_against_journal.matrix.csv | 17 +++ ...sn_validation_against_journal.settings.csv | 19 +++ ...n_validation_against_journal.settings.json | 119 ++++++++++++++++++ ...test_article_acceptable_and_permissions.py | 56 ++++++++- portality/bll/services/article.py | 10 +- 5 files changed, 218 insertions(+), 3 deletions(-) create mode 100644 doajtest/matrices/article_create_article/issn_validation_against_journal.matrix.csv create mode 100644 doajtest/matrices/article_create_article/issn_validation_against_journal.settings.csv create mode 100644 doajtest/matrices/article_create_article/issn_validation_against_journal.settings.json diff --git a/doajtest/matrices/article_create_article/issn_validation_against_journal.matrix.csv b/doajtest/matrices/article_create_article/issn_validation_against_journal.matrix.csv new file mode 100644 index 0000000000..0d2f704aba --- /dev/null +++ b/doajtest/matrices/article_create_article/issn_validation_against_journal.matrix.csv @@ -0,0 +1,17 @@ +test_id,eissn,pissn,validated +1,eissn_in_doaj,pissn_in_doaj,yes +2,eissn_in_doaj,eissn_not_in_doaj, +3,eissn_in_doaj,pissn_not_in_doaj, +4,eissn_in_doaj,!eissn_in_doaj, +5,pissn_in_doaj,eissn_in_doaj, +6,pissn_in_doaj,eissn_not_in_doaj, +7,pissn_in_doaj,pissn_not_in_doaj, +8,pissn_in_doaj,!pissn_in_doaj, +9,eissn_not_in_doaj,eissn_in_doaj, +10,eissn_not_in_doaj,pissn_in_doaj, +11,eissn_not_in_doaj,pissn_not_in_doaj, +12,eissn_not_in_doaj,!eissn_not_in_doaj, +13,pissn_not_in_doaj,eissn_in_doaj, +14,pissn_not_in_doaj,pissn_in_doaj, +15,pissn_not_in_doaj,eissn_not_in_doaj, +16,pissn_not_in_doaj,!pissn_not_in_doaj, diff --git a/doajtest/matrices/article_create_article/issn_validation_against_journal.settings.csv b/doajtest/matrices/article_create_article/issn_validation_against_journal.settings.csv new file mode 100644 index 0000000000..a8eab3f4ce --- /dev/null +++ b/doajtest/matrices/article_create_article/issn_validation_against_journal.settings.csv @@ -0,0 +1,19 @@ +field,test_id,eissn,pissn,validated +type,index,generated,generated,conditional +deafult,,,,no +,,,, +values,,eissn_in_doaj,eissn_in_doaj,yes +values,,pissn_in_doaj,pissn_in_doaj,no +values,,eissn_not_in_doaj,eissn_not_in_doaj, +values,,pissn_not_in_doaj,pissn_not_in_doaj, +,,,, +,,,, +conditional validated,,eissn_in_doaj,pissn_in_doaj,yes +constraint eissn,,eissn_in_doaj,!eissn_in_doaj, +constraint eissn,,eissn_not_in_doaj,!eissn_not_in_doaj, +constraint eissn,,pissn_not_in_doaj,!pissn_not_in_doaj, +constraint eissn,,pissn_in_doaj,!pissn_in_doaj, +constraint pissn,,eissn_in_doaj,!eissn_in_doaj, +constraint pissn,,eissn_not_in_doaj,!eissn_not_in_doaj, +constraint pissn,,pissn_not_in_doaj,!pissn_not_in_doaj, +constraint pissn,,pissn_in_doaj,!pissn_in_doaj, \ No newline at end of file diff --git a/doajtest/matrices/article_create_article/issn_validation_against_journal.settings.json b/doajtest/matrices/article_create_article/issn_validation_against_journal.settings.json new file mode 100644 index 0000000000..11d1012a96 --- /dev/null +++ b/doajtest/matrices/article_create_article/issn_validation_against_journal.settings.json @@ -0,0 +1,119 @@ +{ + "parameters": [ + { + "name": "test_id", + "type": "index" + }, + { + "name": "eissn", + "type": "generated", + "values": { + "eissn_in_doaj": { + "constraints": { + "pissn": { + "nor": [ + "eissn_in_doaj" + ] + } + } + }, + "pissn_in_doaj": { + "constraints": { + "pissn": { + "nor": [ + "pissn_in_doaj" + ] + } + } + }, + "eissn_not_in_doaj": { + "constraints": { + "pissn": { + "nor": [ + "eissn_not_in_doaj" + ] + } + } + }, + "pissn_not_in_doaj": { + "constraints": { + "pissn": { + "nor": [ + "pissn_not_in_doaj" + ] + } + } + } + } + }, + { + "name": "pissn", + "type": "generated", + "values": { + "eissn_in_doaj": {}, + "pissn_in_doaj": {}, + "eissn_not_in_doaj": {}, + "pissn_not_in_doaj": {}, + "!eissn_in_doaj": { + "constraints": { + "eissn": { + "or": [ + "eissn_in_doaj" + ] + } + } + }, + "!eissn_not_in_doaj": { + "constraints": { + "eissn": { + "or": [ + "eissn_not_in_doaj" + ] + } + } + }, + "!pissn_not_in_doaj": { + "constraints": { + "eissn": { + "or": [ + "pissn_not_in_doaj" + ] + } + } + }, + "!pissn_in_doaj": { + "constraints": { + "eissn": { + "or": [ + "pissn_in_doaj" + ] + } + } + } + } + }, + { + "name": "validated", + "type": "conditional", + "values": { + "yes": { + "conditions": [ + { + "eissn": { + "or": [ + "eissn_in_doaj" + ] + }, + "pissn": { + "or": [ + "pissn_in_doaj" + ] + } + } + ] + }, + "no": {} + } + } + ] +} \ No newline at end of file diff --git a/doajtest/unit/test_article_acceptable_and_permissions.py b/doajtest/unit/test_article_acceptable_and_permissions.py index eb4c04d4fb..881323ba92 100644 --- a/doajtest/unit/test_article_acceptable_and_permissions.py +++ b/doajtest/unit/test_article_acceptable_and_permissions.py @@ -14,6 +14,11 @@ def is_acceptable_load_cases(): "test_id", {"test_id": []}) +def issn_validation_against_journal_load_sets(): + return load_parameter_sets(rel2abs(__file__, "..", "matrices", "article_create_article"), "issn_validation_against_journal", + "test_id", + {"test_id": []}) + class TestBLLPrepareUpdatePublisher(DoajTestCase): @@ -110,4 +115,53 @@ def test_has_permissions(self): assert failed_result["unowned"].sort() == [pissn, eissn].sort() # assert failed_result == {'success': 0, 'fail': 1, 'update': 0, 'new': 0, 'shared': [], # 'unowned': [pissn, eissn], - # 'unmatched': []}, "received: {}".format(failed_result) \ No newline at end of file + # 'unmatched': []}, "received: {}".format(failed_result) + + + @parameterized.expand(issn_validation_against_journal_load_sets) + def test_issn_validation_against_journal_load_sets(self, value, kwargs): + kwpissn = kwargs.get("pissn") + kweissn = kwargs.get("eissn") + validated = kwargs.get("validated") + + js = JournalFixtureFactory.make_many_journal_sources(2) + journal_in_doaj = Journal(**js[0]) + journal_in_doaj.set_in_doaj(True) + journal_in_doaj.bibjson().pissn = "1111-1111" + journal_in_doaj.bibjson().eissn = "2222-2222" + journal_in_doaj.save(blocking=True) + + journal_not_in_doaj = Journal(**js[1]) + journal_not_in_doaj.set_in_doaj(False) + journal_not_in_doaj.bibjson().pissn = "3333-3333" + journal_not_in_doaj.bibjson().eissn = "4444-4444" + journal_not_in_doaj.save(blocking=True) + + if (kwpissn == "pissn_in_doaj"): + pissn = journal_in_doaj.bibjson().pissn + elif (kwpissn == "eissn_in_doaj"): + pissn = journal_in_doaj.bibjson().eissn + elif (kwpissn == "pissn_not_in_doaj"): + pissn = journal_not_in_doaj.bibjson().pissn + else: + pissn = journal_not_in_doaj.bibjson().eissn + + if (kweissn == "pissn_in_doaj"): + eissn = journal_in_doaj.bibjson().pissn + elif (kweissn == "eissn_in_doaj"): + eissn = journal_in_doaj.bibjson().eissn + elif (kweissn == "pissn_not_in_doaj"): + eissn = journal_not_in_doaj.bibjson().pissn + else: + eissn = journal_not_in_doaj.bibjson().eissn + + + art_source = ArticleFixtureFactory.make_article_source(pissn=pissn, eissn=eissn) + article = Article(**art_source) + + if validated: + self.assertIsNone(self.svc.is_acceptable(article)) + + else: + with self.assertRaises(exceptions.ArticleNotAcceptable): + self.svc.is_acceptable(article) \ No newline at end of file diff --git a/portality/bll/services/article.py b/portality/bll/services/article.py index 1e0e2ac76d..5430b5f3ef 100644 --- a/portality/bll/services/article.py +++ b/portality/bll/services/article.py @@ -170,8 +170,14 @@ def _validate_issns(article_bibjson: models.ArticleBibJSON): if pissn == eissn: raise exceptions.ArticleNotAcceptable(message=Messages.EXCEPTION_IDENTICAL_PISSN_AND_EISSN) - journals = models.Journal.find_by_issn([pissn,eissn], True) - if journals is None or len(journals) > 1: + journalp = models.Journal.find_by_issn([pissn], True) + journale = models.Journal.find_by_issn([eissn], True) + + # check if only one and the same journal matches pissn and eissn and if they are in the correct fields + if len(journalp) != 1 or \ + len(journale) != 1 or \ + journale[0].id != journalp[0].id or \ + journale[0].bibjson().pissn != pissn: raise exceptions.ArticleNotAcceptable(message=Messages.EXCEPTION_MISMATCHED_ISSNS) def create_article(self, article, account, duplicate_check=True, merge_duplicate=True, From c5392bd7aeb2f6c9198c037062d52619bdea4900 Mon Sep 17 00:00:00 2001 From: Aga Date: Thu, 8 Jun 2023 14:05:12 +0100 Subject: [PATCH 03/56] one more unit test --- ...test_article_acceptable_and_permissions.py | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/doajtest/unit/test_article_acceptable_and_permissions.py b/doajtest/unit/test_article_acceptable_and_permissions.py index 881323ba92..5e0328635f 100644 --- a/doajtest/unit/test_article_acceptable_and_permissions.py +++ b/doajtest/unit/test_article_acceptable_and_permissions.py @@ -164,4 +164,24 @@ def test_issn_validation_against_journal_load_sets(self, value, kwargs): else: with self.assertRaises(exceptions.ArticleNotAcceptable): - self.svc.is_acceptable(article) \ No newline at end of file + self.svc.is_acceptable(article) + + def test_check_validation_for_2_journals(self): + + js = JournalFixtureFactory.make_many_journal_sources(2, in_doaj=True) + journal_in_doaj = Journal(**js[0]) + journal_in_doaj.bibjson().pissn = "1111-1111" + journal_in_doaj.bibjson().eissn = "2222-2222" + journal_in_doaj.save(blocking=True) + + journal_not_in_doaj = Journal(**js[1]) + journal_not_in_doaj.bibjson().pissn = "3333-3333" + journal_not_in_doaj.bibjson().eissn = "4444-4444" + journal_not_in_doaj.save(blocking=True) + + + art_source = ArticleFixtureFactory.make_article_source(pissn="1111-1111", eissn="4444-4444") + article = Article(**art_source) + + with self.assertRaises(exceptions.ArticleNotAcceptable): + self.svc.is_acceptable(article) \ No newline at end of file From bca84c303821ac5323afbc2c2140acada6d04f1c Mon Sep 17 00:00:00 2001 From: Aga Date: Fri, 9 Jun 2023 12:52:30 +0100 Subject: [PATCH 04/56] add new query and unit tests --- doajtest/unit/test_models.py | 27 ++++++++++++++++++++++++ portality/bll/services/article.py | 15 ++++++-------- portality/models/v2/journal.py | 34 +++++++++++++++++++++++++++++++ 3 files changed, 67 insertions(+), 9 deletions(-) diff --git a/doajtest/unit/test_models.py b/doajtest/unit/test_models.py index 09fb0d0205..1459fab2ab 100644 --- a/doajtest/unit/test_models.py +++ b/doajtest/unit/test_models.py @@ -1641,3 +1641,30 @@ def test_get_name_safe(self): # account does not exist assert models.Account.get_name_safe('not existing account id') == '' + def test_11_find_by_issn(self): + js = JournalFixtureFactory.make_many_journal_sources(2, in_doaj=True) + j1 = models.Journal(**js[0]) + j1.bibjson().pissn = "1111-1111" + j1.bibjson().eissn = "2222-2222" + j1.save(blocking=True) + + j2 = models.Journal(**js[1]) + j2.bibjson().pissn = "3333-3333" + j2.bibjson().eissn = "4444-4444" + j2.save(blocking=True) + + journals = models.Journal.find_by_issn(["1111-1111", "2222-2222"], True) + assert len(journals) == 1 + assert journals[0].id == j1.id + + journals = models.Journal.find_by_issn(["1111-1111", "3333-3333"], True) + assert len(journals) == 2 + assert journals[0].id == j1.id + assert journals[1].id == j2.id + + journals = models.Journal.find_by_issn_exact(["1111-1111", "2222-2222"], True) + assert len(journals) == 1 + assert journals[0].id == j1.id + + journals = models.Journal.find_by_issn_exact(["1111-1111", "3333-3333"], True) + assert len(journals) == 0 \ No newline at end of file diff --git a/portality/bll/services/article.py b/portality/bll/services/article.py index 5430b5f3ef..d9fb6701e9 100644 --- a/portality/bll/services/article.py +++ b/portality/bll/services/article.py @@ -170,20 +170,17 @@ def _validate_issns(article_bibjson: models.ArticleBibJSON): if pissn == eissn: raise exceptions.ArticleNotAcceptable(message=Messages.EXCEPTION_IDENTICAL_PISSN_AND_EISSN) - journalp = models.Journal.find_by_issn([pissn], True) - journale = models.Journal.find_by_issn([eissn], True) - - # check if only one and the same journal matches pissn and eissn and if they are in the correct fields - if len(journalp) != 1 or \ - len(journale) != 1 or \ - journale[0].id != journalp[0].id or \ - journale[0].bibjson().pissn != pissn: + journal = models.Journal.find_by_issn_exact([pissn,eissn], True) + + # check if only one journal matches pissn and eissn and if they are in the correct fields + # no need to check eissn, if pissn matches, pissn and eissn are different and only 1 journal has been found - then eissn matches too + if len(journal) != 1 or journal[0].bibjson().pissn != pissn: raise exceptions.ArticleNotAcceptable(message=Messages.EXCEPTION_MISMATCHED_ISSNS) def create_article(self, article, account, duplicate_check=True, merge_duplicate=True, limit_to_account=True, add_journal_info=False, dry_run=False, update_article_id=None): - """ + """# no need to check eissn, if pissn matches, pissn and eissn are different and only 1 journal has been found - then eissn matches too Create an individual article in the database This method will check and merge any duplicates, and report back on successes and failures in a manner consistent with diff --git a/portality/models/v2/journal.py b/portality/models/v2/journal.py index 41c50a7ce9..5a4997a0de 100644 --- a/portality/models/v2/journal.py +++ b/portality/models/v2/journal.py @@ -70,6 +70,22 @@ def find_by_issn(cls, issns, in_doaj=None, max=10): records = [cls(**r.get("_source")) for r in result.get("hits", {}).get("hits", [])] return records + @classmethod + def find_by_issn_exact(cls, issns, in_doaj=None, max=2): + """ + Finds journal that matches given issns exactly - if no data problems should always be only 1 + """ + if not isinstance(issns, list): + issns = [issns] + if len(issns) > 2: + return [] + q = JournalQuery() + q.find_by_issn_exact(issns, in_doaj=in_doaj, max=max) + result = cls.query(q=q.query) + # create an array of objects, using cls rather than Journal, which means subclasses can use it too + records = [cls(**r.get("_source")) for r in result.get("hits", {}).get("hits", [])] + return records + @classmethod def issns_by_owner(cls, owner, in_doaj=None): q = IssnQuery(owner, in_doaj=in_doaj) @@ -922,6 +938,16 @@ class JournalQuery(object): } } + must_query = { + "track_total_hits": True, + "query": { + "bool": { + "must": [ + ] + } + } + } + all_doaj = { "track_total_hits": True, "query": { @@ -947,6 +973,14 @@ def find_by_issn(self, issns, in_doaj=None, max=10): self.query["query"]["bool"]["must"].append({"term": {"admin.in_doaj": in_doaj}}) self.query["size"] = max + def find_by_issn_exact(self, issns, in_doaj=None, max=10): + self.query = deepcopy(self.must_query) + for issn in issns: + self.query["query"]["bool"]["must"].append({"term": {"index.issn.exact": issn}}) + if in_doaj is not None: + self.query["query"]["bool"]["must"].append({"term": {"admin.in_doaj": in_doaj}}) + self.query["size"] = max + def all_in_doaj(self): q = deepcopy(self.all_doaj) if self.minified: From 0122262cd79df3e1c803812114a1a8c81541253a Mon Sep 17 00:00:00 2001 From: Aga Date: Fri, 9 Jun 2023 13:32:25 +0100 Subject: [PATCH 05/56] remove duplicated code --- portality/bll/services/article.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/portality/bll/services/article.py b/portality/bll/services/article.py index d9fb6701e9..feebf2b481 100644 --- a/portality/bll/services/article.py +++ b/portality/bll/services/article.py @@ -159,9 +159,6 @@ def _validate_issns(article_bibjson: models.ArticleBibJSON): if len(pissn) > 1 or len(eissn) > 1: raise exceptions.ArticleNotAcceptable(message=Messages.EXCEPTION_TOO_MANY_ISSNS) - pissn = article_bibjson.get_one_identifier("pissn") - eissn = article_bibjson.get_one_identifier("eissn") - # no pissn or eissn if not pissn and not eissn: raise exceptions.ArticleNotAcceptable(message=Messages.EXCEPTION_NO_ISSNS) From e0db3f581df83e5cc958f96865d073f05e5b11c3 Mon Sep 17 00:00:00 2001 From: Aga Date: Fri, 9 Jun 2023 14:21:06 +0100 Subject: [PATCH 06/56] add script to find all articles with invalid issns --- ...230609_find_articles_with_invalid_issns.py | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 portality/scripts/230609_find_articles_with_invalid_issns.py diff --git a/portality/scripts/230609_find_articles_with_invalid_issns.py b/portality/scripts/230609_find_articles_with_invalid_issns.py new file mode 100644 index 0000000000..acebc0ae7a --- /dev/null +++ b/portality/scripts/230609_find_articles_with_invalid_issns.py @@ -0,0 +1,68 @@ +from portality import models +from portality.bll.services import article as articlesvc +from portality.bll import exceptions +from portality.core import es_connection +from portality.util import ipt_prefix +import esprit +import csv + +IN_DOAJ = { + "query": { + "bool": { + "must": [ + {"term" : {"admin.in_doaj":True}} + ] + } + } +} + + +if __name__ == "__main__": + + # import argparse + # + # parser = argparse.ArgumentParser() + # parser.add_argument("-o", "--out", help="output file path") + # args = parser.parse_args() + # + # if not args.out: + # print("Please specify an output file path with the -o option") + # parser.print_help() + # exit() + + out = "out.csv" + + with open(out, "w", encoding="utf-8") as f: + writer = csv.writer(f) + writer.writerow(["ID", "PISSN", "EISSN", "Journals found with article's PISSN", "In doaj?", "Journals found with article's EISSN", "In doaj?", "Error"]) + + for a in models.Article.iterate(q=IN_DOAJ, page_size=100, keepalive='5m'): + print(a["id"]) + article = models.Article(_source=a) + bibjson = article.bibjson() + + try: + articlesvc.ArticleService._validate_issns(bibjson) + except exceptions.ArticleNotAcceptable as e: + id = article.id + pissn = bibjson.get_identifiers("pissn") + eissn = bibjson.get_identifiers("eissn") + j_p = [j["id"] for j in models.Journal.find_by_issn(pissn)] + j_p_in_doaj = [] + if (j_p): + for j in j_p: + jobj = models.Journal.pull(j) + if (jobj): + j_p_in_doaj.append(jobj.is_in_doaj()) + else: + j_p_in_doaj.append("n/a") + j_e = [j["id"] for j in models.Journal.find_by_issn(eissn)] + j_e_in_doaj = [] + if (j_e): + for j in j_e: + jobj = models.Journal.pull(j) + if (jobj): + j_e_in_doaj.append(jobj.is_in_doaj()) + else: + j_e_in_doaj.append("n/a") + writer.writerow([id, pissn, eissn, j_p, j_p_in_doaj, j_e, j_e_in_doaj, str(e)]) \ No newline at end of file From 3911cff2657ede8ddd966898ca287cb990569308 Mon Sep 17 00:00:00 2001 From: Aga Date: Fri, 9 Jun 2023 14:25:01 +0100 Subject: [PATCH 07/56] revert mistakenly removed code --- portality/bll/services/article.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/portality/bll/services/article.py b/portality/bll/services/article.py index feebf2b481..17f872582d 100644 --- a/portality/bll/services/article.py +++ b/portality/bll/services/article.py @@ -159,6 +159,9 @@ def _validate_issns(article_bibjson: models.ArticleBibJSON): if len(pissn) > 1 or len(eissn) > 1: raise exceptions.ArticleNotAcceptable(message=Messages.EXCEPTION_TOO_MANY_ISSNS) + pissn = article_bibjson.get_one_identifier("pissn") + eissn = article_bibjson.get_one_identifier("eissn") + # no pissn or eissn if not pissn and not eissn: raise exceptions.ArticleNotAcceptable(message=Messages.EXCEPTION_NO_ISSNS) @@ -167,12 +170,24 @@ def _validate_issns(article_bibjson: models.ArticleBibJSON): if pissn == eissn: raise exceptions.ArticleNotAcceptable(message=Messages.EXCEPTION_IDENTICAL_PISSN_AND_EISSN) - journal = models.Journal.find_by_issn_exact([pissn,eissn], True) + issns = [] + if pissn is not None: + issns.append(pissn) + if eissn is not None: + issns.append(eissn) + + journal = models.Journal.find_by_issn_exact(issns, True) # check if only one journal matches pissn and eissn and if they are in the correct fields # no need to check eissn, if pissn matches, pissn and eissn are different and only 1 journal has been found - then eissn matches too - if len(journal) != 1 or journal[0].bibjson().pissn != pissn: + if len(journal) != 1: raise exceptions.ArticleNotAcceptable(message=Messages.EXCEPTION_MISMATCHED_ISSNS) + if pissn is not None: + if journal[0].bibjson().pissn != pissn: + raise exceptions.ArticleNotAcceptable(message=Messages.EXCEPTION_MISMATCHED_ISSNS) + if eissn is not None: + if journal[0].bibjson().eissn != eissn: + raise exceptions.ArticleNotAcceptable(message=Messages.EXCEPTION_MISMATCHED_ISSNS) def create_article(self, article, account, duplicate_check=True, merge_duplicate=True, limit_to_account=True, add_journal_info=False, dry_run=False, update_article_id=None): From 27878361116f77a3ae1e2656b599001aa522006b Mon Sep 17 00:00:00 2001 From: Aga Date: Fri, 9 Jun 2023 14:29:15 +0100 Subject: [PATCH 08/56] remove unnecessary print --- portality/scripts/230609_find_articles_with_invalid_issns.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/portality/scripts/230609_find_articles_with_invalid_issns.py b/portality/scripts/230609_find_articles_with_invalid_issns.py index acebc0ae7a..c45b8c5d63 100644 --- a/portality/scripts/230609_find_articles_with_invalid_issns.py +++ b/portality/scripts/230609_find_articles_with_invalid_issns.py @@ -37,10 +37,8 @@ writer.writerow(["ID", "PISSN", "EISSN", "Journals found with article's PISSN", "In doaj?", "Journals found with article's EISSN", "In doaj?", "Error"]) for a in models.Article.iterate(q=IN_DOAJ, page_size=100, keepalive='5m'): - print(a["id"]) article = models.Article(_source=a) bibjson = article.bibjson() - try: articlesvc.ArticleService._validate_issns(bibjson) except exceptions.ArticleNotAcceptable as e: From a22dddcf9bd7d7c87bbc2a0fe49fb3c41c8af43c Mon Sep 17 00:00:00 2001 From: Aga Date: Mon, 26 Jun 2023 14:20:30 +0100 Subject: [PATCH 09/56] start with copy to click widget --- cms/sass/components/_form.scss | 2 +- portality/forms/application_forms.py | 6 +++++- portality/static/js/formulaic.js | 4 +++- portality/templates/application_form/_field.html | 3 +++ 4 files changed, 12 insertions(+), 3 deletions(-) diff --git a/cms/sass/components/_form.scss b/cms/sass/components/_form.scss index b203ff5988..fd97423f9f 100644 --- a/cms/sass/components/_form.scss +++ b/cms/sass/components/_form.scss @@ -82,7 +82,7 @@ border-left: 1px solid $sanguine; } -.form__long-help { +.form__long-help, .form__click-to-copy { cursor: pointer; &:hover { diff --git a/portality/forms/application_forms.py b/portality/forms/application_forms.py index c290533678..3b491fe065 100644 --- a/portality/forms/application_forms.py +++ b/portality/forms/application_forms.py @@ -108,7 +108,10 @@ class FieldDefinitions: "validate" : [], "disabled": True } - } + }, + "widgets": [ + "click_to_copy", # ~~^-> ClickToCopy:FormWidget~~ + ], } # ~~->$ OAStatementURL:FormField~~ @@ -2922,6 +2925,7 @@ def wtforms(field, settings): JAVASCRIPT_FUNCTIONS = { "clickable_url": "formulaic.widgets.newClickableUrl", # ~~-> ClickableURL:FormWidget~~ + "click_to_copy": "formulaic.widgets.newClickToCopy", # ~~-> ClickToCopy:FormWidget~~ "clickable_owner": "formulaic.widgets.newClickableOwner", # ~~-> ClickableOwner:FormWidget~~ "select": "formulaic.widgets.newSelect", # ~~-> SelectBox:FormWidget~~ "taglist": "formulaic.widgets.newTagList", # ~~-> TagList:FormWidget~~ diff --git a/portality/static/js/formulaic.js b/portality/static/js/formulaic.js index 8c2b826075..8b89703f83 100644 --- a/portality/static/js/formulaic.js +++ b/portality/static/js/formulaic.js @@ -1195,7 +1195,9 @@ var formulaic = { this.init(); }, - + newClickToCopy : function(params) { + return + }, newClickableOwner : function(params) { return edges.instantiate(formulaic.widgets.ClickableOwner, params) }, diff --git a/portality/templates/application_form/_field.html b/portality/templates/application_form/_field.html index 8ebbaa9fe9..8896befec5 100644 --- a/portality/templates/application_form/_field.html +++ b/portality/templates/application_form/_field.html @@ -8,6 +8,9 @@ {% if f.help("long_help") %} More help {% endif %} + {% if f.has_widget("click_to_copy") %} + Copy value + {% endif %} {% if f.optional %}(Optional){% endif %} {% endset %} From 97dd518a6268d8460e3c6cdf78b072e17172d2bf Mon Sep 17 00:00:00 2001 From: Aga Date: Mon, 26 Jun 2023 14:21:19 +0100 Subject: [PATCH 10/56] capitalisation on issns in messages --- portality/ui/messages.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/portality/ui/messages.py b/portality/ui/messages.py index cf8092e212..2164502784 100644 --- a/portality/ui/messages.py +++ b/portality/ui/messages.py @@ -61,7 +61,7 @@ class Messages(object): EXCEPTION_NO_CONTRIBUTORS_EXPLANATION = "DOAJ requires at least one author for each article." EXCEPTION_TOO_MANY_ISSNS = "Too many ISSNs. Only 2 ISSNs are allowed: one Print ISSN and one Online ISSN." - EXCEPTION_MISMATCHED_ISSNS = "Issns provided don't match any journal." + EXCEPTION_MISMATCHED_ISSNS = "ISSNs provided don't match any journal." EXCEPTION_ISSNS_OF_THE_SAME_TYPE = "Both ISSNs have the same type: {type}" EXCEPTION_IDENTICAL_PISSN_AND_EISSN = "The Print and Online ISSNs supplied are identical. If you supply 2 ISSNs they must be different." EXCEPTION_NO_ISSNS = "Neither Print ISSN nor Online ISSN has been supplied. DOAJ requires at least one ISSN." From 318a3f3db5621c31e23b3fed23a295eb4c2ede37 Mon Sep 17 00:00:00 2001 From: Aga Date: Mon, 26 Jun 2023 14:37:49 +0100 Subject: [PATCH 11/56] create separate function for validation issns against journal rather than in validate issn --- portality/bll/services/article.py | 42 +++++++++++++++++-------------- 1 file changed, 23 insertions(+), 19 deletions(-) diff --git a/portality/bll/services/article.py b/portality/bll/services/article.py index 17f872582d..2782538a0f 100644 --- a/portality/bll/services/article.py +++ b/portality/bll/services/article.py @@ -170,25 +170,6 @@ def _validate_issns(article_bibjson: models.ArticleBibJSON): if pissn == eissn: raise exceptions.ArticleNotAcceptable(message=Messages.EXCEPTION_IDENTICAL_PISSN_AND_EISSN) - issns = [] - if pissn is not None: - issns.append(pissn) - if eissn is not None: - issns.append(eissn) - - journal = models.Journal.find_by_issn_exact(issns, True) - - # check if only one journal matches pissn and eissn and if they are in the correct fields - # no need to check eissn, if pissn matches, pissn and eissn are different and only 1 journal has been found - then eissn matches too - if len(journal) != 1: - raise exceptions.ArticleNotAcceptable(message=Messages.EXCEPTION_MISMATCHED_ISSNS) - if pissn is not None: - if journal[0].bibjson().pissn != pissn: - raise exceptions.ArticleNotAcceptable(message=Messages.EXCEPTION_MISMATCHED_ISSNS) - if eissn is not None: - if journal[0].bibjson().eissn != eissn: - raise exceptions.ArticleNotAcceptable(message=Messages.EXCEPTION_MISMATCHED_ISSNS) - def create_article(self, article, account, duplicate_check=True, merge_duplicate=True, limit_to_account=True, add_journal_info=False, dry_run=False, update_article_id=None): @@ -285,6 +266,7 @@ def is_acceptable(self, article: models.Article): raise exceptions.ArticleNotAcceptable(message=Messages.EXCEPTION_NO_DOI_NO_FULLTEXT) self._validate_issns(bj) + self.does_article_match_journal(bj) # is journal in doaj (we do this check last as it has more performance impact) journal = article.get_journal() @@ -292,6 +274,28 @@ def is_acceptable(self, article: models.Article): raise exceptions.ArticleNotAcceptable(message=Messages.EXCEPTION_ADDING_ARTICLE_TO_WITHDRAWN_JOURNAL) @staticmethod + def does_article_match_journal(article_bibjson: models.ArticleBibJSON): + pissn = article_bibjson.get_one_identifier("pissn") + eissn = article_bibjson.get_one_identifier("eissn") + + if pissn is not None: + issns.append(pissn) + if eissn is not None: + issns.append(eissn) + + journal = models.Journal.find_by_issn_exact(issns, True) + + # check if only one journal matches pissn and eissn and if they are in the correct fields + # no need to check eissn, if pissn matches, pissn and eissn are different and only 1 journal has been found - then eissn matches too + if len(journal) != 1: + raise exceptions.ArticleNotAcceptable(message=Messages.EXCEPTION_MISMATCHED_ISSNS) + if pissn is not None: + if journal[0].bibjson().pissn != pissn: + raise exceptions.ArticleNotAcceptable(message=Messages.EXCEPTION_MISMATCHED_ISSNS) + if eissn is not None: + if journal[0].bibjson().eissn != eissn: + raise exceptions.ArticleNotAcceptable(message=Messages.EXCEPTION_MISMATCHED_ISSNS) + @staticmethod def is_legitimate_owner(article, owner): """ Determine if the owner id is the owner of the article From 1b6624ac7ae6b362ffa3e243197469f225468742 Mon Sep 17 00:00:00 2001 From: Aga Date: Mon, 26 Jun 2023 15:09:57 +0100 Subject: [PATCH 12/56] naming bugs --- portality/bll/services/article.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/portality/bll/services/article.py b/portality/bll/services/article.py index 2782538a0f..733788d682 100644 --- a/portality/bll/services/article.py +++ b/portality/bll/services/article.py @@ -295,6 +295,8 @@ def does_article_match_journal(article_bibjson: models.ArticleBibJSON): if eissn is not None: if journal[0].bibjson().eissn != eissn: raise exceptions.ArticleNotAcceptable(message=Messages.EXCEPTION_MISMATCHED_ISSNS) + + return journal[0] @staticmethod def is_legitimate_owner(article, owner): """ From a8353b775598762854083c563df679ba152a97c9 Mon Sep 17 00:00:00 2001 From: Aga Date: Mon, 26 Jun 2023 15:10:33 +0100 Subject: [PATCH 13/56] return journal after match with validation and use it in is_acceptable method --- portality/bll/services/article.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/portality/bll/services/article.py b/portality/bll/services/article.py index 733788d682..93b48376f9 100644 --- a/portality/bll/services/article.py +++ b/portality/bll/services/article.py @@ -266,10 +266,10 @@ def is_acceptable(self, article: models.Article): raise exceptions.ArticleNotAcceptable(message=Messages.EXCEPTION_NO_DOI_NO_FULLTEXT) self._validate_issns(bj) - self.does_article_match_journal(bj) + journal = self.does_article_match_journal(bj) # is journal in doaj (we do this check last as it has more performance impact) - journal = article.get_journal() + # journal = article.get_journal() if journal is None or not journal.is_in_doaj(): raise exceptions.ArticleNotAcceptable(message=Messages.EXCEPTION_ADDING_ARTICLE_TO_WITHDRAWN_JOURNAL) From 68719f0184af4d441d26119e7baf3e6370b810ab Mon Sep 17 00:00:00 2001 From: Aga Date: Wed, 28 Jun 2023 11:02:01 +0100 Subject: [PATCH 14/56] add script to find articles with invalid issns --- ...230609_find_articles_with_invalid_issns.py | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 portality/scripts/230609_find_articles_with_invalid_issns.py diff --git a/portality/scripts/230609_find_articles_with_invalid_issns.py b/portality/scripts/230609_find_articles_with_invalid_issns.py new file mode 100644 index 0000000000..c45b8c5d63 --- /dev/null +++ b/portality/scripts/230609_find_articles_with_invalid_issns.py @@ -0,0 +1,66 @@ +from portality import models +from portality.bll.services import article as articlesvc +from portality.bll import exceptions +from portality.core import es_connection +from portality.util import ipt_prefix +import esprit +import csv + +IN_DOAJ = { + "query": { + "bool": { + "must": [ + {"term" : {"admin.in_doaj":True}} + ] + } + } +} + + +if __name__ == "__main__": + + # import argparse + # + # parser = argparse.ArgumentParser() + # parser.add_argument("-o", "--out", help="output file path") + # args = parser.parse_args() + # + # if not args.out: + # print("Please specify an output file path with the -o option") + # parser.print_help() + # exit() + + out = "out.csv" + + with open(out, "w", encoding="utf-8") as f: + writer = csv.writer(f) + writer.writerow(["ID", "PISSN", "EISSN", "Journals found with article's PISSN", "In doaj?", "Journals found with article's EISSN", "In doaj?", "Error"]) + + for a in models.Article.iterate(q=IN_DOAJ, page_size=100, keepalive='5m'): + article = models.Article(_source=a) + bibjson = article.bibjson() + try: + articlesvc.ArticleService._validate_issns(bibjson) + except exceptions.ArticleNotAcceptable as e: + id = article.id + pissn = bibjson.get_identifiers("pissn") + eissn = bibjson.get_identifiers("eissn") + j_p = [j["id"] for j in models.Journal.find_by_issn(pissn)] + j_p_in_doaj = [] + if (j_p): + for j in j_p: + jobj = models.Journal.pull(j) + if (jobj): + j_p_in_doaj.append(jobj.is_in_doaj()) + else: + j_p_in_doaj.append("n/a") + j_e = [j["id"] for j in models.Journal.find_by_issn(eissn)] + j_e_in_doaj = [] + if (j_e): + for j in j_e: + jobj = models.Journal.pull(j) + if (jobj): + j_e_in_doaj.append(jobj.is_in_doaj()) + else: + j_e_in_doaj.append("n/a") + writer.writerow([id, pissn, eissn, j_p, j_p_in_doaj, j_e, j_e_in_doaj, str(e)]) \ No newline at end of file From 6331c1e39f5f150c8bd319cbb5d69f1d28eb66e6 Mon Sep 17 00:00:00 2001 From: Aga Date: Wed, 28 Jun 2023 11:08:21 +0100 Subject: [PATCH 15/56] refactoring --- portality/bll/services/article.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/portality/bll/services/article.py b/portality/bll/services/article.py index 93b48376f9..982aa6d505 100644 --- a/portality/bll/services/article.py +++ b/portality/bll/services/article.py @@ -266,7 +266,7 @@ def is_acceptable(self, article: models.Article): raise exceptions.ArticleNotAcceptable(message=Messages.EXCEPTION_NO_DOI_NO_FULLTEXT) self._validate_issns(bj) - journal = self.does_article_match_journal(bj) + journal = self.match_journal_with_validation(bj) # is journal in doaj (we do this check last as it has more performance impact) # journal = article.get_journal() From 900ecf7b92e283c4884553d2e30ee64140a05149 Mon Sep 17 00:00:00 2001 From: Aga Date: Wed, 28 Jun 2023 11:24:43 +0100 Subject: [PATCH 16/56] fix errors found by UT --- portality/bll/services/article.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/portality/bll/services/article.py b/portality/bll/services/article.py index 982aa6d505..77aac68600 100644 --- a/portality/bll/services/article.py +++ b/portality/bll/services/article.py @@ -159,9 +159,6 @@ def _validate_issns(article_bibjson: models.ArticleBibJSON): if len(pissn) > 1 or len(eissn) > 1: raise exceptions.ArticleNotAcceptable(message=Messages.EXCEPTION_TOO_MANY_ISSNS) - pissn = article_bibjson.get_one_identifier("pissn") - eissn = article_bibjson.get_one_identifier("eissn") - # no pissn or eissn if not pissn and not eissn: raise exceptions.ArticleNotAcceptable(message=Messages.EXCEPTION_NO_ISSNS) @@ -274,10 +271,12 @@ def is_acceptable(self, article: models.Article): raise exceptions.ArticleNotAcceptable(message=Messages.EXCEPTION_ADDING_ARTICLE_TO_WITHDRAWN_JOURNAL) @staticmethod - def does_article_match_journal(article_bibjson: models.ArticleBibJSON): + def match_journal_with_validation(article_bibjson: models.ArticleBibJSON): pissn = article_bibjson.get_one_identifier("pissn") eissn = article_bibjson.get_one_identifier("eissn") + issns = [] + if pissn is not None: issns.append(pissn) if eissn is not None: From b7c6cdd9c065b3ecbcb046e7154b2527c31b40fd Mon Sep 17 00:00:00 2001 From: Aga Date: Mon, 17 Jul 2023 11:33:28 +0100 Subject: [PATCH 17/56] Add copy value button to simple fields; copies displayed value --- portality/static/js/formulaic.js | 20 ++++++++++++++++++- .../templates/application_form/_field.html | 2 +- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/portality/static/js/formulaic.js b/portality/static/js/formulaic.js index 91ded843fb..2124afae01 100644 --- a/portality/static/js/formulaic.js +++ b/portality/static/js/formulaic.js @@ -1196,7 +1196,25 @@ var formulaic = { this.init(); }, newClickToCopy : function(params) { - return + return edges.instantiate(formulaic.widgets.ClickToCopy, params) + }, + ClickToCopy : function(params) { + this.fieldDef = params.fieldDef; + this.form = params.formulaic; + this.ns = "formulaic-clicktocopy" + this.init = function() { + var elements = $(".form__click-to-copy"); + edges.on(elements, "click", this, "copy"); + }; + this.copy = function(element) { + var field = $("input[name=" + this.fieldDef.name + "]") + value_def = this.fieldDef.options.filter(item => item.value === field.val()); + value_to_copy = value_def[0]["display"]; + navigator.clipboard.writeText(value_to_copy) + console.log("text copied: " + value_to_copy) + + }; + this.init(); }, newClickableOwner : function(params) { return edges.instantiate(formulaic.widgets.ClickableOwner, params) diff --git a/portality/templates/application_form/_field.html b/portality/templates/application_form/_field.html index 8896befec5..1bca546ef5 100644 --- a/portality/templates/application_form/_field.html +++ b/portality/templates/application_form/_field.html @@ -9,7 +9,7 @@ More help {% endif %} {% if f.has_widget("click_to_copy") %} - Copy value + Copy value {% endif %} {% if f.optional %}(Optional){% endif %} From b6097579e2b0cb584cb6723069a6b8aa90c789c9 Mon Sep 17 00:00:00 2001 From: Aga Date: Mon, 17 Jul 2023 11:43:31 +0100 Subject: [PATCH 18/56] add confirmation with copied value --- portality/static/js/formulaic.js | 4 +++- portality/templates/application_form/_field.html | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/portality/static/js/formulaic.js b/portality/static/js/formulaic.js index 2124afae01..0e0aae3cfe 100644 --- a/portality/static/js/formulaic.js +++ b/portality/static/js/formulaic.js @@ -1212,7 +1212,9 @@ var formulaic = { value_to_copy = value_def[0]["display"]; navigator.clipboard.writeText(value_to_copy) console.log("text copied: " + value_to_copy) - + var confirmation = $("#copy-confirmation--" + this.fieldDef.name); + confirmation.text("Value copied: " + value_to_copy); + confirmation.show().delay(3000).fadeOut(); }; this.init(); }, diff --git a/portality/templates/application_form/_field.html b/portality/templates/application_form/_field.html index 1bca546ef5..d5f007ba6e 100644 --- a/portality/templates/application_form/_field.html +++ b/portality/templates/application_form/_field.html @@ -10,6 +10,7 @@ {% endif %} {% if f.has_widget("click_to_copy") %} Copy value + {% endif %} {% if f.optional %}(Optional){% endif %} From 567879e0d23f9efa15bcde042d55106635cc9250 Mon Sep 17 00:00:00 2001 From: Aga Date: Mon, 17 Jul 2023 13:07:35 +0100 Subject: [PATCH 19/56] add click_to_copy to all fields; doesn't work for multiple fields and doesn't decode country code --- cms/sass/components/_tag.scss | 5 + portality/forms/application_forms.py | 165 +- portality/static/js/formulaic.js | 1557 +++++++++-------- .../templates/application_form/_field.html | 2 +- 4 files changed, 923 insertions(+), 806 deletions(-) diff --git a/cms/sass/components/_tag.scss b/cms/sass/components/_tag.scss index 1f24ebce92..836e55c7ef 100644 --- a/cms/sass/components/_tag.scss +++ b/cms/sass/components/_tag.scss @@ -90,3 +90,8 @@ color: $white; } } + +.tag--confirmation { + background: $dark-green; + color: $white; +} diff --git a/portality/forms/application_forms.py b/portality/forms/application_forms.py index 687cd9f5e3..15e69bb7bf 100644 --- a/portality/forms/application_forms.py +++ b/portality/forms/application_forms.py @@ -141,7 +141,8 @@ class FieldDefinitions: ], "widgets": [ "trim_whitespace", # ~~^-> TrimWhitespace:FormWidget~~ - "clickable_url" # ~~^-> ClickableURL:FormWidget~~ + "clickable_url", # ~~^-> ClickableURL:FormWidget~~ + "click_to_copy" ], "attr": { "type": "url" @@ -179,7 +180,10 @@ class FieldDefinitions: "update_request": { "disabled": True } - } + }, + "widgets": [ + "click_to_copy", # ~~^-> ClickToCopy:FormWidget~~ + ] } # ~~->$ AlternativeTitle:FormField~~ @@ -202,7 +206,10 @@ class FieldDefinitions: "update_request": { "disabled": True } - } + }, + "widgets": [ + "click_to_copy", # ~~^-> ClickToCopy:FormWidget~~ + ] } # ~~->$ JournalURL:FormField~~ @@ -229,7 +236,10 @@ class FieldDefinitions: "journal_url_in_public_doaj" # ~~^-> JournalURLInPublicDOAJ:FormValidator~~ ], } - } + }, + "widgets": [ + "click_to_copy", # ~~^-> ClickToCopy:FormWidget~~ + ] } #~~->$ PISSN:FormField~~ @@ -255,7 +265,8 @@ class FieldDefinitions: "widgets" : [ "trim_whitespace", # ~~^-> TrimWhitespace:FormWidget~~ "full_contents", # ~~^->FullContents:FormWidget~~ - "issn_link" # ~~^->IssnLink:FormWidget~~ + "issn_link", # ~~^->IssnLink:FormWidget~~ + "click_to_copy" # ~~^-> ClickToCopy:FormWidget~~ ], "contexts": { "public" : { @@ -326,7 +337,8 @@ class FieldDefinitions: "widgets" : [ "trim_whitespace", # ~~^-> TrimWhitespace:FormWidget~~ "full_contents", # ~~^->FullContents:FormWidget~~ - "issn_link" # ~~^->IssnLink:FormWidget~~ + "issn_link", # ~~^->IssnLink:FormWidget~~ + "click_to_copy" # ~~^-> ClickToCopy:FormWidget~~ ], "contexts": { "public" : { @@ -407,7 +419,8 @@ class FieldDefinitions: "stopWords": STOP_WORDS, "field": "bibjson.keywords" } - } + }, + "click_to_copy" # ~~^-> ClickToCopy:FormWidget~~ ], "attr": { "class": "input-xlarge" @@ -431,7 +444,8 @@ class FieldDefinitions: ], "widgets": [ {"select": {}}, - "multiple_field" + "multiple_field", + "click_to_copy" # ~~^-> ClickToCopy:FormWidget~~ ], "help": { "placeholder": "Type or select the language" @@ -452,7 +466,8 @@ class FieldDefinitions: "widgets": [ "trim_whitespace", # ~~^-> TrimWhitespace:FormWidget~~ {"autocomplete": {"type" : "journal", "field": "bibjson.publisher.name.exact"}}, - "full_contents" # ~~^->FullContents:FormWidget~~ + "full_contents", # ~~^->FullContents:FormWidget~~ + "click_to_copy" # ~~^-> ClickToCopy:FormWidget~~ ], "help": { "placeholder": "Type or select the publisher’s name" @@ -480,7 +495,8 @@ class FieldDefinitions: {"required": {"message": "Enter the country where the publisher carries out its business operations and is registered"}} ], "widgets": [ - {"select": {}} + {"select": {}}, + "click_to_copy" # ~~^-> ClickToCopy:FormWidget~~ ], "attr": { "class": "input-xlarge" @@ -511,7 +527,8 @@ class FieldDefinitions: "widgets": [ "trim_whitespace", # ~~^-> TrimWhitespace:FormWidget~~ {"autocomplete": {"type" : "journal", "field": "bibjson.institution.name.exact"}}, - "full_contents" # ~~^->FullContents:FormWidget~~ + "full_contents", # ~~^->FullContents:FormWidget~~ + "click_to_copy" # ~~^-> ClickToCopy:FormWidget~~ ] } @@ -528,7 +545,8 @@ class FieldDefinitions: "placeholder": "Type or select the country" }, "widgets": [ - {"select": {"allow_clear" : True}} + {"select": {"allow_clear" : True}}, + "click_to_copy" # ~~^-> ClickToCopy:FormWidget~~ ], "attr": { "class": "input-xlarge" @@ -579,6 +597,9 @@ class FieldDefinitions: }, "validate": [ {"required": {"message": "Select at least one type of license"}} + ], + "widgets": [ + "click_to_copy" # ~~^-> ClickToCopy:FormWidget~~ ] } @@ -599,7 +620,10 @@ class FieldDefinitions: ], "help": { "doaj_criteria": "Content must be licensed" - } + }, + "widgets": [ + "click_to_copy" # ~~^-> ClickToCopy:FormWidget~~ + ] } # ~~->$ LicenseTermsURL:FormField~~ @@ -619,7 +643,8 @@ class FieldDefinitions: }, "widgets": [ "trim_whitespace", # ~~^-> TrimWhitespace:FormWidget~~ - "clickable_url" # ~~^-> ClickableURL:FormWidget~~ + "clickable_url", # ~~^-> ClickableURL:FormWidget~~ + "click_to_copy" # ~~^-> ClickToCopy:FormWidget~~ ] } @@ -641,6 +666,9 @@ class FieldDefinitions: }, "validate": [ {"required": {"message": "Select Yes or No"}} + ], + "widgets": [ + "click_to_copy" # ~~^-> ClickToCopy:FormWidget~~ ] } @@ -667,7 +695,8 @@ class FieldDefinitions: ], "widgets": [ "trim_whitespace", # ~~^-> TrimWhitespace:FormWidget~~ - "clickable_url" # ~~^-> ClickableURL:FormWidget~~ + "clickable_url", # ~~^-> ClickableURL:FormWidget~~ + "click_to_copy" # ~~^-> ClickToCopy:FormWidget~~ ] } @@ -692,7 +721,10 @@ class FieldDefinitions: "under any license allowed by the journal " "retain all rights."], "seal_criteria": "The author must retain the copyright" - } + }, + "widgets": [ + "click_to_copy" # ~~^-> ClickToCopy:FormWidget~~ + ] } # ~~->$ CopyrightURL:FormField~~ @@ -710,7 +742,8 @@ class FieldDefinitions: ], "widgets": [ "trim_whitespace", # ~~^-> TrimWhitespace:FormWidget~~ - "clickable_url" # ~~^-> ClickableURL:FormWidget~~ + "clickable_url", # ~~^-> ClickableURL:FormWidget~~ + "click_to_copy" # ~~^-> ClickToCopy:FormWidget~~ ], "contexts": { "public": { @@ -752,6 +785,9 @@ class FieldDefinitions: }, "validate": [ {"required": {"message": "Select at least one type of review process"}} + ], + "widgets": [ + "click_to_copy" # ~~^-> ClickToCopy:FormWidget~~ ] } @@ -774,6 +810,7 @@ class FieldDefinitions: ], "widgets" : [ "trim_whitespace", # ~~^-> TrimWhitespace:FormWidget~~ + "click_to_copy" # ~~^-> ClickToCopy:FormWidget~~ ], "asynchronous_warning": [ {"warn_on_value": {"value": "None"}} @@ -796,7 +833,8 @@ class FieldDefinitions: ], "widgets": [ "trim_whitespace", # ~~^-> TrimWhitespace:FormWidget~~ - "clickable_url" # ~~^-> ClickableURL:FormWidget~~ + "clickable_url", # ~~^-> ClickableURL:FormWidget~~ + "click_to_copy" # ~~^-> ClickToCopy:FormWidget~~ ] } @@ -819,7 +857,10 @@ class FieldDefinitions: "attr": { "min": app.config.get('MINIMAL_OA_START_DATE', 1900), "max": dates.now().year - } + }, + "widgets": [ + "click_to_copy" # ~~^-> ClickToCopy:FormWidget~~ + ] } # ~~->$ PlagiarismDetection:FormField~~ @@ -839,6 +880,9 @@ class FieldDefinitions: ], "validate": [ {"required": {"message": "Select Yes or No"}} + ], + "widgets": [ + "click_to_copy" # ~~^-> ClickToCopy:FormWidget~~ ] } @@ -867,7 +911,8 @@ class FieldDefinitions: ], "widgets": [ "trim_whitespace", # ~~^-> TrimWhitespace:FormWidget~~ - "clickable_url" # ~~^-> ClickableURL:FormWidget~~ + "clickable_url", # ~~^-> ClickableURL:FormWidget~~ + "click_to_copy" # ~~^-> ClickToCopy:FormWidget~~ ] } @@ -886,7 +931,8 @@ class FieldDefinitions: ], "widgets": [ "trim_whitespace", # ~~^-> TrimWhitespace:FormWidget~~ - "clickable_url" # ~~^-> ClickableURL:FormWidget~~ + "clickable_url", # ~~^-> ClickableURL:FormWidget~~ + "click_to_copy" # ~~^-> ClickToCopy:FormWidget~~ ] } @@ -905,7 +951,8 @@ class FieldDefinitions: ], "widgets": [ "trim_whitespace", # ~~^-> TrimWhitespace:FormWidget~~ - "clickable_url" # ~~^-> ClickableURL:FormWidget~~ + "clickable_url", # ~~^-> ClickableURL:FormWidget~~ + "click_to_copy" # ~~^-> ClickToCopy:FormWidget~~ ] } @@ -924,7 +971,8 @@ class FieldDefinitions: ], "widgets": [ "trim_whitespace", # ~~^-> TrimWhitespace:FormWidget~~ - "clickable_url" # ~~^-> ClickableURL:FormWidget~~ + "clickable_url", # ~~^-> ClickableURL:FormWidget~~ + "click_to_copy" # ~~^-> ClickToCopy:FormWidget~~ ] } @@ -944,7 +992,10 @@ class FieldDefinitions: "attr": { "min": "1", "max": "100" - } + }, + "widgets": [ + "click_to_copy" # ~~^-> ClickToCopy:FormWidget~~ + ] } # ~~->$ APC:FormField~~ @@ -965,6 +1016,9 @@ class FieldDefinitions: }, "validate": [ {"required": {"message": "Select Yes or No"}} + ], + "widgets": [ + "click_to_copy" # ~~^-> ClickToCopy:FormWidget~~ ] } @@ -993,7 +1047,8 @@ class FieldDefinitions: "template": "application_form/_list.html", "entry_template": "application_form/_entry_group_horizontal.html", "widgets": [ - "multiple_field" + "multiple_field", + "click_to_copy" # ~~^-> ClickToCopy:FormWidget~~ ] } @@ -1009,7 +1064,8 @@ class FieldDefinitions: "placeholder": "Currency" }, "widgets": [ - {"select": {}} + {"select": {}}, + "click_to_copy" # ~~^-> ClickToCopy:FormWidget~~ ], "attr": { "class": "input-xlarge" @@ -1047,7 +1103,10 @@ class FieldDefinitions: ], "attr": { "min": "1" - } + }, + "widgets":[ + "click_to_copy" # ~~^-> ClickToCopy:FormWidget~~ + ] } # ~~->$ APCURL:FormField~~ @@ -1069,7 +1128,8 @@ class FieldDefinitions: ], "widgets": [ "trim_whitespace", # ~~^-> TrimWhitespace:FormWidget~~ - "clickable_url" # ~~^-> ClickableURL:FormWidget~~ + "clickable_url", # ~~^-> ClickableURL:FormWidget~~ + "click_to_copy" # ~~^-> ClickToCopy:FormWidget~~ ] } @@ -1093,6 +1153,9 @@ class FieldDefinitions: }, "validate": [ {"required": {"message": "Select Yes or No"}} + ], + "widgets": [ + "click_to_copy" # ~~^-> ClickToCopy:FormWidget~~ ] } @@ -1121,7 +1184,8 @@ class FieldDefinitions: ], "widgets": [ "trim_whitespace", # ~~^-> TrimWhitespace:FormWidget~~ - "clickable_url" # ~~^-> ClickableURL:FormWidget~~ + "clickable_url", # ~~^-> ClickableURL:FormWidget~~ + "click_to_copy" # ~~^-> ClickToCopy:FormWidget~~ ] } @@ -1142,6 +1206,9 @@ class FieldDefinitions: }, "validate": [ {"required": {"message": "Select Yes or No"}} + ], + "widgets": [ + "click_to_copy" # ~~^-> ClickToCopy:FormWidget~~ ] } @@ -1169,7 +1236,8 @@ class FieldDefinitions: ], "widgets": [ "trim_whitespace", # ~~^-> TrimWhitespace:FormWidget~~ - "clickable_url" # ~~^-> ClickableURL:FormWidget~~ + "clickable_url", # ~~^-> ClickableURL:FormWidget~~ + "click_to_copy" # ~~^-> ClickToCopy:FormWidget~~ ] } @@ -1201,6 +1269,9 @@ class FieldDefinitions: }, "validate": [ {"required": {"message": "Select at least one option"}} + ], + "widgets": [ + "click_to_copy" # ~~^-> ClickToCopy:FormWidget~~ ] } @@ -1230,7 +1301,8 @@ class FieldDefinitions: ], "widgets": [ "trim_whitespace", # ~~^-> TrimWhitespace:FormWidget~~ - "multiple_field" + "multiple_field", + "click_to_copy" # ~~^-> ClickToCopy:FormWidget~~ ], "attr": { "class": "input-xlarge unstyled-list" @@ -1255,7 +1327,8 @@ class FieldDefinitions: {"warn_on_value": {"value": "None"}} ], "widgets" : [ - "trim_whitespace" # ~~^-> TrimWhitespace:FormWidget~~ + "trim_whitespace", # ~~^-> TrimWhitespace:FormWidget~~ + "click_to_copy" # ~~^-> ClickToCopy:FormWidget~~ ] } @@ -1302,7 +1375,8 @@ class FieldDefinitions: ], "widgets": [ "trim_whitespace", # ~~^-> TrimWhitespace:FormWidget~~ - "clickable_url" # ~~^-> ClickableURL:FormWidget~~ + "clickable_url", # ~~^-> ClickableURL:FormWidget~~ + "click_to_copy" # ~~^-> ClickToCopy:FormWidget~~ ] } @@ -1333,6 +1407,9 @@ class FieldDefinitions: ]}, "validate": [ {"required": {"message": "Select at least one option"}} + ], + "widgets": [ + "click_to_copy" # ~~^-> ClickToCopy:FormWidget~~ ] } @@ -1354,7 +1431,8 @@ class FieldDefinitions: {"warn_on_value": {"value": "None"}} ], "widgets" : [ - "trim_whitespace" # ~~^-> TrimWhitespace:FormWidget~~ + "trim_whitespace", # ~~^-> TrimWhitespace:FormWidget~~ + "click_to_copy" # ~~^-> ClickToCopy:FormWidget~~ ] } @@ -1380,7 +1458,8 @@ class FieldDefinitions: ], "widgets": [ "trim_whitespace", # ~~^-> TrimWhitespace:FormWidget~~ - "clickable_url" # ~~^-> ClickableURL:FormWidget~~ + "clickable_url", # ~~^-> ClickableURL:FormWidget~~ + "click_to_copy" # ~~^-> ClickToCopy:FormWidget~~ ], "contexts" : { "public" : { @@ -1440,6 +1519,9 @@ class FieldDefinitions: }, "validate": [ {"required": {"message": "Select at least one option"}} + ], + "widgets": [ + "click_to_copy" # ~~^-> ClickToCopy:FormWidget~~ ] } @@ -1461,7 +1543,8 @@ class FieldDefinitions: {"warn_on_value": {"value": "None"}} ], "widgets" : [ - "trim_whitespace" # ~~^-> TrimWhitespace:FormWidget~~ + "trim_whitespace", # ~~^-> TrimWhitespace:FormWidget~~ + "click_to_copy" # ~~^-> ClickToCopy:FormWidget~~ ] } @@ -1490,7 +1573,10 @@ class FieldDefinitions: {"required": {"message": "Select Yes or No"}} ] } - } + }, + "widgets": [ + "click_to_copy" # ~~^-> ClickToCopy:FormWidget~~ + ] } # ~~->$ OpenCitations:FormField~~ @@ -1517,7 +1603,10 @@ class FieldDefinitions: {"required": {"message": "Select Yes or No"}} ] } - } + }, + "widgets": [ + "click_to_copy" # ~~^-> ClickToCopy:FormWidget~~ + ] } ####################################### diff --git a/portality/static/js/formulaic.js b/portality/static/js/formulaic.js index 0e0aae3cfe..ccff926425 100644 --- a/portality/static/js/formulaic.js +++ b/portality/static/js/formulaic.js @@ -1203,243 +1203,266 @@ var formulaic = { this.form = params.formulaic; this.ns = "formulaic-clicktocopy" this.init = function() { - var elements = $(".form__click-to-copy"); + var elements = $("#click-to-copy--" + this.fieldDef.name); edges.on(elements, "click", this, "copy"); }; this.copy = function(element) { - var field = $("input[name=" + this.fieldDef.name + "]") - value_def = this.fieldDef.options.filter(item => item.value === field.val()); - value_to_copy = value_def[0]["display"]; - navigator.clipboard.writeText(value_to_copy) - console.log("text copied: " + value_to_copy) - var confirmation = $("#copy-confirmation--" + this.fieldDef.name); - confirmation.text("Value copied: " + value_to_copy); - confirmation.show().delay(3000).fadeOut(); - }; - this.init(); - }, - newClickableOwner : function(params) { - return edges.instantiate(formulaic.widgets.ClickableOwner, params) - }, - ClickableOwner : function(params) { - this.fieldDef = params.fieldDef; - this.form = params.formulaic; - - this.ns = "formulaic-clickableowner"; - - this.link = false; - - this.init = function() { - var elements = this.form.controlSelect.input({name: this.fieldDef.name}); - edges.on(elements, "change.ClickableOwner", this, "updateOwner"); - for (var i = 0; i < elements.length; i++) { - this.updateOwner(elements[i]); + // todo: multiple fields! + if (this.fieldDef.input == "radio") { + var field = $("input[name=" + this.fieldDef.name + "]:checked"); + value_def = this.fieldDef.options.filter(item => item.value === field.val()); + value_to_copy = value_def[0]["display"]; } - }; + else if (this.fieldDef.input == "checkbox") { + value_to_copy = "" + $( "input[name=" + this.fieldDef.name + "]:checked" ).each(function() { + value_to_copy = value_to_copy + " " + $(this)[0].value; + }); + console.log(value_to_copy) + } + else if (this.fieldDef.input == "taglist") { + var field = $("#" + this.fieldDef.name); + value_to_copy = field.val() + } + else if (this.fieldDef.input == "select") { + // todo: countries value instead of code! + var field = $("select[name=" + this.fieldDef.name + "]"); + value_to_copy = field.val() + } + else { + var field = $("input[name=" + this.fieldDef.name + "]"); + value_to_copy = field.val() + } + navigator.clipboard.writeText(value_to_copy) + console.log("text copied: " + value_to_copy) + var confirmation = $("#copy-confirmation--" + this.fieldDef.name); + confirmation.text("Value copied: " + value_to_copy); + confirmation.show().delay(3000).fadeOut(); + }; + this.init(); + }, + newClickableOwner : function(params) { + return edges.instantiate(formulaic.widgets.ClickableOwner, params) + }, + ClickableOwner : function(params) { + this.fieldDef = params.fieldDef; + this.form = params.formulaic; + + this.ns = "formulaic-clickableowner"; + + this.link = false; + + this.init = function() { + var elements = this.form.controlSelect.input({name: this.fieldDef.name}); + edges.on(elements, "change.ClickableOwner", this, "updateOwner"); + for (var i = 0; i < elements.length; i++) { + this.updateOwner(elements[i]); + } + }; - this.updateOwner = function(element) { - var that = $(element); - var val = that.val(); + this.updateOwner = function(element) { + var that = $(element); + var val = that.val(); - if (val) { - if (this.link) { - this.link.attr("href", "/account/" + val); - } else { - var classes = edges.css_classes(this.ns, "visit"); - var id = edges.css_id(this.ns, this.fieldDef.name); - that.after('

See this account’s profile

'); + if (val) { + if (this.link) { + this.link.attr("href", "/account/" + val); + } else { + var classes = edges.css_classes(this.ns, "visit"); + var id = edges.css_id(this.ns, this.fieldDef.name); + that.after('

See this account’s profile

'); - var selector = edges.css_id_selector(this.ns, this.fieldDef.name); - this.link = $(selector, this.form.context); + var selector = edges.css_id_selector(this.ns, this.fieldDef.name); + this.link = $(selector, this.form.context); + } + } else if (this.link) { + this.link.remove(); + this.link = false; } - } else if (this.link) { - this.link.remove(); - this.link = false; - } - }; + }; - this.init(); - }, + this.init(); + }, - newTrimWhitespace : function(params) { - return edges.instantiate(formulaic.widgets.TrimWhitespace, params) - }, - TrimWhitespace : function(params) { - this.fieldDef = params.fieldDef; - this.form = params.formulaic; + newTrimWhitespace : function(params) { + return edges.instantiate(formulaic.widgets.TrimWhitespace, params) + }, + TrimWhitespace : function(params) { + this.fieldDef = params.fieldDef; + this.form = params.formulaic; - this.ns = "formulaic-trimwhitespace"; + this.ns = "formulaic-trimwhitespace"; - this.link = false; + this.link = false; - this.init = function () { - var elements = this.form.controlSelect.input({name: this.fieldDef.name}); - edges.on(elements, "focus.TrimWhitespace", this, "trim"); - edges.on(elements, "blur.TrimWhitespace", this, "trim"); + this.init = function () { + var elements = this.form.controlSelect.input({name: this.fieldDef.name}); + edges.on(elements, "focus.TrimWhitespace", this, "trim"); + edges.on(elements, "blur.TrimWhitespace", this, "trim"); - for (var i = 0; i < elements.length; i++) { - this.trim(elements[i]); - } - }; + for (var i = 0; i < elements.length; i++) { + this.trim(elements[i]); + } + }; - this.trim = function(element) { - var that = $(element); - var val = that.val(); - var nv = val.trim(); - if (nv !== val) { - that.val(nv); - } - }; + this.trim = function(element) { + var that = $(element); + var val = that.val(); + var nv = val.trim(); + if (nv !== val) { + that.val(nv); + } + }; - this.init(); - }, + this.init(); + }, - newClickableUrl : function(params) { - return edges.instantiate(formulaic.widgets.ClickableUrl, params) - }, - ClickableUrl : function(params) { - this.fieldDef = params.fieldDef; - this.form = params.formulaic; + newClickableUrl : function(params) { + return edges.instantiate(formulaic.widgets.ClickableUrl, params) + }, + ClickableUrl : function(params) { + this.fieldDef = params.fieldDef; + this.form = params.formulaic; - this.ns = "formulaic-clickableurl"; + this.ns = "formulaic-clickableurl"; - this.link = false; + this.link = false; - this.init = function() { - var elements = this.form.controlSelect.input( - {name: this.fieldDef.name}); - // TODO: should work as-you-type by changing "change" to "keyup" event; doesn't work in edges - //edges.on(elements, "change.ClickableUrl", this, "updateUrl"); - edges.on(elements, "keyup.ClickableUrl", this, "updateUrl"); - - for (var i = 0; i < elements.length; i++) { - this.updateUrl(elements[i]); - } - }; - - this.updateUrl = function(element) { - var that = $(element); - var val = that.val(); - var id = edges.css_id(this.ns, this.fieldDef.name); - - if (val && (val.substring(0,7) === "http://" || val.substring(0,8) === "https://") && val.length > 10) { - if (this.link) { - this.link.text(val); - this.link.attr("href", val); - } else { - var classes = edges.css_classes(this.ns, "visit"); - that.after('

' + val + '

'); + this.init = function() { + var elements = this.form.controlSelect.input( + {name: this.fieldDef.name}); + // TODO: should work as-you-type by changing "change" to "keyup" event; doesn't work in edges + //edges.on(elements, "change.ClickableUrl", this, "updateUrl"); + edges.on(elements, "keyup.ClickableUrl", this, "updateUrl"); - var selector = edges.css_id_selector(this.ns, this.fieldDef.name); - this.link = $(selector, this.form.context); + for (var i = 0; i < elements.length; i++) { + this.updateUrl(elements[i]); } - } else if (this.link) { - this.link.remove(); - this.link = false; - } - }; - - this.init(); - }, + }; - newFullContents : function(params) { - return edges.instantiate(formulaic.widgets.FullContents, params) - }, - FullContents : function(params) { - this.fieldDef = params.fieldDef; - this.form = params.formulaic; - this.args = params.args; + this.updateUrl = function(element) { + var that = $(element); + var val = that.val(); + var id = edges.css_id(this.ns, this.fieldDef.name); - this.ns = "formulaic-fullcontents"; + if (val && (val.substring(0,7) === "http://" || val.substring(0,8) === "https://") && val.length > 10) { + if (this.link) { + this.link.text(val); + this.link.attr("href", val); + } else { + var classes = edges.css_classes(this.ns, "visit"); + that.after('

' + val + '

'); - this.container = false; + var selector = edges.css_id_selector(this.ns, this.fieldDef.name); + this.link = $(selector, this.form.context); + } + } else if (this.link) { + this.link.remove(); + this.link = false; + } + }; - this.init = function() { - var elements = this.form.controlSelect.input({name: this.fieldDef.name}); - edges.on(elements, "keyup.FullContents", this, "updateContents"); + this.init(); + }, - for (var i = 0; i < elements.length; i++) { - this.updateContents(elements[i]); - } - }; + newFullContents : function(params) { + return edges.instantiate(formulaic.widgets.FullContents, params) + }, + FullContents : function(params) { + this.fieldDef = params.fieldDef; + this.form = params.formulaic; + this.args = params.args; - this.updateContents = function(element) { - var that = $(element); - var val = that.val(); + this.ns = "formulaic-fullcontents"; - // if there is a behaviour for when the field is empty and disabled, then check if it is, and if - // it is include the desired alternative text - if (this.args && this.args.empty_disabled) { - if (val === "" && that.prop("disabled")) { - val = this.args.empty_disabled; - } - } + this.container = false; - if (val) { - if (this.container) { - this.container.html('Full contents: ' + edges.escapeHtml(val) + ''); - } else { - var classes = edges.css_classes(this.ns, "contents"); - var id = edges.css_id(this.ns, this.fieldDef.name); - that.after('

Full contents: ' + edges.escapeHtml(val) + '

'); + this.init = function() { + var elements = this.form.controlSelect.input({name: this.fieldDef.name}); + edges.on(elements, "keyup.FullContents", this, "updateContents"); - var selector = edges.css_id_selector(this.ns, this.fieldDef.name); - this.container = $(selector, this.form.context); + for (var i = 0; i < elements.length; i++) { + this.updateContents(elements[i]); } - } else if (this.container) { - this.container.remove(); - this.container = false; - } - }; + }; - this.init(); - }, + this.updateContents = function(element) { + var that = $(element); + var val = that.val(); - newNoteModal : function(params) { - return edges.instantiate(formulaic.widgets.NoteModal, params) - }, - NoteModal : function(params) { - this.fieldDef = params.fieldDef; - this.form = params.formulaic; + // if there is a behaviour for when the field is empty and disabled, then check if it is, and if + // it is include the desired alternative text + if (this.args && this.args.empty_disabled) { + if (val === "" && that.prop("disabled")) { + val = this.args.empty_disabled; + } + } - this.ns = "formulaic-notemodal"; + if (val) { + if (this.container) { + this.container.html('Full contents: ' + edges.escapeHtml(val) + ''); + } else { + var classes = edges.css_classes(this.ns, "contents"); + var id = edges.css_id(this.ns, this.fieldDef.name); + that.after('

Full contents: ' + edges.escapeHtml(val) + '

'); - this.container = false; + var selector = edges.css_id_selector(this.ns, this.fieldDef.name); + this.container = $(selector, this.form.context); + } + } else if (this.container) { + this.container.remove(); + this.container = false; + } + }; - this.init = function() { - var viewClass = edges.css_classes(this.ns, "view"); - var closeClass = edges.css_classes(this.ns, "close"); - let group = $("div[name='" + this.fieldDef["name"] + "__group']") - var textarea = group.find("textarea"); - - let inputs = group.find("input[type=text]") - for (let i = 0; i < inputs.length; i++) { - let jqin = $(inputs[i]); - let iid = jqin.attr("id"); - if (iid.endsWith("_author")) { - let val = jqin.val() - if (val === "") { - jqin.hide(); + this.init(); + }, + + newNoteModal : function(params) { + return edges.instantiate(formulaic.widgets.NoteModal, params) + }, + NoteModal : function(params) { + this.fieldDef = params.fieldDef; + this.form = params.formulaic; + + this.ns = "formulaic-notemodal"; + + this.container = false; + + this.init = function() { + var viewClass = edges.css_classes(this.ns, "view"); + var closeClass = edges.css_classes(this.ns, "close"); + let group = $("div[name='" + this.fieldDef["name"] + "__group']") + var textarea = group.find("textarea"); + + let inputs = group.find("input[type=text]") + for (let i = 0; i < inputs.length; i++) { + let jqin = $(inputs[i]); + let iid = jqin.attr("id"); + if (iid.endsWith("_author")) { + let val = jqin.val() + if (val === "") { + jqin.hide(); + } } } - } - for (var i = 0; i < textarea.length; i++) { - var container = $(textarea[i]); + for (var i = 0; i < textarea.length; i++) { + var container = $(textarea[i]); - let contentHeight = container[0].scrollHeight; - if (contentHeight > 200) { - contentHeight = 200; - } - container.css("height", (contentHeight + 5) + "px"); + let contentHeight = container[0].scrollHeight; + if (contentHeight > 200) { + contentHeight = 200; + } + container.css("height", (contentHeight + 5) + "px"); - var modalId = "modal-" + this.fieldDef["name"] + "-" + i; + var modalId = "modal-" + this.fieldDef["name"] + "-" + i; - var date = $("#" + this.fieldDef["name"] + "-" + i + "-note_date"); - var note = $("#" + this.fieldDef["name"] + "-" + i + "-note"); - var author = $("#" + this.fieldDef["name"] + "-" + i + "-note_author"); + var date = $("#" + this.fieldDef["name"] + "-" + i + "-note_date"); + var note = $("#" + this.fieldDef["name"] + "-" + i + "-note"); + var author = $("#" + this.fieldDef["name"] + "-" + i + "-note_author"); - $(` + $(` `).insertAfter(container); - } - - var viewSelector = edges.css_class_selector(this.ns, "view"); - edges.on(viewSelector, "click", this, "showModal"); - - var closeSelector = edges.css_class_selector(this.ns, "close"); - edges.on(closeSelector, "click", this, "closeModal"); - }; + } - this.showModal = function(element) { - var that = $(element); - var modal = that.siblings(".modal"); - modal.show(); - }; + var viewSelector = edges.css_class_selector(this.ns, "view"); + edges.on(viewSelector, "click", this, "showModal"); - this.closeModal = function(element) { - var that = $(element); - var modal = that.parents(".modal"); - modal.hide(); - }; + var closeSelector = edges.css_class_selector(this.ns, "close"); + edges.on(closeSelector, "click", this, "closeModal"); + }; - this.init(); - }, + this.showModal = function(element) { + var that = $(element); + var modal = that.siblings(".modal"); + modal.show(); + }; - newInfiniteRepeat : function(params) { - return edges.instantiate(formulaic.widgets.InfiniteRepeat, params) - }, - InfiniteRepeat: function(params) { - this.fieldDef = params.fieldDef; - this.args = params.args; + this.closeModal = function(element) { + var that = $(element); + var modal = that.parents(".modal"); + modal.hide(); + }; - this.idRx = /(.+?-)(\d+)(-.+)/; - this.template = ""; - this.container = false; - this.divs = false; + this.init(); + }, + + newInfiniteRepeat : function(params) { + return edges.instantiate(formulaic.widgets.InfiniteRepeat, params) + }, + InfiniteRepeat: function(params) { + this.fieldDef = params.fieldDef; + this.args = params.args; + + this.idRx = /(.+?-)(\d+)(-.+)/; + this.template = ""; + this.container = false; + this.divs = false; + + this.init = function() { + this.divs = $("div[name='" + this.fieldDef["name"] + "__group']"); + for (var i = 0 ; i < this.divs.length; i++) { + var div = $(this.divs[i]); + div.append($('')); + feather.replace(); + } - this.init = function() { - this.divs = $("div[name='" + this.fieldDef["name"] + "__group']"); - for (var i = 0 ; i < this.divs.length; i++) { - var div = $(this.divs[i]); - div.append($('')); - feather.replace(); - } + this.template = $(this.divs[0]).html(); + this.container = $(this.divs[0]).parents(".removable-fields"); - this.template = $(this.divs[0]).html(); - this.container = $(this.divs[0]).parents(".removable-fields"); - - if (this.divs.length === 1) { - let div = $(this.divs[0]); - let inputs = div.find(":input"); - let tripwire = false; - for (var i = 0; i < inputs.length; i++) { - if ($(inputs[i]).val()) { - tripwire = true; - break; + if (this.divs.length === 1) { + let div = $(this.divs[0]); + let inputs = div.find(":input"); + let tripwire = false; + for (var i = 0; i < inputs.length; i++) { + if ($(inputs[i]).val()) { + tripwire = true; + break; + } + } + if (!tripwire) { + // the field is empty + $(this.divs[0]).remove(); + this.divs = []; } } - if (!tripwire) { - // the field is empty - $(this.divs[0]).remove(); - this.divs = []; - } - } - this.addFieldBtn = $("#add_field__" + this.fieldDef["name"]); - this.removeFieldBtns = $('[id^="remove_field__' + this.fieldDef["name"] + '"]'); + this.addFieldBtn = $("#add_field__" + this.fieldDef["name"]); + this.removeFieldBtns = $('[id^="remove_field__' + this.fieldDef["name"] + '"]'); - edges.on(this.addFieldBtn, "click", this, "addField"); - edges.on(this.removeFieldBtns, "click", this, "removeField"); + edges.on(this.addFieldBtn, "click", this, "addField"); + edges.on(this.removeFieldBtns, "click", this, "removeField"); - // show or hide the remove buttons - for (let i = 0; i < this.divs.length; i++) { - let cur_div = $(this.divs[i]); - if (!cur_div.find('textarea').is(':disabled')) { - cur_div.find('[id^="remove_field__"]').show(); + // show or hide the remove buttons + for (let i = 0; i < this.divs.length; i++) { + let cur_div = $(this.divs[i]); + if (!cur_div.find('textarea').is(':disabled')) { + cur_div.find('[id^="remove_field__"]').show(); + } } - } - }; + }; - this.addField = function() { - var currentLargest = -1; - for (var i = 0; i < this.divs.length; i++) { - var div = $(this.divs[i]); - var id = div.find(":input").attr("id"); - var match = id.match(this.idRx); - var thisId = parseInt(match[2]); - if (thisId > currentLargest) { - currentLargest = thisId; + this.addField = function() { + var currentLargest = -1; + for (var i = 0; i < this.divs.length; i++) { + var div = $(this.divs[i]); + var id = div.find(":input").attr("id"); + var match = id.match(this.idRx); + var thisId = parseInt(match[2]); + if (thisId > currentLargest) { + currentLargest = thisId; + } } - } - var newId = currentLargest + 1; - - var frag = '
' + this.template + '
'; - var jqt = $(frag); - var that = this; - jqt.find(":input").each(function() { - var el = $(this); - var id = el.attr("id"); + var newId = currentLargest + 1; - var match = id.match(that.idRx); - if (match) { - var bits = id.split(that.idRx); - var newName = bits[1] + newId + bits[3]; - el.attr("id", newName).attr("name", newName).val(""); - } else { - // could be the remove button - if (id.substring(0, "remove_field".length) === "remove_field") { - el.attr("id", "remove_field__" + that.fieldDef["name"] + "--id_" + newId); - el.show(); + var frag = '
' + this.template + '
'; + var jqt = $(frag); + var that = this; + jqt.find(":input").each(function() { + var el = $(this); + var id = el.attr("id"); + + var match = id.match(that.idRx); + if (match) { + var bits = id.split(that.idRx); + var newName = bits[1] + newId + bits[3]; + el.attr("id", newName).attr("name", newName).val(""); + } else { + // could be the remove button + if (id.substring(0, "remove_field".length) === "remove_field") { + el.attr("id", "remove_field__" + that.fieldDef["name"] + "--id_" + newId); + el.show(); + } + } + }); + if (this.args.enable_on_repeat) { + for (var i = 0; i < this.args.enable_on_repeat.length; i++) { + var enables = jqt.find(that.args.enable_on_repeat[i]); + enables.removeAttr("disabled"); } } - }); - if (this.args.enable_on_repeat) { - for (var i = 0; i < this.args.enable_on_repeat.length; i++) { - var enables = jqt.find(that.args.enable_on_repeat[i]); - enables.removeAttr("disabled"); - } - } - var topPlacement = this.fieldDef.repeatable.add_button_placement === "top"; - if (this.divs.length > 0) { - if (topPlacement) { - $(this.divs[0]).before(jqt); + var topPlacement = this.fieldDef.repeatable.add_button_placement === "top"; + if (this.divs.length > 0) { + if (topPlacement) { + $(this.divs[0]).before(jqt); + } else { + $(this.divs[this.divs.length - 1]).after(jqt); + } } else { - $(this.divs[this.divs.length - 1]).after(jqt); + this.container.append(jqt); } - } else { - this.container.append(jqt); - } - this.divs = $("div[name='" + this.fieldDef["name"] + "__group']"); - this.removeFieldBtns = $('[id^="remove_field__' + this.fieldDef["name"] + '"]'); - edges.on(this.removeFieldBtns, "click", this, "removeField"); - }; + this.divs = $("div[name='" + this.fieldDef["name"] + "__group']"); + this.removeFieldBtns = $('[id^="remove_field__' + this.fieldDef["name"] + '"]'); + edges.on(this.removeFieldBtns, "click", this, "removeField"); + }; - this.removeField = function(element) { - var container = $(element).parents("div[name='" + this.fieldDef["name"] + "__group']"); - container.remove(); - this.divs = $("div[name='" + this.fieldDef["name"] + "__group']"); - }; + this.removeField = function(element) { + var container = $(element).parents("div[name='" + this.fieldDef["name"] + "__group']"); + container.remove(); + this.divs = $("div[name='" + this.fieldDef["name"] + "__group']"); + }; - this.init(); - }, + this.init(); + }, - newMultipleField : function(params) { - return edges.instantiate(formulaic.widgets.MultipleField, params) - }, - MultipleField: function(params) { - this.fieldDef = params.fieldDef; - this.max = this.fieldDef["repeatable"]["initial"] - 1; + newMultipleField : function(params) { + return edges.instantiate(formulaic.widgets.MultipleField, params) + }, + MultipleField: function(params) { + this.fieldDef = params.fieldDef; + this.max = this.fieldDef["repeatable"]["initial"] - 1; - this.init = () => { - if (this.fieldDef["input"] === "group") { - this._setupRepeatingGroup(); - } else { - this._setupRepeatingIndividual(); - } - feather.replace(); - }; + this.init = () => { + if (this.fieldDef["input"] === "group") { + this._setupRepeatingGroup(); + } else { + this._setupRepeatingIndividual(); + } + feather.replace(); + }; - this._setupIndividualSelect2 = function() { - for (var idx = 0; idx < this.fields.length; idx++) { - let f = this.fields[idx]; - let s2_input = $($(f).select2()); - $(f).on("focus", formulaic.widgets._select2_shift_focus); - s2_input.after($('')); - if (idx !== 0) { - s2_input.attr("required", false); - s2_input.attr("data-parsley-validate-if-empty", "true"); - if (!s2_input.val()) { - s2_input.closest('li').hide(); - } else { - this.count++; + this._setupIndividualSelect2 = function() { + for (var idx = 0; idx < this.fields.length; idx++) { + let f = this.fields[idx]; + let s2_input = $($(f).select2()); + $(f).on("focus", formulaic.widgets._select2_shift_focus); + s2_input.after($('')); + if (idx !== 0) { + s2_input.attr("required", false); + s2_input.attr("data-parsley-validate-if-empty", "true"); + if (!s2_input.val()) { + s2_input.closest('li').hide(); + } else { + this.count++; + } } } - } - this.remove_btns = $('[id^="remove_field__' + this.fieldDef["name"] + '"]'); - if (this.count === 0) { - $(this.remove_btns[0]).hide(); - } - - this.addFieldBtn = $("#add_field__" + this.fieldDef["name"]); - this.addFieldBtn.on("click", () => { - $('#s2id_' + this.fieldDef["name"] + '-' + (this.count + 1)).closest('li').show(); - this.count++; - if (this.count > 0) { - $(this.remove_btns[0]).show(); - } - if (this.count >= this.max) { - $(this.addFieldBtn).hide(); + this.remove_btns = $('[id^="remove_field__' + this.fieldDef["name"] + '"]'); + if (this.count === 0) { + $(this.remove_btns[0]).hide(); } - }); - if (this.count >= this.max) { - this.addFieldBtn.hide(); - } - - $(this.remove_btns).each((idx, btn) => { - $(btn).on("click", (event) => { - for (let i = idx; i < this.count; i++) { - let data = $(this.fields[i + 1]).select2('data'); - if (data === null) { - data = {id: i, text: ""}; - } - $(this.fields[i]).select2('data', {id: data.id, text: data.text}); - } - this.count--; - $(this.fields[this.count + 1]).select2('data', {id: this.count + 1, text: ""}); - $('#s2id_' + this.fieldDef["name"] + '-' + (this.count + 1)).closest('li').hide(); - if (this.count === 0) { - $(this.remove_btns[0]).hide(); + this.addFieldBtn = $("#add_field__" + this.fieldDef["name"]); + this.addFieldBtn.on("click", () => { + $('#s2id_' + this.fieldDef["name"] + '-' + (this.count + 1)).closest('li').show(); + this.count++; + if (this.count > 0) { + $(this.remove_btns[0]).show(); } - if (this.count < this.max) { - $(this.addFieldBtn).show(); - } - }) - }) - }; - - this._setupIndividualField = function() { - for (var idx = 0; idx < this.fields.length; idx++) { - let f = this.fields[idx]; - let jqf = $(f); - jqf.after($('')); - if (idx !== 0) { - jqf.attr("required", false); - jqf.attr("data-parsley-validate-if-empty", "true"); - if (!jqf.val()) { - jqf.parent().hide(); - } else { - this.count++; + if (this.count >= this.max) { + $(this.addFieldBtn).hide(); } - } - } - - this.remove_btns = $('[id^="remove_field__' + this.fieldDef["name"] + '-"]'); - if (this.count === 0) { - $(this.remove_btns[0]).hide(); - } + }); - this.addFieldBtn = $("#add_field__" + this.fieldDef["name"]); - this.addFieldBtn.on("click", () => { - let next_input = $('[id="' + this.fieldDef["name"] + '-' + (this.count + 1) +'"]').parent(); - // TODO: why .show() does not work? - $(next_input).show(); - this.count++; - if (this.count > 0) { - $(this.remove_btns[0]).show(); - } if (this.count >= this.max) { - $(this.addFieldBtn).hide(); + this.addFieldBtn.hide(); } - }); - if (this.count >= this.max) { - this.addFieldBtn.hide(); - } + $(this.remove_btns).each((idx, btn) => { + $(btn).on("click", (event) => { + for (let i = idx; i < this.count; i++) { + let data = $(this.fields[i + 1]).select2('data'); + if (data === null) { + data = {id: i, text: ""}; + } + $(this.fields[i]).select2('data', {id: data.id, text: data.text}); + } + this.count--; + $(this.fields[this.count + 1]).select2('data', {id: this.count + 1, text: ""}); + $('#s2id_' + this.fieldDef["name"] + '-' + (this.count + 1)).closest('li').hide(); + if (this.count === 0) { + $(this.remove_btns[0]).hide(); + } + if (this.count < this.max) { + $(this.addFieldBtn).show(); + } + }) + }) + }; - $(this.remove_btns).each((idx, btn) => { - $(btn).on("click", (event) => { - for (let i = idx; i < this.count; i++) { - let data = $(this.fields[i + 1]).val(); - if (data === null) { - data = ""; + this._setupIndividualField = function() { + for (var idx = 0; idx < this.fields.length; idx++) { + let f = this.fields[idx]; + let jqf = $(f); + jqf.after($('')); + if (idx !== 0) { + jqf.attr("required", false); + jqf.attr("data-parsley-validate-if-empty", "true"); + if (!jqf.val()) { + jqf.parent().hide(); + } else { + this.count++; } - $(this.fields[i]).val(data); } + } + + this.remove_btns = $('[id^="remove_field__' + this.fieldDef["name"] + '-"]'); + if (this.count === 0) { + $(this.remove_btns[0]).hide(); + } - this.count--; - $(this.fields[this.count + 1]).val(""); - let last_input = $('[id="' + this.fieldDef["name"] + '-' + (this.count + 1) +'"]').parent(); - $(last_input).hide(); - if (this.count === 0) { - $(this.remove_btns[0]).hide(); + this.addFieldBtn = $("#add_field__" + this.fieldDef["name"]); + this.addFieldBtn.on("click", () => { + let next_input = $('[id="' + this.fieldDef["name"] + '-' + (this.count + 1) +'"]').parent(); + // TODO: why .show() does not work? + $(next_input).show(); + this.count++; + if (this.count > 0) { + $(this.remove_btns[0]).show(); } - if (this.count < this.max) { - $(this.addFieldBtn).show(); + if (this.count >= this.max) { + $(this.addFieldBtn).hide(); } - }) - }); - }; + }); - this._setupRepeatingIndividual = function() { - let tag = this.fieldDef["input"] === "select" ? "select" : "input"; - this.fields = $(tag + '[id^="' + this.fieldDef["name"] + '-"]'); - this.count = 0; + if (this.count >= this.max) { + this.addFieldBtn.hide(); + } - if (tag === "select"){ - this._setupIndividualSelect2(); - } else { - this._setupIndividualField(); - } - }; + $(this.remove_btns).each((idx, btn) => { + $(btn).on("click", (event) => { + for (let i = idx; i < this.count; i++) { + let data = $(this.fields[i + 1]).val(); + if (data === null) { + data = ""; + } + $(this.fields[i]).val(data); + } - this._setupRepeatingGroup = function() { - this.divs = $("div[name='" + this.fieldDef["name"] + "__group']"); - this.count = 0; + this.count--; + $(this.fields[this.count + 1]).val(""); + let last_input = $('[id="' + this.fieldDef["name"] + '-' + (this.count + 1) +'"]').parent(); + $(last_input).hide(); + if (this.count === 0) { + $(this.remove_btns[0]).hide(); + } + if (this.count < this.max) { + $(this.addFieldBtn).show(); + } + }) + }); + }; - for (var idx = 0; idx < this.divs.length; idx++) { - let div = $(this.divs[idx]); - div.append($('')); + this._setupRepeatingIndividual = function() { + let tag = this.fieldDef["input"] === "select" ? "select" : "input"; + this.fields = $(tag + '[id^="' + this.fieldDef["name"] + '-"]'); + this.count = 0; - if (idx !== 0) { - let inputs = div.find(":input"); - var hasVal = false; - for (var j = 0; j < inputs.length; j++) { - $(inputs[j]).attr("required", false) - .attr("data-parsley-required-if", false) - .attr("data-parsley-validate-if-empty", "true"); - if ($(inputs[j]).val()) { - hasVal = true; + if (tag === "select"){ + this._setupIndividualSelect2(); + } else { + this._setupIndividualField(); + } + }; + + this._setupRepeatingGroup = function() { + this.divs = $("div[name='" + this.fieldDef["name"] + "__group']"); + this.count = 0; + + for (var idx = 0; idx < this.divs.length; idx++) { + let div = $(this.divs[idx]); + div.append($('')); + + if (idx !== 0) { + let inputs = div.find(":input"); + var hasVal = false; + for (var j = 0; j < inputs.length; j++) { + $(inputs[j]).attr("required", false) + .attr("data-parsley-required-if", false) + .attr("data-parsley-validate-if-empty", "true"); + if ($(inputs[j]).val()) { + hasVal = true; + } + } + if (!hasVal) { + div.hide(); + } else { + this.count++; } - } - if (!hasVal) { - div.hide(); - } else { - this.count++; } } - } - - this.remove_btns = $('[id^="remove_field__' + this.fieldDef["name"] + '"]'); - if (this.count === 0) { - $(this.remove_btns[0]).hide(); - } - this.addFieldBtn = $("#add_field__" + this.fieldDef["name"]); - this.addFieldBtn.on("click", () => { - $(this.divs[this.count + 1]).show(); - this.count++; - if (this.count > 0) { - $(this.remove_btns[0]).show(); + this.remove_btns = $('[id^="remove_field__' + this.fieldDef["name"] + '"]'); + if (this.count === 0) { + $(this.remove_btns[0]).hide(); } - if (this.count === this.max) { - $(this.addFieldBtn).hide(); + + this.addFieldBtn = $("#add_field__" + this.fieldDef["name"]); + this.addFieldBtn.on("click", () => { + $(this.divs[this.count + 1]).show(); + this.count++; + if (this.count > 0) { + $(this.remove_btns[0]).show(); + } + if (this.count === this.max) { + $(this.addFieldBtn).hide(); + } + }); + + if (this.count >= this.max) { + this.addFieldBtn.hide(); } - }); - if (this.count >= this.max) { - this.addFieldBtn.hide(); - } + $(this.remove_btns).each((idx, btn) => { + $(btn).on("click", () => { + let thisDiv = $(btn).parent(); + let nextDiv = $(thisDiv); + for (let i = idx; i < this.count; i++) { + thisDiv = nextDiv; + nextDiv = nextDiv.next(); + let thisInputs = $(thisDiv).find("select, input[id^='" + this.fieldDef["name"] + "']"); + let nextInputs = $(nextDiv).find("select, input[id^='" + this.fieldDef["name"] + "']"); + for (let j = 0; j < thisInputs.length; j++){ + let thisInput = $(thisInputs[j]); + let nextInput = $(nextInputs[j]); + if (thisInput.is("select")){ + let data = $(nextInput).select2('data'); + if (data === null) { + data = {id: i, text: ""}; + } + $(thisInput).select2('data', {id: data.id, text: data.text}); - $(this.remove_btns).each((idx, btn) => { - $(btn).on("click", () => { - let thisDiv = $(btn).parent(); - let nextDiv = $(thisDiv); - for (let i = idx; i < this.count; i++) { - thisDiv = nextDiv; - nextDiv = nextDiv.next(); - let thisInputs = $(thisDiv).find("select, input[id^='" + this.fieldDef["name"] + "']"); - let nextInputs = $(nextDiv).find("select, input[id^='" + this.fieldDef["name"] + "']"); - for (let j = 0; j < thisInputs.length; j++){ - let thisInput = $(thisInputs[j]); - let nextInput = $(nextInputs[j]); - if (thisInput.is("select")){ - let data = $(nextInput).select2('data'); - if (data === null) { - data = {id: i, text: ""}; } - $(thisInput).select2('data', {id: data.id, text: data.text}); - + else { + $(thisInputs[j]).val($(nextInputs[j]).val()); + } + } + } + this.count--; + $(this.divs[this.count + 1]).find("select, input[id^='" + this.fieldDef["name"] + "']").each((idx, inp) => { + if ($(inp).is("select")){ + $(inp).select2('data', {id: this.count + 1, text: ""}); } else { - $(thisInputs[j]).val($(nextInputs[j]).val()); + $(inp).val(""); } + }); + $(this.divs[this.count + 1]).hide(); + if (this.count === 0) { + $(this.remove_btns[0]).hide(); } - } - this.count--; - $(this.divs[this.count + 1]).find("select, input[id^='" + this.fieldDef["name"] + "']").each((idx, inp) => { - if ($(inp).is("select")){ - $(inp).select2('data', {id: this.count + 1, text: ""}); - } - else { - $(inp).val(""); + if (this.count < this.max) { + $(this.addFieldBtn).show(); } - }); - $(this.divs[this.count + 1]).hide(); - if (this.count === 0) { - $(this.remove_btns[0]).hide(); - } - if (this.count < this.max) { - $(this.addFieldBtn).show(); - } + }) }) - }) - }; + }; - this.init() - }, + this.init() + }, - newSelect : function(params) { - return edges.instantiate(formulaic.widgets.Select, params); - }, - Select : function(params) { - this.fieldDef = params.fieldDef; - this.form = params.formulaic; - this.args = params.args; + newSelect : function(params) { + return edges.instantiate(formulaic.widgets.Select, params); + }, + Select : function(params) { + this.fieldDef = params.fieldDef; + this.form = params.formulaic; + this.args = params.args; - this.ns = "formulaic-select"; - this.elements = false; + this.ns = "formulaic-select"; + this.elements = false; - this.init = function() { - let allow_clear = this.args.allow_clear || false; - this.elements = $("select[name$='" + this.fieldDef.name + "']"); - this.elements.select2({ - allowClear: allow_clear, - newOption: true, - placeholder: "Start typing…" - }); - $(this.elements).on("focus", formulaic.widgets._select2_shift_focus); - }; + this.init = function() { + let allow_clear = this.args.allow_clear || false; + this.elements = $("select[name$='" + this.fieldDef.name + "']"); + this.elements.select2({ + allowClear: allow_clear, + newOption: true, + placeholder: "Start typing…" + }); + $(this.elements).on("focus", formulaic.widgets._select2_shift_focus); + }; - this.init(); - }, + this.init(); + }, + + newTagList : function(params) { + return edges.instantiate(formulaic.widgets.TagList, params); + }, + TagList : function(params) { + this.fieldDef = params.fieldDef; + this.form = params.formulaic; + this.args = params.args; + + this.ns = "formulaic-taglist"; + + this.init = function() { + var stopWords = edges.getParam(this.args.stopWords, []); + + var ajax = { + url: current_scheme + "//" + current_domain + "/autocomplete/journal/" + this.args["field"], + dataType: 'json', + data: function (term, page) { + return { + q: term + }; + }, + results: function (data, page) { + return {results: data["suggestions"].filter((x) => $.inArray(x.text.toLowerCase(), stopWords) === -1)}; + } + }; - newTagList : function(params) { - return edges.instantiate(formulaic.widgets.TagList, params); - }, - TagList : function(params) { - this.fieldDef = params.fieldDef; - this.form = params.formulaic; - this.args = params.args; + var csc = function (term) { + if ($.inArray(term.toLowerCase(), stopWords) !== -1) { + return null; + } + return {id: $.trim(term), text: $.trim(term)}; + }; - this.ns = "formulaic-taglist"; - this.init = function() { - var stopWords = edges.getParam(this.args.stopWords, []); - - var ajax = { - url: current_scheme + "//" + current_domain + "/autocomplete/journal/" + this.args["field"], - dataType: 'json', - data: function (term, page) { - return { - q: term - }; - }, - results: function (data, page) { - return {results: data["suggestions"].filter((x) => $.inArray(x.text.toLowerCase(), stopWords) === -1)}; - } - }; + var initSel = function (element, callback) { + var initial = element.val(); + var entries = initial.split(",").map(x => x.trim()).filter(x => x !== ""); + var data = []; + for (var i = 0; i < entries.length; i++) { + data.push({id: entries[i], text: entries[i]}); + } + callback(data); + }; - var csc = function (term) { - if ($.inArray(term.toLowerCase(), stopWords) !== -1) { - return null; - } - return {id: $.trim(term), text: $.trim(term)}; - }; + // apply the create search choice + let selector = "[name='" + this.fieldDef.name + "']"; + $(selector).select2({ + multiple: true, + minimumInputLength: 1, + ajax: ajax, + createSearchChoice: csc, + initSelection: initSel, + placeholder: "Start typing…", + allowClear: false, + tags: true, + tokenSeparators: [',', ";"], + maximumSelectionSize: this.args["maximumSelectionSize"], + width: 'resolve' + }); + $(selector).on("focus", formulaic.widgets._select2_shift_focus); - var initSel = function (element, callback) { - var initial = element.val(); - var entries = initial.split(",").map(x => x.trim()).filter(x => x !== ""); - var data = []; - for (var i = 0; i < entries.length; i++) { - data.push({id: entries[i], text: entries[i]}); - } - callback(data); }; - // apply the create search choice - let selector = "[name='" + this.fieldDef.name + "']"; - $(selector).select2({ - multiple: true, - minimumInputLength: 1, - ajax: ajax, - createSearchChoice: csc, - initSelection: initSel, - placeholder: "Start typing…", - allowClear: false, - tags: true, - tokenSeparators: [',', ";"], - maximumSelectionSize: this.args["maximumSelectionSize"], - width: 'resolve' - }); - - $(selector).on("focus", formulaic.widgets._select2_shift_focus); - - }; - - this.init(); - }, + this.init(); + }, - newTagEntry : function(params) { - return edges.instantiate(formulaic.widgets.TagEntry, params); - }, - TagEntry : function(params) { - this.fieldDef = params.fieldDef; - this.form = params.formulaic; - this.args = params.args; + newTagEntry : function(params) { + return edges.instantiate(formulaic.widgets.TagEntry, params); + }, + TagEntry : function(params) { + this.fieldDef = params.fieldDef; + this.form = params.formulaic; + this.args = params.args; - this.ns = "formulaic-tagentry"; + this.ns = "formulaic-tagentry"; - this.init = function() { - let selector = "[name='" + this.fieldDef.name + "']"; - $(selector).select2({ - minimumInputLength: 1, - tags: [], - tokenSeparators: [','], - width: 'resolve' - }); - $(selector).on("focus", formulaic.widgets._select2_shift_focus); - }; + this.init = function() { + let selector = "[name='" + this.fieldDef.name + "']"; + $(selector).select2({ + minimumInputLength: 1, + tags: [], + tokenSeparators: [','], + width: 'resolve' + }); + $(selector).on("focus", formulaic.widgets._select2_shift_focus); + }; - this.init(); - }, + this.init(); + }, - newLoadEditors: function(params) { - return edges.instantiate(formulaic.widgets.LoadEditors, params); - }, + newLoadEditors: function(params) { + return edges.instantiate(formulaic.widgets.LoadEditors, params); + }, - LoadEditors: function(params) { - this.fieldDef = params.fieldDef; - this.params = params.args; + LoadEditors: function(params) { + this.fieldDef = params.fieldDef; + this.params = params.args; - this.groupField = false; - this.editorField = false; + this.groupField = false; + this.editorField = false; - this.init = function() { - this.groupField = $("[name='" + this.fieldDef.name + "']"); - this.editorField = $("[name='" + this.params.field + "']"); - edges.on(this.groupField, "change", this, "updateEditors"); - }; + this.init = function() { + this.groupField = $("[name='" + this.fieldDef.name + "']"); + this.editorField = $("[name='" + this.params.field + "']"); + edges.on(this.groupField, "change", this, "updateEditors"); + }; - this.updateEditors = function(element) { - var ed_group_name = $(element).val(); - var ed_query_url = "/admin/dropdown/eg_associates"; + this.updateEditors = function(element) { + var ed_group_name = $(element).val(); + var ed_query_url = "/admin/dropdown/eg_associates"; - // var ed_group_name = $("#s2id_editor_group").find('span').text(); - var that = this; - $.ajax({ - type : "GET", - data : {egn : ed_group_name}, - dataType: "json", - url: ed_query_url, - success: function(resp) - { - // Get the options for the drop-down from our ajax request - var assoc_options = []; - if (resp != null) + // var ed_group_name = $("#s2id_editor_group").find('span').text(); + var that = this; + $.ajax({ + type : "GET", + data : {egn : ed_group_name}, + dataType: "json", + url: ed_query_url, + success: function(resp) { - assoc_options = [["", "No editor assigned"]]; + // Get the options for the drop-down from our ajax request + var assoc_options = []; + if (resp != null) + { + assoc_options = [["", "No editor assigned"]]; - for (var i=0; i").attr("value", assoc_options[j][0]).text(assoc_options[j][1]) - ); + for (var j=0; j < assoc_options.length; j++) { + that.editorField.append( + $("").attr("value", assoc_options[j][0]).text(assoc_options[j][1]) + ); + } } - } - }) - }; - - this.init(); - }, - - newAutocomplete: function(params){ - return edges.instantiate(formulaic.widgets.Autocomplete, params); - }, - - Autocomplete: function(params){ - this.fieldDef = params.fieldDef; - this.params = params.args; - - this.init = function() { - let doc_type = this.params.type || "journal"; - let doc_field = this.params.field; - let mininput = this.params.min_input === undefined ? 3 : this.params.min_input; - let include_input = this.params.include === undefined ? true : this.params.include; - let allow_clear = this.params.allow_clear_input === undefined ? true : this.params.allow_clear_input; - - let ajax = { - url: current_scheme + "//" + current_domain + "/autocomplete/" + doc_type + "/" + doc_field, - dataType: 'json', - data: function (term, page) { - return { - q: term - }; - }, - results: function (data, page) { - return { results: data["suggestions"] }; - } + }) }; - var csc = function(term) {return {"id":term, "text": term};}; + this.init(); + }, + + newAutocomplete: function(params){ + return edges.instantiate(formulaic.widgets.Autocomplete, params); + }, + + Autocomplete: function(params){ + this.fieldDef = params.fieldDef; + this.params = params.args; + + this.init = function() { + let doc_type = this.params.type || "journal"; + let doc_field = this.params.field; + let mininput = this.params.min_input === undefined ? 3 : this.params.min_input; + let include_input = this.params.include === undefined ? true : this.params.include; + let allow_clear = this.params.allow_clear_input === undefined ? true : this.params.allow_clear_input; + + let ajax = { + url: current_scheme + "//" + current_domain + "/autocomplete/" + doc_type + "/" + doc_field, + dataType: 'json', + data: function (term, page) { + return { + q: term + }; + }, + results: function (data, page) { + return { results: data["suggestions"] }; + } + }; - var initSel = function (element, callback) { - var data = {id: element.val(), text: element.val()}; - callback(data); - }; + var csc = function(term) {return {"id":term, "text": term};}; - let selector = "[name='" + this.fieldDef.name + "']"; + var initSel = function (element, callback) { + var data = {id: element.val(), text: element.val()}; + callback(data); + }; - $(selector).on("focus", formulaic.widgets._select2_shift_focus); + let selector = "[name='" + this.fieldDef.name + "']"; - if (include_input) { - // apply the create search choice - $(selector).select2({ - minimumInputLength: mininput, - ajax: ajax, - createSearchChoice: csc, - initSelection : initSel, - allowClear: allow_clear, - width: 'resolve' - }); - } else { - // go without the create search choice option - $(selector).select2({ - minimumInputLength: mininput, - ajax: ajax, - initSelection : initSel, - allowClear: allow_clear, - width: 'resolve' - }); - } + $(selector).on("focus", formulaic.widgets._select2_shift_focus); - $(selector).on("focus", formulaic.widgets._select2_shift_focus); - }; + if (include_input) { + // apply the create search choice + $(selector).select2({ + minimumInputLength: mininput, + ajax: ajax, + createSearchChoice: csc, + initSelection : initSel, + allowClear: allow_clear, + width: 'resolve' + }); + } else { + // go without the create search choice option + $(selector).select2({ + minimumInputLength: mininput, + ajax: ajax, + initSelection : initSel, + allowClear: allow_clear, + width: 'resolve' + }); + } - this.init() - }, - newIssnLink : function(params) { - return edges.instantiate(formulaic.widgets.IssnLink, params) - }, - IssnLink : function(params) { - this.fieldDef = params.fieldDef; - this.form = params.formulaic; - this.issn = params.issn; + $(selector).on("focus", formulaic.widgets._select2_shift_focus); + }; - this.ns = "formulaic-issnlink"; + this.init() + }, + newIssnLink : function(params) { + return edges.instantiate(formulaic.widgets.IssnLink, params) + }, + IssnLink : function(params) { + this.fieldDef = params.fieldDef; + this.form = params.formulaic; + this.issn = params.issn; - this.link = false; - this.url = "https://portal.issn.org/resource/ISSN/"; + this.ns = "formulaic-issnlink"; - this.init = function() { - var elements = this.form.controlSelect.input( - {name: this.fieldDef.name}); - edges.on(elements, "keyup.IssnLink", this, "updateUrl"); + this.link = false; + this.url = "https://portal.issn.org/resource/ISSN/"; - for (var i = 0; i < elements.length; i++) { - this.updateUrl(elements[i]); - } - }; + this.init = function() { + var elements = this.form.controlSelect.input( + {name: this.fieldDef.name}); + edges.on(elements, "keyup.IssnLink", this, "updateUrl"); - this.updateUrl = function(element) { - var that = $(element); - var val = that.val(); - var id = edges.css_id(this.ns, this.fieldDef.name); + for (var i = 0; i < elements.length; i++) { + this.updateUrl(elements[i]); + } + }; - var match = val.match(/[d0-9]{4}-{0,1}[0-9]{3}[0-9xX]{1}/); - var url = this.url + val; + this.updateUrl = function(element) { + var that = $(element); + var val = that.val(); + var id = edges.css_id(this.ns, this.fieldDef.name); - if (val && match) { - if (this.link) { - this.link.text(url); - this.link.attr("href", url); - } else { - var classes = edges.css_classes(this.ns, "visit"); - that.after('

' + url + '

'); + var match = val.match(/[d0-9]{4}-{0,1}[0-9]{3}[0-9xX]{1}/); + var url = this.url + val; - var selector = edges.css_id_selector(this.ns, this.fieldDef.name); - this.link = $(selector, this.form.context); + if (val && match) { + if (this.link) { + this.link.text(url); + this.link.attr("href", url); + } else { + var classes = edges.css_classes(this.ns, "visit"); + that.after('

' + url + '

'); + + var selector = edges.css_id_selector(this.ns, this.fieldDef.name); + this.link = $(selector, this.form.context); + } + } else if (this.link) { + this.link.remove(); + this.link = false; } - } else if (this.link) { - this.link.remove(); - this.link = false; - } - }; + }; - this.init(); - }, - } -}; + this.init(); + }, + } + }; diff --git a/portality/templates/application_form/_field.html b/portality/templates/application_form/_field.html index d5f007ba6e..40be86ce55 100644 --- a/portality/templates/application_form/_field.html +++ b/portality/templates/application_form/_field.html @@ -10,7 +10,7 @@ {% endif %} {% if f.has_widget("click_to_copy") %} Copy value - + {% endif %} {% if f.optional %}(Optional){% endif %} From c451e05fa8e850440dfaed1e449eeb895b3b2ca8 Mon Sep 17 00:00:00 2001 From: Aga Date: Mon, 17 Jul 2023 13:35:18 +0100 Subject: [PATCH 20/56] copy text value of select instead of attr value --- portality/static/js/formulaic.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/portality/static/js/formulaic.js b/portality/static/js/formulaic.js index ccff926425..9d4ccc5659 100644 --- a/portality/static/js/formulaic.js +++ b/portality/static/js/formulaic.js @@ -1226,8 +1226,8 @@ var formulaic = { } else if (this.fieldDef.input == "select") { // todo: countries value instead of code! - var field = $("select[name=" + this.fieldDef.name + "]"); - value_to_copy = field.val() + var field = $("select[name=" + this.fieldDef.name + "] option:selected"); + value_to_copy = field.text() } else { var field = $("input[name=" + this.fieldDef.name + "]"); From 994da75773171ddda2c83568337456a3bd037a44 Mon Sep 17 00:00:00 2001 From: Aga Date: Tue, 18 Jul 2023 12:19:31 +0100 Subject: [PATCH 21/56] Make sure the copy button is displayed in multiple questions Change the appropriate methods of BaseApplication to static Use existing BaseApplication method to copy values of the fields --- portality/static/js/application_form.js | 4 +- portality/static/js/formulaic.js | 43 +++++-------------- .../templates/application_form/_list.html | 4 ++ 3 files changed, 16 insertions(+), 35 deletions(-) diff --git a/portality/static/js/application_form.js b/portality/static/js/application_form.js index 178aef09c4..7046f5ca8b 100644 --- a/portality/static/js/application_form.js +++ b/portality/static/js/application_form.js @@ -117,7 +117,7 @@ doaj.af.BaseApplicationForm = class { }); }; - determineFieldsValue(name) { + static determineFieldsValue(name) { let inputs = this.jq(":input[id='" + name + "']"); if (inputs.length === 0) { inputs = this.jq(":input[id^='" + name + "-']"); @@ -150,7 +150,7 @@ doaj.af.BaseApplicationForm = class { return result; }; - convertValueToText(value){ + static convertValueToText(value){ value = value.filter(v=>(v!=="" && v!==" ")); let result = ""; if (value.length > 0){ diff --git a/portality/static/js/formulaic.js b/portality/static/js/formulaic.js index 9d4ccc5659..39b5718689 100644 --- a/portality/static/js/formulaic.js +++ b/portality/static/js/formulaic.js @@ -1207,39 +1207,16 @@ var formulaic = { edges.on(elements, "click", this, "copy"); }; this.copy = function(element) { - // todo: multiple fields! - if (this.fieldDef.input == "radio") { - var field = $("input[name=" + this.fieldDef.name + "]:checked"); - value_def = this.fieldDef.options.filter(item => item.value === field.val()); - value_to_copy = value_def[0]["display"]; - } - else if (this.fieldDef.input == "checkbox") { - value_to_copy = "" - $( "input[name=" + this.fieldDef.name + "]:checked" ).each(function() { - value_to_copy = value_to_copy + " " + $(this)[0].value; - }); - console.log(value_to_copy) - } - else if (this.fieldDef.input == "taglist") { - var field = $("#" + this.fieldDef.name); - value_to_copy = field.val() - } - else if (this.fieldDef.input == "select") { - // todo: countries value instead of code! - var field = $("select[name=" + this.fieldDef.name + "] option:selected"); - value_to_copy = field.text() - } - else { - var field = $("input[name=" + this.fieldDef.name + "]"); - value_to_copy = field.val() - } - navigator.clipboard.writeText(value_to_copy) - console.log("text copied: " + value_to_copy) - var confirmation = $("#copy-confirmation--" + this.fieldDef.name); - confirmation.text("Value copied: " + value_to_copy); - confirmation.show().delay(3000).fadeOut(); - }; - this.init(); + let value = doaj.af.BaseApplicationForm.determineFieldsValue(this.fieldDef.name) + let value_to_copy = doaj.af.BaseApplicationForm.convertValueToText(value); + navigator.clipboard.writeText(value_to_copy) + console.log("text copied: " + value_to_copy) + var confirmation = $("#copy-confirmation--" + this.fieldDef.name); + confirmation.text("Value copied: " + value_to_copy); + confirmation.show().delay(3000).fadeOut(); + }; + this.init(); + }, newClickableOwner : function(params) { return edges.instantiate(formulaic.widgets.ClickableOwner, params) diff --git a/portality/templates/application_form/_list.html b/portality/templates/application_form/_list.html index bd75ea5795..2f8fc00597 100644 --- a/portality/templates/application_form/_list.html +++ b/portality/templates/application_form/_list.html @@ -7,6 +7,10 @@ {% if f.help("long_help") %} More help {% endif %} + {% if f.has_widget("click_to_copy") %} + Copy value + + {% endif %} {% if f.optional %}(Optional){% endif %} {% if f.get("hint") %}

{{ f.hint | safe }}

{% endif %} From f78b9523602f2b41f67769b49d5ded7aadae01fd Mon Sep 17 00:00:00 2001 From: Aga Date: Tue, 18 Jul 2023 12:21:24 +0100 Subject: [PATCH 22/56] clean up --- portality/static/js/formulaic.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/portality/static/js/formulaic.js b/portality/static/js/formulaic.js index 39b5718689..103855a316 100644 --- a/portality/static/js/formulaic.js +++ b/portality/static/js/formulaic.js @@ -1200,8 +1200,6 @@ var formulaic = { }, ClickToCopy : function(params) { this.fieldDef = params.fieldDef; - this.form = params.formulaic; - this.ns = "formulaic-clicktocopy" this.init = function() { var elements = $("#click-to-copy--" + this.fieldDef.name); edges.on(elements, "click", this, "copy"); @@ -1210,7 +1208,6 @@ var formulaic = { let value = doaj.af.BaseApplicationForm.determineFieldsValue(this.fieldDef.name) let value_to_copy = doaj.af.BaseApplicationForm.convertValueToText(value); navigator.clipboard.writeText(value_to_copy) - console.log("text copied: " + value_to_copy) var confirmation = $("#copy-confirmation--" + this.fieldDef.name); confirmation.text("Value copied: " + value_to_copy); confirmation.show().delay(3000).fadeOut(); From 3ef5f43d810db5c127272bb52e92a8b3804c3513 Mon Sep 17 00:00:00 2001 From: Aga Date: Tue, 18 Jul 2023 13:09:00 +0100 Subject: [PATCH 23/56] remove static from applications_forms js add click to copy only to editor forms and chosen fields --- portality/forms/application_forms.py | 191 ++++++++---------------- portality/static/js/application_form.js | 4 +- portality/static/js/formulaic.js | 5 +- 3 files changed, 66 insertions(+), 134 deletions(-) diff --git a/portality/forms/application_forms.py b/portality/forms/application_forms.py index beb33be64f..c46b30adee 100644 --- a/portality/forms/application_forms.py +++ b/portality/forms/application_forms.py @@ -109,10 +109,7 @@ class FieldDefinitions: "validate" : [], "disabled": True } - }, - "widgets": [ - "click_to_copy", # ~~^-> ClickToCopy:FormWidget~~ - ], + } } # ~~->$ OAStatementURL:FormField~~ @@ -141,8 +138,7 @@ class FieldDefinitions: ], "widgets": [ "trim_whitespace", # ~~^-> TrimWhitespace:FormWidget~~ - "clickable_url", # ~~^-> ClickableURL:FormWidget~~ - "click_to_copy" + "clickable_url" # ~~^-> ClickableURL:FormWidget~~ ], "attr": { "type": "url" @@ -179,11 +175,13 @@ class FieldDefinitions: }, "update_request": { "disabled": True + }, + "admin": { + "widgets": [ + "click_to_copy", # ~~^-> ClickToCopy:FormWidget~~ + ] } - }, - "widgets": [ - "click_to_copy", # ~~^-> ClickToCopy:FormWidget~~ - ] + } } # ~~->$ AlternativeTitle:FormField~~ @@ -205,11 +203,13 @@ class FieldDefinitions: "contexts": { "update_request": { "disabled": True + }, + "admin": { + "widgets": [ + "click_to_copy", # ~~^-> ClickToCopy:FormWidget~~ + ] } - }, - "widgets": [ - "click_to_copy", # ~~^-> ClickToCopy:FormWidget~~ - ] + } } # ~~->$ JournalURL:FormField~~ @@ -236,10 +236,7 @@ class FieldDefinitions: "journal_url_in_public_doaj" # ~~^-> JournalURLInPublicDOAJ:FormValidator~~ ], } - }, - "widgets": [ - "click_to_copy", # ~~^-> ClickToCopy:FormWidget~~ - ] + } } #~~->$ PISSN:FormField~~ @@ -265,8 +262,7 @@ class FieldDefinitions: "widgets" : [ "trim_whitespace", # ~~^-> TrimWhitespace:FormWidget~~ "full_contents", # ~~^->FullContents:FormWidget~~ - "issn_link", # ~~^->IssnLink:FormWidget~~ - "click_to_copy" # ~~^-> ClickToCopy:FormWidget~~ + "issn_link" # ~~^->IssnLink:FormWidget~~ ], "contexts": { "public" : { @@ -337,8 +333,7 @@ class FieldDefinitions: "widgets" : [ "trim_whitespace", # ~~^-> TrimWhitespace:FormWidget~~ "full_contents", # ~~^->FullContents:FormWidget~~ - "issn_link", # ~~^->IssnLink:FormWidget~~ - "click_to_copy" # ~~^-> ClickToCopy:FormWidget~~ + "issn_link" # ~~^->IssnLink:FormWidget~~ ], "contexts": { "public" : { @@ -419,8 +414,7 @@ class FieldDefinitions: "stopWords": STOP_WORDS, "field": "bibjson.keywords" } - }, - "click_to_copy" # ~~^-> ClickToCopy:FormWidget~~ + } ], "attr": { "class": "input-xlarge" @@ -444,8 +438,7 @@ class FieldDefinitions: ], "widgets": [ {"select": {}}, - "multiple_field", - "click_to_copy" # ~~^-> ClickToCopy:FormWidget~~ + "multiple_field" ], "help": { "placeholder": "Type or select the language" @@ -466,8 +459,7 @@ class FieldDefinitions: "widgets": [ "trim_whitespace", # ~~^-> TrimWhitespace:FormWidget~~ {"autocomplete": {"type" : "journal", "field": "bibjson.publisher.name.exact"}}, - "full_contents", # ~~^->FullContents:FormWidget~~ - "click_to_copy" # ~~^-> ClickToCopy:FormWidget~~ + "full_contents" # ~~^->FullContents:FormWidget~~ ], "help": { "placeholder": "Type or select the publisher’s name" @@ -475,6 +467,11 @@ class FieldDefinitions: "contexts" : { "bulk_edit" : { "validate" : [] + }, + "admin": { + "widgets": [ + "click_to_copy", # ~~^-> ClickToCopy:FormWidget~~ + ] } } } @@ -495,8 +492,7 @@ class FieldDefinitions: {"required": {"message": "Enter the country where the publisher carries out its business operations and is registered"}} ], "widgets": [ - {"select": {}}, - "click_to_copy" # ~~^-> ClickToCopy:FormWidget~~ + {"select": {}} ], "attr": { "class": "input-xlarge" @@ -524,11 +520,17 @@ class FieldDefinitions: "a society or other type of institution, enter that here."], "placeholder": "Type or select the society or institution’s name" }, + "contexts" : { + "admin": { + "widgets": [ + "click_to_copy", # ~~^-> ClickToCopy:FormWidget~~ + ] + } + }, "widgets": [ "trim_whitespace", # ~~^-> TrimWhitespace:FormWidget~~ {"autocomplete": {"type" : "journal", "field": "bibjson.institution.name.exact"}}, - "full_contents", # ~~^->FullContents:FormWidget~~ - "click_to_copy" # ~~^-> ClickToCopy:FormWidget~~ + "full_contents" # ~~^->FullContents:FormWidget~~ ] } @@ -545,8 +547,7 @@ class FieldDefinitions: "placeholder": "Type or select the country" }, "widgets": [ - {"select": {"allow_clear" : True}}, - "click_to_copy" # ~~^-> ClickToCopy:FormWidget~~ + {"select": {"allow_clear" : True}} ], "attr": { "class": "input-xlarge" @@ -597,9 +598,6 @@ class FieldDefinitions: }, "validate": [ {"required": {"message": "Select at least one type of license"}} - ], - "widgets": [ - "click_to_copy" # ~~^-> ClickToCopy:FormWidget~~ ] } @@ -620,10 +618,7 @@ class FieldDefinitions: ], "help": { "doaj_criteria": "Content must be licensed" - }, - "widgets": [ - "click_to_copy" # ~~^-> ClickToCopy:FormWidget~~ - ] + } } # ~~->$ LicenseTermsURL:FormField~~ @@ -643,8 +638,7 @@ class FieldDefinitions: }, "widgets": [ "trim_whitespace", # ~~^-> TrimWhitespace:FormWidget~~ - "clickable_url", # ~~^-> ClickableURL:FormWidget~~ - "click_to_copy" # ~~^-> ClickToCopy:FormWidget~~ + "clickable_url" # ~~^-> ClickableURL:FormWidget~~ ] } @@ -666,9 +660,6 @@ class FieldDefinitions: }, "validate": [ {"required": {"message": "Select Yes or No"}} - ], - "widgets": [ - "click_to_copy" # ~~^-> ClickToCopy:FormWidget~~ ] } @@ -695,8 +686,7 @@ class FieldDefinitions: ], "widgets": [ "trim_whitespace", # ~~^-> TrimWhitespace:FormWidget~~ - "clickable_url", # ~~^-> ClickableURL:FormWidget~~ - "click_to_copy" # ~~^-> ClickToCopy:FormWidget~~ + "clickable_url" # ~~^-> ClickableURL:FormWidget~~ ] } @@ -721,10 +711,7 @@ class FieldDefinitions: "under any license allowed by the journal " "retain all rights."], "seal_criteria": "The author must retain the copyright" - }, - "widgets": [ - "click_to_copy" # ~~^-> ClickToCopy:FormWidget~~ - ] + } } # ~~->$ CopyrightURL:FormField~~ @@ -742,8 +729,7 @@ class FieldDefinitions: ], "widgets": [ "trim_whitespace", # ~~^-> TrimWhitespace:FormWidget~~ - "clickable_url", # ~~^-> ClickableURL:FormWidget~~ - "click_to_copy" # ~~^-> ClickToCopy:FormWidget~~ + "clickable_url" # ~~^-> ClickableURL:FormWidget~~ ], "contexts": { "public": { @@ -785,9 +771,6 @@ class FieldDefinitions: }, "validate": [ {"required": {"message": "Select at least one type of review process"}} - ], - "widgets": [ - "click_to_copy" # ~~^-> ClickToCopy:FormWidget~~ ] } @@ -809,8 +792,7 @@ class FieldDefinitions: } ], "widgets" : [ - "trim_whitespace", # ~~^-> TrimWhitespace:FormWidget~~ - "click_to_copy" # ~~^-> ClickToCopy:FormWidget~~ + "trim_whitespace" # ~~^-> TrimWhitespace:FormWidget~~ ], "asynchronous_warning": [ {"warn_on_value": {"value": "None"}} @@ -833,8 +815,7 @@ class FieldDefinitions: ], "widgets": [ "trim_whitespace", # ~~^-> TrimWhitespace:FormWidget~~ - "clickable_url", # ~~^-> ClickableURL:FormWidget~~ - "click_to_copy" # ~~^-> ClickToCopy:FormWidget~~ + "clickable_url" # ~~^-> ClickableURL:FormWidget~~ ] } @@ -857,10 +838,7 @@ class FieldDefinitions: "attr": { "min": app.config.get('MINIMAL_OA_START_DATE', 1900), "max": dates.now().year - }, - "widgets": [ - "click_to_copy" # ~~^-> ClickToCopy:FormWidget~~ - ] + } } # ~~->$ PlagiarismDetection:FormField~~ @@ -880,9 +858,6 @@ class FieldDefinitions: ], "validate": [ {"required": {"message": "Select Yes or No"}} - ], - "widgets": [ - "click_to_copy" # ~~^-> ClickToCopy:FormWidget~~ ] } @@ -911,8 +886,7 @@ class FieldDefinitions: ], "widgets": [ "trim_whitespace", # ~~^-> TrimWhitespace:FormWidget~~ - "clickable_url", # ~~^-> ClickableURL:FormWidget~~ - "click_to_copy" # ~~^-> ClickToCopy:FormWidget~~ + "clickable_url" # ~~^-> ClickableURL:FormWidget~~ ] } @@ -931,8 +905,7 @@ class FieldDefinitions: ], "widgets": [ "trim_whitespace", # ~~^-> TrimWhitespace:FormWidget~~ - "clickable_url", # ~~^-> ClickableURL:FormWidget~~ - "click_to_copy" # ~~^-> ClickToCopy:FormWidget~~ + "clickable_url" # ~~^-> ClickableURL:FormWidget~~ ] } @@ -951,8 +924,7 @@ class FieldDefinitions: ], "widgets": [ "trim_whitespace", # ~~^-> TrimWhitespace:FormWidget~~ - "clickable_url", # ~~^-> ClickableURL:FormWidget~~ - "click_to_copy" # ~~^-> ClickToCopy:FormWidget~~ + "clickable_url" # ~~^-> ClickableURL:FormWidget~~ ] } @@ -971,8 +943,7 @@ class FieldDefinitions: ], "widgets": [ "trim_whitespace", # ~~^-> TrimWhitespace:FormWidget~~ - "clickable_url", # ~~^-> ClickableURL:FormWidget~~ - "click_to_copy" # ~~^-> ClickToCopy:FormWidget~~ + "clickable_url" # ~~^-> ClickableURL:FormWidget~~ ] } @@ -992,10 +963,7 @@ class FieldDefinitions: "attr": { "min": "1", "max": "100" - }, - "widgets": [ - "click_to_copy" # ~~^-> ClickToCopy:FormWidget~~ - ] + } } # ~~->$ APC:FormField~~ @@ -1016,9 +984,6 @@ class FieldDefinitions: }, "validate": [ {"required": {"message": "Select Yes or No"}} - ], - "widgets": [ - "click_to_copy" # ~~^-> ClickToCopy:FormWidget~~ ] } @@ -1047,8 +1012,7 @@ class FieldDefinitions: "template": "application_form/_list.html", "entry_template": "application_form/_entry_group_horizontal.html", "widgets": [ - "multiple_field", - "click_to_copy" # ~~^-> ClickToCopy:FormWidget~~ + "multiple_field" ] } @@ -1064,8 +1028,7 @@ class FieldDefinitions: "placeholder": "Currency" }, "widgets": [ - {"select": {}}, - "click_to_copy" # ~~^-> ClickToCopy:FormWidget~~ + {"select": {}} ], "attr": { "class": "input-xlarge" @@ -1103,10 +1066,7 @@ class FieldDefinitions: ], "attr": { "min": "1" - }, - "widgets":[ - "click_to_copy" # ~~^-> ClickToCopy:FormWidget~~ - ] + } } # ~~->$ APCURL:FormField~~ @@ -1128,8 +1088,7 @@ class FieldDefinitions: ], "widgets": [ "trim_whitespace", # ~~^-> TrimWhitespace:FormWidget~~ - "clickable_url", # ~~^-> ClickableURL:FormWidget~~ - "click_to_copy" # ~~^-> ClickToCopy:FormWidget~~ + "clickable_url" # ~~^-> ClickableURL:FormWidget~~ ] } @@ -1153,9 +1112,6 @@ class FieldDefinitions: }, "validate": [ {"required": {"message": "Select Yes or No"}} - ], - "widgets": [ - "click_to_copy" # ~~^-> ClickToCopy:FormWidget~~ ] } @@ -1184,8 +1140,7 @@ class FieldDefinitions: ], "widgets": [ "trim_whitespace", # ~~^-> TrimWhitespace:FormWidget~~ - "clickable_url", # ~~^-> ClickableURL:FormWidget~~ - "click_to_copy" # ~~^-> ClickToCopy:FormWidget~~ + "clickable_url" # ~~^-> ClickableURL:FormWidget~~ ] } @@ -1206,9 +1161,6 @@ class FieldDefinitions: }, "validate": [ {"required": {"message": "Select Yes or No"}} - ], - "widgets": [ - "click_to_copy" # ~~^-> ClickToCopy:FormWidget~~ ] } @@ -1236,8 +1188,7 @@ class FieldDefinitions: ], "widgets": [ "trim_whitespace", # ~~^-> TrimWhitespace:FormWidget~~ - "clickable_url", # ~~^-> ClickableURL:FormWidget~~ - "click_to_copy" # ~~^-> ClickToCopy:FormWidget~~ + "clickable_url" # ~~^-> ClickableURL:FormWidget~~ ] } @@ -1269,9 +1220,6 @@ class FieldDefinitions: }, "validate": [ {"required": {"message": "Select at least one option"}} - ], - "widgets": [ - "click_to_copy" # ~~^-> ClickToCopy:FormWidget~~ ] } @@ -1301,8 +1249,7 @@ class FieldDefinitions: ], "widgets": [ "trim_whitespace", # ~~^-> TrimWhitespace:FormWidget~~ - "multiple_field", - "click_to_copy" # ~~^-> ClickToCopy:FormWidget~~ + "multiple_field" ], "attr": { "class": "input-xlarge unstyled-list" @@ -1327,8 +1274,7 @@ class FieldDefinitions: {"warn_on_value": {"value": "None"}} ], "widgets" : [ - "trim_whitespace", # ~~^-> TrimWhitespace:FormWidget~~ - "click_to_copy" # ~~^-> ClickToCopy:FormWidget~~ + "trim_whitespace" # ~~^-> TrimWhitespace:FormWidget~~ ] } @@ -1375,8 +1321,7 @@ class FieldDefinitions: ], "widgets": [ "trim_whitespace", # ~~^-> TrimWhitespace:FormWidget~~ - "clickable_url", # ~~^-> ClickableURL:FormWidget~~ - "click_to_copy" # ~~^-> ClickToCopy:FormWidget~~ + "clickable_url" # ~~^-> ClickableURL:FormWidget~~ ] } @@ -1407,9 +1352,6 @@ class FieldDefinitions: ]}, "validate": [ {"required": {"message": "Select at least one option"}} - ], - "widgets": [ - "click_to_copy" # ~~^-> ClickToCopy:FormWidget~~ ] } @@ -1431,8 +1373,7 @@ class FieldDefinitions: {"warn_on_value": {"value": "None"}} ], "widgets" : [ - "trim_whitespace", # ~~^-> TrimWhitespace:FormWidget~~ - "click_to_copy" # ~~^-> ClickToCopy:FormWidget~~ + "trim_whitespace" # ~~^-> TrimWhitespace:FormWidget~~ ] } @@ -1518,9 +1459,6 @@ class FieldDefinitions: }, "validate": [ {"required": {"message": "Select at least one option"}} - ], - "widgets": [ - "click_to_copy" # ~~^-> ClickToCopy:FormWidget~~ ] } @@ -1542,8 +1480,7 @@ class FieldDefinitions: {"warn_on_value": {"value": "None"}} ], "widgets" : [ - "trim_whitespace", # ~~^-> TrimWhitespace:FormWidget~~ - "click_to_copy" # ~~^-> ClickToCopy:FormWidget~~ + "trim_whitespace" # ~~^-> TrimWhitespace:FormWidget~~ ] } @@ -1572,10 +1509,7 @@ class FieldDefinitions: {"required": {"message": "Select Yes or No"}} ] } - }, - "widgets": [ - "click_to_copy" # ~~^-> ClickToCopy:FormWidget~~ - ] + } } # ~~->$ OpenCitations:FormField~~ @@ -1602,10 +1536,7 @@ class FieldDefinitions: {"required": {"message": "Select Yes or No"}} ] } - }, - "widgets": [ - "click_to_copy" # ~~^-> ClickToCopy:FormWidget~~ - ] + } } ####################################### diff --git a/portality/static/js/application_form.js b/portality/static/js/application_form.js index 7046f5ca8b..178aef09c4 100644 --- a/portality/static/js/application_form.js +++ b/portality/static/js/application_form.js @@ -117,7 +117,7 @@ doaj.af.BaseApplicationForm = class { }); }; - static determineFieldsValue(name) { + determineFieldsValue(name) { let inputs = this.jq(":input[id='" + name + "']"); if (inputs.length === 0) { inputs = this.jq(":input[id^='" + name + "-']"); @@ -150,7 +150,7 @@ doaj.af.BaseApplicationForm = class { return result; }; - static convertValueToText(value){ + convertValueToText(value){ value = value.filter(v=>(v!=="" && v!==" ")); let result = ""; if (value.length > 0){ diff --git a/portality/static/js/formulaic.js b/portality/static/js/formulaic.js index 103855a316..02722312db 100644 --- a/portality/static/js/formulaic.js +++ b/portality/static/js/formulaic.js @@ -1205,8 +1205,9 @@ var formulaic = { edges.on(elements, "click", this, "copy"); }; this.copy = function(element) { - let value = doaj.af.BaseApplicationForm.determineFieldsValue(this.fieldDef.name) - let value_to_copy = doaj.af.BaseApplicationForm.convertValueToText(value); + let form = new doaj.af.BaseApplicationForm() + let value = form.determineFieldsValue(this.fieldDef.name) + let value_to_copy = form.convertValueToText(value); navigator.clipboard.writeText(value_to_copy) var confirmation = $("#copy-confirmation--" + this.fieldDef.name); confirmation.text("Value copied: " + value_to_copy); From 695c00ab8440e6c136824e3879992cc04a059d77 Mon Sep 17 00:00:00 2001 From: Aga Date: Tue, 18 Jul 2023 13:26:11 +0100 Subject: [PATCH 24/56] add forms to correct contexts --- portality/forms/application_forms.py | 51 +++++++++++++++++++++++----- 1 file changed, 43 insertions(+), 8 deletions(-) diff --git a/portality/forms/application_forms.py b/portality/forms/application_forms.py index c46b30adee..d1f8a44d20 100644 --- a/portality/forms/application_forms.py +++ b/portality/forms/application_forms.py @@ -167,19 +167,25 @@ class FieldDefinitions: "full_contents" # ~~^->FullContents:FormWidget~~ ], "contexts": { + "admin": { + "widgets": [ + "click_to_copy", # ~~^-> ClickToCopy:FormWidget~~ + ] + }, "editor": { - "disabled": True + "disabled": True, + "widgets": [ + "click_to_copy", # ~~^-> ClickToCopy:FormWidget~~ + ] }, "associate_editor": { - "disabled": True - }, - "update_request": { - "disabled": True - }, - "admin": { + "disabled": True, "widgets": [ "click_to_copy", # ~~^-> ClickToCopy:FormWidget~~ ] + }, + "update_request": { + "disabled": True } } } @@ -208,6 +214,16 @@ class FieldDefinitions: "widgets": [ "click_to_copy", # ~~^-> ClickToCopy:FormWidget~~ ] + }, + "associate_editor": { + "widgets": [ + "click_to_copy", # ~~^-> ClickToCopy:FormWidget~~ + ] + }, + "editor": { + "widgets": [ + "click_to_copy", # ~~^-> ClickToCopy:FormWidget~~ + ] } } } @@ -472,6 +488,16 @@ class FieldDefinitions: "widgets": [ "click_to_copy", # ~~^-> ClickToCopy:FormWidget~~ ] + }, + "associate_editor": { + "widgets": [ + "click_to_copy", # ~~^-> ClickToCopy:FormWidget~~ + ] + }, + "editor": { + "widgets": [ + "click_to_copy", # ~~^-> ClickToCopy:FormWidget~~ + ] } } } @@ -525,6 +551,16 @@ class FieldDefinitions: "widgets": [ "click_to_copy", # ~~^-> ClickToCopy:FormWidget~~ ] + }, + "associate_editor": { + "widgets": [ + "click_to_copy", # ~~^-> ClickToCopy:FormWidget~~ + ] + }, + "editor": { + "widgets": [ + "click_to_copy", # ~~^-> ClickToCopy:FormWidget~~ + ] } }, "widgets": [ @@ -1399,7 +1435,6 @@ class FieldDefinitions: "widgets": [ "trim_whitespace", # ~~^-> TrimWhitespace:FormWidget~~ "clickable_url", # ~~^-> ClickableURL:FormWidget~~ - "click_to_copy" # ~~^-> ClickToCopy:FormWidget~~ ], "contexts" : { "public" : { From af8005cea15e0ad13ed7c6558d76de3393c203c7 Mon Sep 17 00:00:00 2001 From: Aga Date: Tue, 18 Jul 2023 13:31:19 +0100 Subject: [PATCH 25/56] add click_to_copy functional tests --- doajtest/testbook/journal_form/editor_form.yml | 6 ++++++ doajtest/testbook/journal_form/maned_form.yml | 6 ++++++ .../new_application_form/associate_editor_form.yml | 6 ++++++ doajtest/testbook/new_application_form/editor_form.yml | 8 +++++++- doajtest/testbook/new_application_form/maned_form.yml | 6 ++++++ 5 files changed, 31 insertions(+), 1 deletion(-) diff --git a/doajtest/testbook/journal_form/editor_form.yml b/doajtest/testbook/journal_form/editor_form.yml index 16b78e2c77..747bd3f81d 100644 --- a/doajtest/testbook/journal_form/editor_form.yml +++ b/doajtest/testbook/journal_form/editor_form.yml @@ -80,3 +80,9 @@ tests: - step: Attempt to click the "Remove" button results: - You are unable to delete the note + - step: Click "copy" button next to one of the fields (eg. Title) + results: + - Confirmation with fields value is displayed for 3 seconds + - step: Attempt to paste the value (use separate editor) + results: + - Correct value is pasted diff --git a/doajtest/testbook/journal_form/maned_form.yml b/doajtest/testbook/journal_form/maned_form.yml index 0a05f70530..935fe4504f 100644 --- a/doajtest/testbook/journal_form/maned_form.yml +++ b/doajtest/testbook/journal_form/maned_form.yml @@ -120,3 +120,9 @@ tests: - step: Attempt to click the "Remove" button results: - You are unable to delete the note + - step: Click "copy" button next to one of the fields (eg. Title) + results: + - Confirmation with fields value is displayed for 3 seconds + - step: Attempt to paste the value (use separate editor) + results: + - Correct value is pasted diff --git a/doajtest/testbook/new_application_form/associate_editor_form.yml b/doajtest/testbook/new_application_form/associate_editor_form.yml index 366fac92c4..f9f9fd4619 100644 --- a/doajtest/testbook/new_application_form/associate_editor_form.yml +++ b/doajtest/testbook/new_application_form/associate_editor_form.yml @@ -63,3 +63,9 @@ tests: - step: Attempt to click the "Remove" button results: - You are unable to delete the note + - step: Click "copy" button next to one of the fields (eg. Title) + results: + - Confirmation with fields value is displayed for 3 seconds + - step: Attempt to paste the value (use separate editor) + results: + - Correct value is pasted diff --git a/doajtest/testbook/new_application_form/editor_form.yml b/doajtest/testbook/new_application_form/editor_form.yml index cd9b8edf3d..b9db0066f7 100644 --- a/doajtest/testbook/new_application_form/editor_form.yml +++ b/doajtest/testbook/new_application_form/editor_form.yml @@ -64,4 +64,10 @@ tests: - you are unable to edit the note - step: Attempt to click the "Remove" button results: - - You are unable to delete the note \ No newline at end of file + - You are unable to delete the note + - step: Click "copy" button next to one of the fields (eg. Title) + results: + - Confirmation with fields value is displayed for 3 seconds + - step: Attempt to paste the value (use separate editor) + results: + - Correct value is pasted \ No newline at end of file diff --git a/doajtest/testbook/new_application_form/maned_form.yml b/doajtest/testbook/new_application_form/maned_form.yml index 907791691b..98dc0211f6 100644 --- a/doajtest/testbook/new_application_form/maned_form.yml +++ b/doajtest/testbook/new_application_form/maned_form.yml @@ -95,3 +95,9 @@ tests: - step: Attempt to click the "Remove" button results: - You are unable to delete the note + - step: Click "copy" button next to one of the fields (eg. Title) + results: + - Confirmation with fields value is displayed for 3 seconds + - step: Attempt to paste the value (use separate editor) + results: + - Correct value is pasted From 8d4275dd9449fc1b00100fb487a71808ba143b07 Mon Sep 17 00:00:00 2001 From: Aga Date: Tue, 18 Jul 2023 13:40:15 +0100 Subject: [PATCH 26/56] one more functional test --- doajtest/testbook/journal_form/associate_form.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/doajtest/testbook/journal_form/associate_form.yml b/doajtest/testbook/journal_form/associate_form.yml index cd333cbc2d..8a9e68b11c 100644 --- a/doajtest/testbook/journal_form/associate_form.yml +++ b/doajtest/testbook/journal_form/associate_form.yml @@ -70,4 +70,10 @@ tests: - step: Attempt to click the "Remove" button results: - You are unable to delete the note + - step: Click "copy" button next to one of the fields (eg. Title) + results: + - Confirmation with fields value is displayed for 3 seconds + - step: Attempt to paste the value (use separate editor) + results: + - Correct value is pasted From 67067bffe39f97fc0f3e5779aff0d2395f73c63b Mon Sep 17 00:00:00 2001 From: Aga Date: Tue, 18 Jul 2023 13:50:27 +0100 Subject: [PATCH 27/56] clean up part 1 --- portality/static/js/formulaic.js | 1521 +++++++++++++++--------------- 1 file changed, 750 insertions(+), 771 deletions(-) diff --git a/portality/static/js/formulaic.js b/portality/static/js/formulaic.js index 02722312db..55935d606d 100644 --- a/portality/static/js/formulaic.js +++ b/portality/static/js/formulaic.js @@ -1195,249 +1195,228 @@ var formulaic = { this.init(); }, - newClickToCopy : function(params) { - return edges.instantiate(formulaic.widgets.ClickToCopy, params) + newClickableOwner : function(params) { + return edges.instantiate(formulaic.widgets.ClickableOwner, params) }, - ClickToCopy : function(params) { + ClickableOwner : function(params) { this.fieldDef = params.fieldDef; + this.form = params.formulaic; + + this.ns = "formulaic-clickableowner"; + + this.link = false; + this.init = function() { - var elements = $("#click-to-copy--" + this.fieldDef.name); - edges.on(elements, "click", this, "copy"); - }; - this.copy = function(element) { - let form = new doaj.af.BaseApplicationForm() - let value = form.determineFieldsValue(this.fieldDef.name) - let value_to_copy = form.convertValueToText(value); - navigator.clipboard.writeText(value_to_copy) - var confirmation = $("#copy-confirmation--" + this.fieldDef.name); - confirmation.text("Value copied: " + value_to_copy); - confirmation.show().delay(3000).fadeOut(); + var elements = this.form.controlSelect.input({name: this.fieldDef.name}); + edges.on(elements, "change.ClickableOwner", this, "updateOwner"); + for (var i = 0; i < elements.length; i++) { + this.updateOwner(elements[i]); + } }; - this.init(); - - }, - newClickableOwner : function(params) { - return edges.instantiate(formulaic.widgets.ClickableOwner, params) - }, - ClickableOwner : function(params) { - this.fieldDef = params.fieldDef; - this.form = params.formulaic; - this.ns = "formulaic-clickableowner"; + this.updateOwner = function(element) { + var that = $(element); + var val = that.val(); - this.link = false; + if (val) { + if (this.link) { + this.link.attr("href", "/account/" + val); + } else { + var classes = edges.css_classes(this.ns, "visit"); + var id = edges.css_id(this.ns, this.fieldDef.name); + that.after('

See this account’s profile

'); - this.init = function() { - var elements = this.form.controlSelect.input({name: this.fieldDef.name}); - edges.on(elements, "change.ClickableOwner", this, "updateOwner"); - for (var i = 0; i < elements.length; i++) { - this.updateOwner(elements[i]); + var selector = edges.css_id_selector(this.ns, this.fieldDef.name); + this.link = $(selector, this.form.context); } - }; - - this.updateOwner = function(element) { - var that = $(element); - var val = that.val(); + } else if (this.link) { + this.link.remove(); + this.link = false; + } + }; - if (val) { - if (this.link) { - this.link.attr("href", "/account/" + val); - } else { - var classes = edges.css_classes(this.ns, "visit"); - var id = edges.css_id(this.ns, this.fieldDef.name); - that.after('

See this account’s profile

'); + this.init(); + }, - var selector = edges.css_id_selector(this.ns, this.fieldDef.name); - this.link = $(selector, this.form.context); - } - } else if (this.link) { - this.link.remove(); - this.link = false; - } - }; + newTrimWhitespace : function(params) { + return edges.instantiate(formulaic.widgets.TrimWhitespace, params) + }, + TrimWhitespace : function(params) { + this.fieldDef = params.fieldDef; + this.form = params.formulaic; - this.init(); - }, + this.ns = "formulaic-trimwhitespace"; - newTrimWhitespace : function(params) { - return edges.instantiate(formulaic.widgets.TrimWhitespace, params) - }, - TrimWhitespace : function(params) { - this.fieldDef = params.fieldDef; - this.form = params.formulaic; + this.link = false; - this.ns = "formulaic-trimwhitespace"; + this.init = function () { + var elements = this.form.controlSelect.input({name: this.fieldDef.name}); + edges.on(elements, "focus.TrimWhitespace", this, "trim"); + edges.on(elements, "blur.TrimWhitespace", this, "trim"); - this.link = false; + for (var i = 0; i < elements.length; i++) { + this.trim(elements[i]); + } + }; - this.init = function () { - var elements = this.form.controlSelect.input({name: this.fieldDef.name}); - edges.on(elements, "focus.TrimWhitespace", this, "trim"); - edges.on(elements, "blur.TrimWhitespace", this, "trim"); + this.trim = function(element) { + var that = $(element); + var val = that.val(); + var nv = val.trim(); + if (nv !== val) { + that.val(nv); + } + }; - for (var i = 0; i < elements.length; i++) { - this.trim(elements[i]); - } - }; + this.init(); + }, - this.trim = function(element) { - var that = $(element); - var val = that.val(); - var nv = val.trim(); - if (nv !== val) { - that.val(nv); - } - }; + newClickableUrl : function(params) { + return edges.instantiate(formulaic.widgets.ClickableUrl, params) + }, + ClickableUrl : function(params) { + this.fieldDef = params.fieldDef; + this.form = params.formulaic; - this.init(); - }, + this.ns = "formulaic-clickableurl"; - newClickableUrl : function(params) { - return edges.instantiate(formulaic.widgets.ClickableUrl, params) - }, - ClickableUrl : function(params) { - this.fieldDef = params.fieldDef; - this.form = params.formulaic; + this.link = false; - this.ns = "formulaic-clickableurl"; + this.init = function() { + var elements = this.form.controlSelect.input( + {name: this.fieldDef.name}); + // TODO: should work as-you-type by changing "change" to "keyup" event; doesn't work in edges + //edges.on(elements, "change.ClickableUrl", this, "updateUrl"); + edges.on(elements, "keyup.ClickableUrl", this, "updateUrl"); + + for (var i = 0; i < elements.length; i++) { + this.updateUrl(elements[i]); + } + }; - this.link = false; + this.updateUrl = function(element) { + var that = $(element); + var val = that.val(); + var id = edges.css_id(this.ns, this.fieldDef.name); - this.init = function() { - var elements = this.form.controlSelect.input( - {name: this.fieldDef.name}); - // TODO: should work as-you-type by changing "change" to "keyup" event; doesn't work in edges - //edges.on(elements, "change.ClickableUrl", this, "updateUrl"); - edges.on(elements, "keyup.ClickableUrl", this, "updateUrl"); + if (val && (val.substring(0,7) === "http://" || val.substring(0,8) === "https://") && val.length > 10) { + if (this.link) { + this.link.text(val); + this.link.attr("href", val); + } else { + var classes = edges.css_classes(this.ns, "visit"); + that.after('

' + val + '

'); - for (var i = 0; i < elements.length; i++) { - this.updateUrl(elements[i]); + var selector = edges.css_id_selector(this.ns, this.fieldDef.name); + this.link = $(selector, this.form.context); } - }; - - this.updateUrl = function(element) { - var that = $(element); - var val = that.val(); - var id = edges.css_id(this.ns, this.fieldDef.name); + } else if (this.link) { + this.link.remove(); + this.link = false; + } + }; - if (val && (val.substring(0,7) === "http://" || val.substring(0,8) === "https://") && val.length > 10) { - if (this.link) { - this.link.text(val); - this.link.attr("href", val); - } else { - var classes = edges.css_classes(this.ns, "visit"); - that.after('

' + val + '

'); + this.init(); + }, - var selector = edges.css_id_selector(this.ns, this.fieldDef.name); - this.link = $(selector, this.form.context); - } - } else if (this.link) { - this.link.remove(); - this.link = false; - } - }; + newFullContents : function(params) { + return edges.instantiate(formulaic.widgets.FullContents, params) + }, + FullContents : function(params) { + this.fieldDef = params.fieldDef; + this.form = params.formulaic; + this.args = params.args; - this.init(); - }, + this.ns = "formulaic-fullcontents"; - newFullContents : function(params) { - return edges.instantiate(formulaic.widgets.FullContents, params) - }, - FullContents : function(params) { - this.fieldDef = params.fieldDef; - this.form = params.formulaic; - this.args = params.args; + this.container = false; - this.ns = "formulaic-fullcontents"; + this.init = function() { + var elements = this.form.controlSelect.input({name: this.fieldDef.name}); + edges.on(elements, "keyup.FullContents", this, "updateContents"); - this.container = false; + for (var i = 0; i < elements.length; i++) { + this.updateContents(elements[i]); + } + }; - this.init = function() { - var elements = this.form.controlSelect.input({name: this.fieldDef.name}); - edges.on(elements, "keyup.FullContents", this, "updateContents"); + this.updateContents = function(element) { + var that = $(element); + var val = that.val(); - for (var i = 0; i < elements.length; i++) { - this.updateContents(elements[i]); + // if there is a behaviour for when the field is empty and disabled, then check if it is, and if + // it is include the desired alternative text + if (this.args && this.args.empty_disabled) { + if (val === "" && that.prop("disabled")) { + val = this.args.empty_disabled; } - }; + } - this.updateContents = function(element) { - var that = $(element); - var val = that.val(); + if (val) { + if (this.container) { + this.container.html('Full contents: ' + edges.escapeHtml(val) + ''); + } else { + var classes = edges.css_classes(this.ns, "contents"); + var id = edges.css_id(this.ns, this.fieldDef.name); + that.after('

Full contents: ' + edges.escapeHtml(val) + '

'); - // if there is a behaviour for when the field is empty and disabled, then check if it is, and if - // it is include the desired alternative text - if (this.args && this.args.empty_disabled) { - if (val === "" && that.prop("disabled")) { - val = this.args.empty_disabled; - } + var selector = edges.css_id_selector(this.ns, this.fieldDef.name); + this.container = $(selector, this.form.context); } + } else if (this.container) { + this.container.remove(); + this.container = false; + } + }; - if (val) { - if (this.container) { - this.container.html('Full contents: ' + edges.escapeHtml(val) + ''); - } else { - var classes = edges.css_classes(this.ns, "contents"); - var id = edges.css_id(this.ns, this.fieldDef.name); - that.after('

Full contents: ' + edges.escapeHtml(val) + '

'); + this.init(); + }, - var selector = edges.css_id_selector(this.ns, this.fieldDef.name); - this.container = $(selector, this.form.context); - } - } else if (this.container) { - this.container.remove(); - this.container = false; - } - }; + newNoteModal : function(params) { + return edges.instantiate(formulaic.widgets.NoteModal, params) + }, + NoteModal : function(params) { + this.fieldDef = params.fieldDef; + this.form = params.formulaic; - this.init(); - }, - - newNoteModal : function(params) { - return edges.instantiate(formulaic.widgets.NoteModal, params) - }, - NoteModal : function(params) { - this.fieldDef = params.fieldDef; - this.form = params.formulaic; - - this.ns = "formulaic-notemodal"; - - this.container = false; - - this.init = function() { - var viewClass = edges.css_classes(this.ns, "view"); - var closeClass = edges.css_classes(this.ns, "close"); - let group = $("div[name='" + this.fieldDef["name"] + "__group']") - var textarea = group.find("textarea"); - - let inputs = group.find("input[type=text]") - for (let i = 0; i < inputs.length; i++) { - let jqin = $(inputs[i]); - let iid = jqin.attr("id"); - if (iid.endsWith("_author")) { - let val = jqin.val() - if (val === "") { - jqin.hide(); - } + this.ns = "formulaic-notemodal"; + + this.container = false; + + this.init = function() { + var viewClass = edges.css_classes(this.ns, "view"); + var closeClass = edges.css_classes(this.ns, "close"); + let group = $("div[name='" + this.fieldDef["name"] + "__group']") + var textarea = group.find("textarea"); + + let inputs = group.find("input[type=text]") + for (let i = 0; i < inputs.length; i++) { + let jqin = $(inputs[i]); + let iid = jqin.attr("id"); + if (iid.endsWith("_author")) { + let val = jqin.val() + if (val === "") { + jqin.hide(); } } + } - for (var i = 0; i < textarea.length; i++) { - var container = $(textarea[i]); + for (var i = 0; i < textarea.length; i++) { + var container = $(textarea[i]); - let contentHeight = container[0].scrollHeight; - if (contentHeight > 200) { - contentHeight = 200; - } - container.css("height", (contentHeight + 5) + "px"); + let contentHeight = container[0].scrollHeight; + if (contentHeight > 200) { + contentHeight = 200; + } + container.css("height", (contentHeight + 5) + "px"); - var modalId = "modal-" + this.fieldDef["name"] + "-" + i; + var modalId = "modal-" + this.fieldDef["name"] + "-" + i; - var date = $("#" + this.fieldDef["name"] + "-" + i + "-note_date"); - var note = $("#" + this.fieldDef["name"] + "-" + i + "-note"); - var author = $("#" + this.fieldDef["name"] + "-" + i + "-note_author"); + var date = $("#" + this.fieldDef["name"] + "-" + i + "-note_date"); + var note = $("#" + this.fieldDef["name"] + "-" + i + "-note"); + var author = $("#" + this.fieldDef["name"] + "-" + i + "-note_author"); - $(` + $(` `).insertAfter(container); - } + } - var viewSelector = edges.css_class_selector(this.ns, "view"); - edges.on(viewSelector, "click", this, "showModal"); + var viewSelector = edges.css_class_selector(this.ns, "view"); + edges.on(viewSelector, "click", this, "showModal"); - var closeSelector = edges.css_class_selector(this.ns, "close"); - edges.on(closeSelector, "click", this, "closeModal"); - }; + var closeSelector = edges.css_class_selector(this.ns, "close"); + edges.on(closeSelector, "click", this, "closeModal"); + }; - this.showModal = function(element) { - var that = $(element); - var modal = that.siblings(".modal"); - modal.show(); - }; + this.showModal = function(element) { + var that = $(element); + var modal = that.siblings(".modal"); + modal.show(); + }; - this.closeModal = function(element) { - var that = $(element); - var modal = that.parents(".modal"); - modal.hide(); - }; + this.closeModal = function(element) { + var that = $(element); + var modal = that.parents(".modal"); + modal.hide(); + }; - this.init(); - }, - - newInfiniteRepeat : function(params) { - return edges.instantiate(formulaic.widgets.InfiniteRepeat, params) - }, - InfiniteRepeat: function(params) { - this.fieldDef = params.fieldDef; - this.args = params.args; - - this.idRx = /(.+?-)(\d+)(-.+)/; - this.template = ""; - this.container = false; - this.divs = false; - - this.init = function() { - this.divs = $("div[name='" + this.fieldDef["name"] + "__group']"); - for (var i = 0 ; i < this.divs.length; i++) { - var div = $(this.divs[i]); - div.append($('')); - feather.replace(); - } + this.init(); + }, - this.template = $(this.divs[0]).html(); - this.container = $(this.divs[0]).parents(".removable-fields"); + newInfiniteRepeat : function(params) { + return edges.instantiate(formulaic.widgets.InfiniteRepeat, params) + }, + InfiniteRepeat: function(params) { + this.fieldDef = params.fieldDef; + this.args = params.args; - if (this.divs.length === 1) { - let div = $(this.divs[0]); - let inputs = div.find(":input"); - let tripwire = false; - for (var i = 0; i < inputs.length; i++) { - if ($(inputs[i]).val()) { - tripwire = true; - break; - } - } - if (!tripwire) { - // the field is empty - $(this.divs[0]).remove(); - this.divs = []; + this.idRx = /(.+?-)(\d+)(-.+)/; + this.template = ""; + this.container = false; + this.divs = false; + + this.init = function() { + this.divs = $("div[name='" + this.fieldDef["name"] + "__group']"); + for (var i = 0 ; i < this.divs.length; i++) { + var div = $(this.divs[i]); + div.append($('')); + feather.replace(); + } + + this.template = $(this.divs[0]).html(); + this.container = $(this.divs[0]).parents(".removable-fields"); + + if (this.divs.length === 1) { + let div = $(this.divs[0]); + let inputs = div.find(":input"); + let tripwire = false; + for (var i = 0; i < inputs.length; i++) { + if ($(inputs[i]).val()) { + tripwire = true; + break; } } + if (!tripwire) { + // the field is empty + $(this.divs[0]).remove(); + this.divs = []; + } + } - this.addFieldBtn = $("#add_field__" + this.fieldDef["name"]); - this.removeFieldBtns = $('[id^="remove_field__' + this.fieldDef["name"] + '"]'); + this.addFieldBtn = $("#add_field__" + this.fieldDef["name"]); + this.removeFieldBtns = $('[id^="remove_field__' + this.fieldDef["name"] + '"]'); - edges.on(this.addFieldBtn, "click", this, "addField"); - edges.on(this.removeFieldBtns, "click", this, "removeField"); + edges.on(this.addFieldBtn, "click", this, "addField"); + edges.on(this.removeFieldBtns, "click", this, "removeField"); - // show or hide the remove buttons - for (let i = 0; i < this.divs.length; i++) { - let cur_div = $(this.divs[i]); - if (!cur_div.find('textarea').is(':disabled')) { - cur_div.find('[id^="remove_field__"]').show(); - } + // show or hide the remove buttons + for (let i = 0; i < this.divs.length; i++) { + let cur_div = $(this.divs[i]); + if (!cur_div.find('textarea').is(':disabled')) { + cur_div.find('[id^="remove_field__"]').show(); } + } - }; + }; - this.addField = function() { - var currentLargest = -1; - for (var i = 0; i < this.divs.length; i++) { - var div = $(this.divs[i]); - var id = div.find(":input").attr("id"); - var match = id.match(this.idRx); - var thisId = parseInt(match[2]); - if (thisId > currentLargest) { - currentLargest = thisId; - } + this.addField = function() { + var currentLargest = -1; + for (var i = 0; i < this.divs.length; i++) { + var div = $(this.divs[i]); + var id = div.find(":input").attr("id"); + var match = id.match(this.idRx); + var thisId = parseInt(match[2]); + if (thisId > currentLargest) { + currentLargest = thisId; } - var newId = currentLargest + 1; + } + var newId = currentLargest + 1; - var frag = '
' + this.template + '
'; - var jqt = $(frag); - var that = this; - jqt.find(":input").each(function() { - var el = $(this); - var id = el.attr("id"); - - var match = id.match(that.idRx); - if (match) { - var bits = id.split(that.idRx); - var newName = bits[1] + newId + bits[3]; - el.attr("id", newName).attr("name", newName).val(""); - } else { - // could be the remove button - if (id.substring(0, "remove_field".length) === "remove_field") { - el.attr("id", "remove_field__" + that.fieldDef["name"] + "--id_" + newId); - el.show(); - } - } - }); - if (this.args.enable_on_repeat) { - for (var i = 0; i < this.args.enable_on_repeat.length; i++) { - var enables = jqt.find(that.args.enable_on_repeat[i]); - enables.removeAttr("disabled"); + var frag = '
' + this.template + '
'; + var jqt = $(frag); + var that = this; + jqt.find(":input").each(function() { + var el = $(this); + var id = el.attr("id"); + + var match = id.match(that.idRx); + if (match) { + var bits = id.split(that.idRx); + var newName = bits[1] + newId + bits[3]; + el.attr("id", newName).attr("name", newName).val(""); + } else { + // could be the remove button + if (id.substring(0, "remove_field".length) === "remove_field") { + el.attr("id", "remove_field__" + that.fieldDef["name"] + "--id_" + newId); + el.show(); } } + }); + if (this.args.enable_on_repeat) { + for (var i = 0; i < this.args.enable_on_repeat.length; i++) { + var enables = jqt.find(that.args.enable_on_repeat[i]); + enables.removeAttr("disabled"); + } + } - var topPlacement = this.fieldDef.repeatable.add_button_placement === "top"; - if (this.divs.length > 0) { - if (topPlacement) { - $(this.divs[0]).before(jqt); - } else { - $(this.divs[this.divs.length - 1]).after(jqt); - } + var topPlacement = this.fieldDef.repeatable.add_button_placement === "top"; + if (this.divs.length > 0) { + if (topPlacement) { + $(this.divs[0]).before(jqt); } else { - this.container.append(jqt); + $(this.divs[this.divs.length - 1]).after(jqt); } + } else { + this.container.append(jqt); + } - this.divs = $("div[name='" + this.fieldDef["name"] + "__group']"); - this.removeFieldBtns = $('[id^="remove_field__' + this.fieldDef["name"] + '"]'); - edges.on(this.removeFieldBtns, "click", this, "removeField"); - }; + this.divs = $("div[name='" + this.fieldDef["name"] + "__group']"); + this.removeFieldBtns = $('[id^="remove_field__' + this.fieldDef["name"] + '"]'); + edges.on(this.removeFieldBtns, "click", this, "removeField"); + }; - this.removeField = function(element) { - var container = $(element).parents("div[name='" + this.fieldDef["name"] + "__group']"); - container.remove(); - this.divs = $("div[name='" + this.fieldDef["name"] + "__group']"); - }; + this.removeField = function(element) { + var container = $(element).parents("div[name='" + this.fieldDef["name"] + "__group']"); + container.remove(); + this.divs = $("div[name='" + this.fieldDef["name"] + "__group']"); + }; - this.init(); - }, + this.init(); + }, - newMultipleField : function(params) { - return edges.instantiate(formulaic.widgets.MultipleField, params) - }, - MultipleField: function(params) { - this.fieldDef = params.fieldDef; - this.max = this.fieldDef["repeatable"]["initial"] - 1; + newMultipleField : function(params) { + return edges.instantiate(formulaic.widgets.MultipleField, params) + }, + MultipleField: function(params) { + this.fieldDef = params.fieldDef; + this.max = this.fieldDef["repeatable"]["initial"] - 1; - this.init = () => { - if (this.fieldDef["input"] === "group") { - this._setupRepeatingGroup(); - } else { - this._setupRepeatingIndividual(); - } - feather.replace(); - }; + this.init = () => { + if (this.fieldDef["input"] === "group") { + this._setupRepeatingGroup(); + } else { + this._setupRepeatingIndividual(); + } + feather.replace(); + }; - this._setupIndividualSelect2 = function() { - for (var idx = 0; idx < this.fields.length; idx++) { - let f = this.fields[idx]; - let s2_input = $($(f).select2()); - $(f).on("focus", formulaic.widgets._select2_shift_focus); - s2_input.after($('')); - if (idx !== 0) { - s2_input.attr("required", false); - s2_input.attr("data-parsley-validate-if-empty", "true"); - if (!s2_input.val()) { - s2_input.closest('li').hide(); - } else { - this.count++; - } + this._setupIndividualSelect2 = function() { + for (var idx = 0; idx < this.fields.length; idx++) { + let f = this.fields[idx]; + let s2_input = $($(f).select2()); + $(f).on("focus", formulaic.widgets._select2_shift_focus); + s2_input.after($('')); + if (idx !== 0) { + s2_input.attr("required", false); + s2_input.attr("data-parsley-validate-if-empty", "true"); + if (!s2_input.val()) { + s2_input.closest('li').hide(); + } else { + this.count++; } } + } - this.remove_btns = $('[id^="remove_field__' + this.fieldDef["name"] + '"]'); - if (this.count === 0) { - $(this.remove_btns[0]).hide(); - } - - this.addFieldBtn = $("#add_field__" + this.fieldDef["name"]); - this.addFieldBtn.on("click", () => { - $('#s2id_' + this.fieldDef["name"] + '-' + (this.count + 1)).closest('li').show(); - this.count++; - if (this.count > 0) { - $(this.remove_btns[0]).show(); - } - if (this.count >= this.max) { - $(this.addFieldBtn).hide(); - } - }); + this.remove_btns = $('[id^="remove_field__' + this.fieldDef["name"] + '"]'); + if (this.count === 0) { + $(this.remove_btns[0]).hide(); + } + this.addFieldBtn = $("#add_field__" + this.fieldDef["name"]); + this.addFieldBtn.on("click", () => { + $('#s2id_' + this.fieldDef["name"] + '-' + (this.count + 1)).closest('li').show(); + this.count++; + if (this.count > 0) { + $(this.remove_btns[0]).show(); + } if (this.count >= this.max) { - this.addFieldBtn.hide(); + $(this.addFieldBtn).hide(); } + }); - $(this.remove_btns).each((idx, btn) => { - $(btn).on("click", (event) => { - for (let i = idx; i < this.count; i++) { - let data = $(this.fields[i + 1]).select2('data'); - if (data === null) { - data = {id: i, text: ""}; - } - $(this.fields[i]).select2('data', {id: data.id, text: data.text}); - } - this.count--; - $(this.fields[this.count + 1]).select2('data', {id: this.count + 1, text: ""}); - $('#s2id_' + this.fieldDef["name"] + '-' + (this.count + 1)).closest('li').hide(); - if (this.count === 0) { - $(this.remove_btns[0]).hide(); - } - if (this.count < this.max) { - $(this.addFieldBtn).show(); - } - }) - }) - }; + if (this.count >= this.max) { + this.addFieldBtn.hide(); + } - this._setupIndividualField = function() { - for (var idx = 0; idx < this.fields.length; idx++) { - let f = this.fields[idx]; - let jqf = $(f); - jqf.after($('')); - if (idx !== 0) { - jqf.attr("required", false); - jqf.attr("data-parsley-validate-if-empty", "true"); - if (!jqf.val()) { - jqf.parent().hide(); - } else { - this.count++; + $(this.remove_btns).each((idx, btn) => { + $(btn).on("click", (event) => { + for (let i = idx; i < this.count; i++) { + let data = $(this.fields[i + 1]).select2('data'); + if (data === null) { + data = {id: i, text: ""}; } + $(this.fields[i]).select2('data', {id: data.id, text: data.text}); } - } + this.count--; + $(this.fields[this.count + 1]).select2('data', {id: this.count + 1, text: ""}); + $('#s2id_' + this.fieldDef["name"] + '-' + (this.count + 1)).closest('li').hide(); + if (this.count === 0) { + $(this.remove_btns[0]).hide(); + } + if (this.count < this.max) { + $(this.addFieldBtn).show(); + } + }) + }) + }; - this.remove_btns = $('[id^="remove_field__' + this.fieldDef["name"] + '-"]'); - if (this.count === 0) { - $(this.remove_btns[0]).hide(); + this._setupIndividualField = function() { + for (var idx = 0; idx < this.fields.length; idx++) { + let f = this.fields[idx]; + let jqf = $(f); + jqf.after($('')); + if (idx !== 0) { + jqf.attr("required", false); + jqf.attr("data-parsley-validate-if-empty", "true"); + if (!jqf.val()) { + jqf.parent().hide(); + } else { + this.count++; + } } + } - this.addFieldBtn = $("#add_field__" + this.fieldDef["name"]); - this.addFieldBtn.on("click", () => { - let next_input = $('[id="' + this.fieldDef["name"] + '-' + (this.count + 1) +'"]').parent(); - // TODO: why .show() does not work? - $(next_input).show(); - this.count++; - if (this.count > 0) { - $(this.remove_btns[0]).show(); - } - if (this.count >= this.max) { - $(this.addFieldBtn).hide(); - } - }); + this.remove_btns = $('[id^="remove_field__' + this.fieldDef["name"] + '-"]'); + if (this.count === 0) { + $(this.remove_btns[0]).hide(); + } + this.addFieldBtn = $("#add_field__" + this.fieldDef["name"]); + this.addFieldBtn.on("click", () => { + let next_input = $('[id="' + this.fieldDef["name"] + '-' + (this.count + 1) +'"]').parent(); + // TODO: why .show() does not work? + $(next_input).show(); + this.count++; + if (this.count > 0) { + $(this.remove_btns[0]).show(); + } if (this.count >= this.max) { - this.addFieldBtn.hide(); + $(this.addFieldBtn).hide(); } + }); - $(this.remove_btns).each((idx, btn) => { - $(btn).on("click", (event) => { - for (let i = idx; i < this.count; i++) { - let data = $(this.fields[i + 1]).val(); - if (data === null) { - data = ""; - } - $(this.fields[i]).val(data); - } + if (this.count >= this.max) { + this.addFieldBtn.hide(); + } - this.count--; - $(this.fields[this.count + 1]).val(""); - let last_input = $('[id="' + this.fieldDef["name"] + '-' + (this.count + 1) +'"]').parent(); - $(last_input).hide(); - if (this.count === 0) { - $(this.remove_btns[0]).hide(); + $(this.remove_btns).each((idx, btn) => { + $(btn).on("click", (event) => { + for (let i = idx; i < this.count; i++) { + let data = $(this.fields[i + 1]).val(); + if (data === null) { + data = ""; } - if (this.count < this.max) { - $(this.addFieldBtn).show(); - } - }) - }); - }; + $(this.fields[i]).val(data); + } - this._setupRepeatingIndividual = function() { - let tag = this.fieldDef["input"] === "select" ? "select" : "input"; - this.fields = $(tag + '[id^="' + this.fieldDef["name"] + '-"]'); - this.count = 0; + this.count--; + $(this.fields[this.count + 1]).val(""); + let last_input = $('[id="' + this.fieldDef["name"] + '-' + (this.count + 1) +'"]').parent(); + $(last_input).hide(); + if (this.count === 0) { + $(this.remove_btns[0]).hide(); + } + if (this.count < this.max) { + $(this.addFieldBtn).show(); + } + }) + }); + }; - if (tag === "select"){ - this._setupIndividualSelect2(); - } else { - this._setupIndividualField(); - } - }; + this._setupRepeatingIndividual = function() { + let tag = this.fieldDef["input"] === "select" ? "select" : "input"; + this.fields = $(tag + '[id^="' + this.fieldDef["name"] + '-"]'); + this.count = 0; - this._setupRepeatingGroup = function() { - this.divs = $("div[name='" + this.fieldDef["name"] + "__group']"); - this.count = 0; - - for (var idx = 0; idx < this.divs.length; idx++) { - let div = $(this.divs[idx]); - div.append($('')); - - if (idx !== 0) { - let inputs = div.find(":input"); - var hasVal = false; - for (var j = 0; j < inputs.length; j++) { - $(inputs[j]).attr("required", false) - .attr("data-parsley-required-if", false) - .attr("data-parsley-validate-if-empty", "true"); - if ($(inputs[j]).val()) { - hasVal = true; - } - } - if (!hasVal) { - div.hide(); - } else { - this.count++; - } - } - } + if (tag === "select"){ + this._setupIndividualSelect2(); + } else { + this._setupIndividualField(); + } + }; - this.remove_btns = $('[id^="remove_field__' + this.fieldDef["name"] + '"]'); - if (this.count === 0) { - $(this.remove_btns[0]).hide(); - } + this._setupRepeatingGroup = function() { + this.divs = $("div[name='" + this.fieldDef["name"] + "__group']"); + this.count = 0; - this.addFieldBtn = $("#add_field__" + this.fieldDef["name"]); - this.addFieldBtn.on("click", () => { - $(this.divs[this.count + 1]).show(); - this.count++; - if (this.count > 0) { - $(this.remove_btns[0]).show(); + for (var idx = 0; idx < this.divs.length; idx++) { + let div = $(this.divs[idx]); + div.append($('')); + + if (idx !== 0) { + let inputs = div.find(":input"); + var hasVal = false; + for (var j = 0; j < inputs.length; j++) { + $(inputs[j]).attr("required", false) + .attr("data-parsley-required-if", false) + .attr("data-parsley-validate-if-empty", "true"); + if ($(inputs[j]).val()) { + hasVal = true; + } } - if (this.count === this.max) { - $(this.addFieldBtn).hide(); + if (!hasVal) { + div.hide(); + } else { + this.count++; } - }); + } + } - if (this.count >= this.max) { - this.addFieldBtn.hide(); + this.remove_btns = $('[id^="remove_field__' + this.fieldDef["name"] + '"]'); + if (this.count === 0) { + $(this.remove_btns[0]).hide(); + } + + this.addFieldBtn = $("#add_field__" + this.fieldDef["name"]); + this.addFieldBtn.on("click", () => { + $(this.divs[this.count + 1]).show(); + this.count++; + if (this.count > 0) { + $(this.remove_btns[0]).show(); + } + if (this.count === this.max) { + $(this.addFieldBtn).hide(); } + }); - $(this.remove_btns).each((idx, btn) => { - $(btn).on("click", () => { - let thisDiv = $(btn).parent(); - let nextDiv = $(thisDiv); - for (let i = idx; i < this.count; i++) { - thisDiv = nextDiv; - nextDiv = nextDiv.next(); - let thisInputs = $(thisDiv).find("select, input[id^='" + this.fieldDef["name"] + "']"); - let nextInputs = $(nextDiv).find("select, input[id^='" + this.fieldDef["name"] + "']"); - for (let j = 0; j < thisInputs.length; j++){ - let thisInput = $(thisInputs[j]); - let nextInput = $(nextInputs[j]); - if (thisInput.is("select")){ - let data = $(nextInput).select2('data'); - if (data === null) { - data = {id: i, text: ""}; - } - $(thisInput).select2('data', {id: data.id, text: data.text}); + if (this.count >= this.max) { + this.addFieldBtn.hide(); + } + $(this.remove_btns).each((idx, btn) => { + $(btn).on("click", () => { + let thisDiv = $(btn).parent(); + let nextDiv = $(thisDiv); + for (let i = idx; i < this.count; i++) { + thisDiv = nextDiv; + nextDiv = nextDiv.next(); + let thisInputs = $(thisDiv).find("select, input[id^='" + this.fieldDef["name"] + "']"); + let nextInputs = $(nextDiv).find("select, input[id^='" + this.fieldDef["name"] + "']"); + for (let j = 0; j < thisInputs.length; j++){ + let thisInput = $(thisInputs[j]); + let nextInput = $(nextInputs[j]); + if (thisInput.is("select")){ + let data = $(nextInput).select2('data'); + if (data === null) { + data = {id: i, text: ""}; } - else { - $(thisInputs[j]).val($(nextInputs[j]).val()); - } - } - } - this.count--; - $(this.divs[this.count + 1]).find("select, input[id^='" + this.fieldDef["name"] + "']").each((idx, inp) => { - if ($(inp).is("select")){ - $(inp).select2('data', {id: this.count + 1, text: ""}); + $(thisInput).select2('data', {id: data.id, text: data.text}); + } else { - $(inp).val(""); + $(thisInputs[j]).val($(nextInputs[j]).val()); } - }); - $(this.divs[this.count + 1]).hide(); - if (this.count === 0) { - $(this.remove_btns[0]).hide(); } - if (this.count < this.max) { - $(this.addFieldBtn).show(); + } + this.count--; + $(this.divs[this.count + 1]).find("select, input[id^='" + this.fieldDef["name"] + "']").each((idx, inp) => { + if ($(inp).is("select")){ + $(inp).select2('data', {id: this.count + 1, text: ""}); + } + else { + $(inp).val(""); } - }) + }); + $(this.divs[this.count + 1]).hide(); + if (this.count === 0) { + $(this.remove_btns[0]).hide(); + } + if (this.count < this.max) { + $(this.addFieldBtn).show(); + } }) - }; + }) + }; - this.init() - }, + this.init() + }, - newSelect : function(params) { - return edges.instantiate(formulaic.widgets.Select, params); - }, - Select : function(params) { - this.fieldDef = params.fieldDef; - this.form = params.formulaic; - this.args = params.args; + newSelect : function(params) { + return edges.instantiate(formulaic.widgets.Select, params); + }, + Select : function(params) { + this.fieldDef = params.fieldDef; + this.form = params.formulaic; + this.args = params.args; - this.ns = "formulaic-select"; - this.elements = false; + this.ns = "formulaic-select"; + this.elements = false; - this.init = function() { - let allow_clear = this.args.allow_clear || false; - this.elements = $("select[name$='" + this.fieldDef.name + "']"); - this.elements.select2({ - allowClear: allow_clear, - newOption: true, - placeholder: "Start typing…" - }); - $(this.elements).on("focus", formulaic.widgets._select2_shift_focus); - }; + this.init = function() { + let allow_clear = this.args.allow_clear || false; + this.elements = $("select[name$='" + this.fieldDef.name + "']"); + this.elements.select2({ + allowClear: allow_clear, + newOption: true, + placeholder: "Start typing…" + }); + $(this.elements).on("focus", formulaic.widgets._select2_shift_focus); + }; - this.init(); - }, - - newTagList : function(params) { - return edges.instantiate(formulaic.widgets.TagList, params); - }, - TagList : function(params) { - this.fieldDef = params.fieldDef; - this.form = params.formulaic; - this.args = params.args; - - this.ns = "formulaic-taglist"; - - this.init = function() { - var stopWords = edges.getParam(this.args.stopWords, []); - - var ajax = { - url: current_scheme + "//" + current_domain + "/autocomplete/journal/" + this.args["field"], - dataType: 'json', - data: function (term, page) { - return { - q: term - }; - }, - results: function (data, page) { - return {results: data["suggestions"].filter((x) => $.inArray(x.text.toLowerCase(), stopWords) === -1)}; - } - }; + this.init(); + }, - var csc = function (term) { - if ($.inArray(term.toLowerCase(), stopWords) !== -1) { - return null; - } - return {id: $.trim(term), text: $.trim(term)}; - }; + newTagList : function(params) { + return edges.instantiate(formulaic.widgets.TagList, params); + }, + TagList : function(params) { + this.fieldDef = params.fieldDef; + this.form = params.formulaic; + this.args = params.args; + this.ns = "formulaic-taglist"; - var initSel = function (element, callback) { - var initial = element.val(); - var entries = initial.split(",").map(x => x.trim()).filter(x => x !== ""); - var data = []; - for (var i = 0; i < entries.length; i++) { - data.push({id: entries[i], text: entries[i]}); - } - callback(data); - }; + this.init = function() { + var stopWords = edges.getParam(this.args.stopWords, []); + + var ajax = { + url: current_scheme + "//" + current_domain + "/autocomplete/journal/" + this.args["field"], + dataType: 'json', + data: function (term, page) { + return { + q: term + }; + }, + results: function (data, page) { + return {results: data["suggestions"].filter((x) => $.inArray(x.text.toLowerCase(), stopWords) === -1)}; + } + }; - // apply the create search choice - let selector = "[name='" + this.fieldDef.name + "']"; - $(selector).select2({ - multiple: true, - minimumInputLength: 1, - ajax: ajax, - createSearchChoice: csc, - initSelection: initSel, - placeholder: "Start typing…", - allowClear: false, - tags: true, - tokenSeparators: [',', ";"], - maximumSelectionSize: this.args["maximumSelectionSize"], - width: 'resolve' - }); + var csc = function (term) { + if ($.inArray(term.toLowerCase(), stopWords) !== -1) { + return null; + } + return {id: $.trim(term), text: $.trim(term)}; + }; - $(selector).on("focus", formulaic.widgets._select2_shift_focus); + var initSel = function (element, callback) { + var initial = element.val(); + var entries = initial.split(",").map(x => x.trim()).filter(x => x !== ""); + var data = []; + for (var i = 0; i < entries.length; i++) { + data.push({id: entries[i], text: entries[i]}); + } + callback(data); }; - this.init(); - }, + // apply the create search choice + let selector = "[name='" + this.fieldDef.name + "']"; + $(selector).select2({ + multiple: true, + minimumInputLength: 1, + ajax: ajax, + createSearchChoice: csc, + initSelection: initSel, + placeholder: "Start typing…", + allowClear: false, + tags: true, + tokenSeparators: [',', ";"], + maximumSelectionSize: this.args["maximumSelectionSize"], + width: 'resolve' + }); - newTagEntry : function(params) { - return edges.instantiate(formulaic.widgets.TagEntry, params); - }, - TagEntry : function(params) { - this.fieldDef = params.fieldDef; - this.form = params.formulaic; - this.args = params.args; + $(selector).on("focus", formulaic.widgets._select2_shift_focus); - this.ns = "formulaic-tagentry"; + }; - this.init = function() { - let selector = "[name='" + this.fieldDef.name + "']"; - $(selector).select2({ - minimumInputLength: 1, - tags: [], - tokenSeparators: [','], - width: 'resolve' - }); - $(selector).on("focus", formulaic.widgets._select2_shift_focus); - }; + this.init(); + }, - this.init(); - }, + newTagEntry : function(params) { + return edges.instantiate(formulaic.widgets.TagEntry, params); + }, + TagEntry : function(params) { + this.fieldDef = params.fieldDef; + this.form = params.formulaic; + this.args = params.args; + this.ns = "formulaic-tagentry"; + this.init = function() { + let selector = "[name='" + this.fieldDef.name + "']"; + $(selector).select2({ + minimumInputLength: 1, + tags: [], + tokenSeparators: [','], + width: 'resolve' + }); + $(selector).on("focus", formulaic.widgets._select2_shift_focus); + }; - newLoadEditors: function(params) { - return edges.instantiate(formulaic.widgets.LoadEditors, params); - }, + this.init(); + }, - LoadEditors: function(params) { - this.fieldDef = params.fieldDef; - this.params = params.args; - this.groupField = false; - this.editorField = false; - this.init = function() { - this.groupField = $("[name='" + this.fieldDef.name + "']"); - this.editorField = $("[name='" + this.params.field + "']"); - edges.on(this.groupField, "change", this, "updateEditors"); - }; + newLoadEditors: function(params) { + return edges.instantiate(formulaic.widgets.LoadEditors, params); + }, - this.updateEditors = function(element) { - var ed_group_name = $(element).val(); - var ed_query_url = "/admin/dropdown/eg_associates"; + LoadEditors: function(params) { + this.fieldDef = params.fieldDef; + this.params = params.args; - // var ed_group_name = $("#s2id_editor_group").find('span').text(); - var that = this; - $.ajax({ - type : "GET", - data : {egn : ed_group_name}, - dataType: "json", - url: ed_query_url, - success: function(resp) - { - // Get the options for the drop-down from our ajax request - var assoc_options = []; - if (resp != null) - { - assoc_options = [["", "No editor assigned"]]; + this.groupField = false; + this.editorField = false; - for (var i=0; i").attr("value", assoc_options[j][0]).text(assoc_options[j][1]) - ); + // var ed_group_name = $("#s2id_editor_group").find('span').text(); + var that = this; + $.ajax({ + type : "GET", + data : {egn : ed_group_name}, + dataType: "json", + url: ed_query_url, + success: function(resp) + { + // Get the options for the drop-down from our ajax request + var assoc_options = []; + if (resp != null) + { + assoc_options = [["", "No editor assigned"]]; + + for (var i=0; i").attr("value", assoc_options[j][0]).text(assoc_options[j][1]) + ); + } + } + }) + }; - let selector = "[name='" + this.fieldDef.name + "']"; + this.init(); + }, - $(selector).on("focus", formulaic.widgets._select2_shift_focus); + newAutocomplete: function(params){ + return edges.instantiate(formulaic.widgets.Autocomplete, params); + }, - if (include_input) { - // apply the create search choice - $(selector).select2({ - minimumInputLength: mininput, - ajax: ajax, - createSearchChoice: csc, - initSelection : initSel, - allowClear: allow_clear, - width: 'resolve' - }); - } else { - // go without the create search choice option - $(selector).select2({ - minimumInputLength: mininput, - ajax: ajax, - initSelection : initSel, - allowClear: allow_clear, - width: 'resolve' - }); + Autocomplete: function(params){ + this.fieldDef = params.fieldDef; + this.params = params.args; + + this.init = function() { + let doc_type = this.params.type || "journal"; + let doc_field = this.params.field; + let mininput = this.params.min_input === undefined ? 3 : this.params.min_input; + let include_input = this.params.include === undefined ? true : this.params.include; + let allow_clear = this.params.allow_clear_input === undefined ? true : this.params.allow_clear_input; + + let ajax = { + url: current_scheme + "//" + current_domain + "/autocomplete/" + doc_type + "/" + doc_field, + dataType: 'json', + data: function (term, page) { + return { + q: term + }; + }, + results: function (data, page) { + return { results: data["suggestions"] }; } + }; - $(selector).on("focus", formulaic.widgets._select2_shift_focus); + var csc = function(term) {return {"id":term, "text": term};}; + + var initSel = function (element, callback) { + var data = {id: element.val(), text: element.val()}; + callback(data); }; - this.init() - }, - newIssnLink : function(params) { - return edges.instantiate(formulaic.widgets.IssnLink, params) - }, - IssnLink : function(params) { - this.fieldDef = params.fieldDef; - this.form = params.formulaic; - this.issn = params.issn; + let selector = "[name='" + this.fieldDef.name + "']"; - this.ns = "formulaic-issnlink"; + $(selector).on("focus", formulaic.widgets._select2_shift_focus); - this.link = false; - this.url = "https://portal.issn.org/resource/ISSN/"; + if (include_input) { + // apply the create search choice + $(selector).select2({ + minimumInputLength: mininput, + ajax: ajax, + createSearchChoice: csc, + initSelection : initSel, + allowClear: allow_clear, + width: 'resolve' + }); + } else { + // go without the create search choice option + $(selector).select2({ + minimumInputLength: mininput, + ajax: ajax, + initSelection : initSel, + allowClear: allow_clear, + width: 'resolve' + }); + } - this.init = function() { - var elements = this.form.controlSelect.input( - {name: this.fieldDef.name}); - edges.on(elements, "keyup.IssnLink", this, "updateUrl"); + $(selector).on("focus", formulaic.widgets._select2_shift_focus); + }; - for (var i = 0; i < elements.length; i++) { - this.updateUrl(elements[i]); - } - }; + this.init() + }, + newIssnLink : function(params) { + return edges.instantiate(formulaic.widgets.IssnLink, params) + }, + IssnLink : function(params) { + this.fieldDef = params.fieldDef; + this.form = params.formulaic; + this.issn = params.issn; - this.updateUrl = function(element) { - var that = $(element); - var val = that.val(); - var id = edges.css_id(this.ns, this.fieldDef.name); + this.ns = "formulaic-issnlink"; - var match = val.match(/[d0-9]{4}-{0,1}[0-9]{3}[0-9xX]{1}/); - var url = this.url + val; + this.link = false; + this.url = "https://portal.issn.org/resource/ISSN/"; - if (val && match) { - if (this.link) { - this.link.text(url); - this.link.attr("href", url); - } else { - var classes = edges.css_classes(this.ns, "visit"); - that.after('

' + url + '

'); + this.init = function() { + var elements = this.form.controlSelect.input( + {name: this.fieldDef.name}); + edges.on(elements, "keyup.IssnLink", this, "updateUrl"); - var selector = edges.css_id_selector(this.ns, this.fieldDef.name); - this.link = $(selector, this.form.context); - } - } else if (this.link) { - this.link.remove(); - this.link = false; + for (var i = 0; i < elements.length; i++) { + this.updateUrl(elements[i]); + } + }; + + this.updateUrl = function(element) { + var that = $(element); + var val = that.val(); + var id = edges.css_id(this.ns, this.fieldDef.name); + + var match = val.match(/[d0-9]{4}-{0,1}[0-9]{3}[0-9xX]{1}/); + var url = this.url + val; + + if (val && match) { + if (this.link) { + this.link.text(url); + this.link.attr("href", url); + } else { + var classes = edges.css_classes(this.ns, "visit"); + that.after('

' + url + '

'); + + var selector = edges.css_id_selector(this.ns, this.fieldDef.name); + this.link = $(selector, this.form.context); } - }; + } else if (this.link) { + this.link.remove(); + this.link = false; + } + }; - this.init(); - }, - } - }; + this.init(); + }, + } +}; From 8874351e1522b22119c78d068290ba0f4e2be09d Mon Sep 17 00:00:00 2001 From: Aga Date: Tue, 18 Jul 2023 13:51:25 +0100 Subject: [PATCH 28/56] clean up part 2 --- portality/static/js/formulaic.js | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/portality/static/js/formulaic.js b/portality/static/js/formulaic.js index 55935d606d..5e08724644 100644 --- a/portality/static/js/formulaic.js +++ b/portality/static/js/formulaic.js @@ -1198,6 +1198,27 @@ var formulaic = { newClickableOwner : function(params) { return edges.instantiate(formulaic.widgets.ClickableOwner, params) }, + newClickToCopy : function(params) { + return edges.instantiate(formulaic.widgets.ClickToCopy, params) + }, + ClickToCopy : function(params) { + this.fieldDef = params.fieldDef; + this.init = function() { + var elements = $("#click-to-copy--" + this.fieldDef.name); + edges.on(elements, "click", this, "copy"); + }; + this.copy = function(element) { + let form = new doaj.af.BaseApplicationForm() + let value = form.determineFieldsValue(this.fieldDef.name) + let value_to_copy = form.convertValueToText(value); + navigator.clipboard.writeText(value_to_copy) + var confirmation = $("#copy-confirmation--" + this.fieldDef.name); + confirmation.text("Value copied: " + value_to_copy); + confirmation.show().delay(3000).fadeOut(); + }; + this.init(); + + }, ClickableOwner : function(params) { this.fieldDef = params.fieldDef; this.form = params.formulaic; From 0996481255198719edab47ff9d2c243d6cada8c8 Mon Sep 17 00:00:00 2001 From: Aga Date: Tue, 25 Jul 2023 09:22:00 +0100 Subject: [PATCH 29/56] change order of functions in formulaic --- portality/static/js/formulaic.js | 41 ++++++++++++++++---------------- 1 file changed, 20 insertions(+), 21 deletions(-) diff --git a/portality/static/js/formulaic.js b/portality/static/js/formulaic.js index 5e08724644..58ddf4f403 100644 --- a/portality/static/js/formulaic.js +++ b/portality/static/js/formulaic.js @@ -1198,27 +1198,6 @@ var formulaic = { newClickableOwner : function(params) { return edges.instantiate(formulaic.widgets.ClickableOwner, params) }, - newClickToCopy : function(params) { - return edges.instantiate(formulaic.widgets.ClickToCopy, params) - }, - ClickToCopy : function(params) { - this.fieldDef = params.fieldDef; - this.init = function() { - var elements = $("#click-to-copy--" + this.fieldDef.name); - edges.on(elements, "click", this, "copy"); - }; - this.copy = function(element) { - let form = new doaj.af.BaseApplicationForm() - let value = form.determineFieldsValue(this.fieldDef.name) - let value_to_copy = form.convertValueToText(value); - navigator.clipboard.writeText(value_to_copy) - var confirmation = $("#copy-confirmation--" + this.fieldDef.name); - confirmation.text("Value copied: " + value_to_copy); - confirmation.show().delay(3000).fadeOut(); - }; - this.init(); - - }, ClickableOwner : function(params) { this.fieldDef = params.fieldDef; this.form = params.formulaic; @@ -1258,7 +1237,27 @@ var formulaic = { this.init(); }, + newClickToCopy : function(params) { + return edges.instantiate(formulaic.widgets.ClickToCopy, params) + }, + ClickToCopy : function(params) { + this.fieldDef = params.fieldDef; + this.init = function() { + var elements = $("#click-to-copy--" + this.fieldDef.name); + edges.on(elements, "click", this, "copy"); + }; + this.copy = function(element) { + let form = new doaj.af.BaseApplicationForm() + let value = form.determineFieldsValue(this.fieldDef.name) + let value_to_copy = form.convertValueToText(value); + navigator.clipboard.writeText(value_to_copy) + var confirmation = $("#copy-confirmation--" + this.fieldDef.name); + confirmation.text("Value copied: " + value_to_copy); + confirmation.show().delay(3000).fadeOut(); + }; + this.init(); + }, newTrimWhitespace : function(params) { return edges.instantiate(formulaic.widgets.TrimWhitespace, params) }, From e313ee5fcd9dd52568c4458bd9e8e9c2f71c84ab Mon Sep 17 00:00:00 2001 From: Richard Jones Date: Fri, 4 Aug 2023 14:33:58 +0100 Subject: [PATCH 30/56] minor code layout tweaks --- portality/bll/services/article.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/portality/bll/services/article.py b/portality/bll/services/article.py index 77aac68600..777d499636 100644 --- a/portality/bll/services/article.py +++ b/portality/bll/services/article.py @@ -170,7 +170,7 @@ def _validate_issns(article_bibjson: models.ArticleBibJSON): def create_article(self, article, account, duplicate_check=True, merge_duplicate=True, limit_to_account=True, add_journal_info=False, dry_run=False, update_article_id=None): - """# no need to check eissn, if pissn matches, pissn and eissn are different and only 1 journal has been found - then eissn matches too + """ Create an individual article in the database This method will check and merge any duplicates, and report back on successes and failures in a manner consistent with @@ -249,7 +249,8 @@ def has_permissions(self, account, article, limit_to_account): def is_acceptable(self, article: models.Article): """ conduct some deep validation on the article to make sure we will accept it - or the moment, this just means making sure it has a DOI and a fulltext + this just means making sure it has a DOI and a fulltext, and that its ISSNs + match a single journal """ try: bj = article.bibjson() @@ -266,7 +267,6 @@ def is_acceptable(self, article: models.Article): journal = self.match_journal_with_validation(bj) # is journal in doaj (we do this check last as it has more performance impact) - # journal = article.get_journal() if journal is None or not journal.is_in_doaj(): raise exceptions.ArticleNotAcceptable(message=Messages.EXCEPTION_ADDING_ARTICLE_TO_WITHDRAWN_JOURNAL) @@ -296,6 +296,7 @@ def match_journal_with_validation(article_bibjson: models.ArticleBibJSON): raise exceptions.ArticleNotAcceptable(message=Messages.EXCEPTION_MISMATCHED_ISSNS) return journal[0] + @staticmethod def is_legitimate_owner(article, owner): """ From 1fbe10c628bcfd4782cdae73b8f9a035bcfd8cb7 Mon Sep 17 00:00:00 2001 From: Steve Eardley Date: Fri, 4 Aug 2023 14:53:36 +0100 Subject: [PATCH 31/56] Fix a few warnings from the test run caused by our code --- .../test_application_processor_emails.py | 3 ++- doajtest/unit/test_formrender.py | 8 ++++++++ portality/bll/services/background_task_status.py | 2 +- portality/core.py | 4 ++-- portality/dao.py | 14 +++++++------- portality/tasks/async_workflow_notifications.py | 2 +- portality/tasks/harvester_helpers/epmc/client.py | 4 ++-- portality/tasks/helpers/background_helper.py | 4 ++-- portality/view/admin.py | 6 +++--- 9 files changed, 28 insertions(+), 19 deletions(-) diff --git a/doajtest/unit/application_processors/test_application_processor_emails.py b/doajtest/unit/application_processors/test_application_processor_emails.py index 44630b518c..05bd355700 100644 --- a/doajtest/unit/application_processors/test_application_processor_emails.py +++ b/doajtest/unit/application_processors/test_application_processor_emails.py @@ -65,13 +65,14 @@ def editor_account_pull(self, _id): ACTUAL_ACCOUNT_PULL = models.Account.pull # A regex string for searching the log entries -email_log_regex = 'template.*%s.*to:\[u{0,1}\'%s.*subject:.*%s' +email_log_regex = r'template.*%s.*to:\[u{0,1}\'%s.*subject:.*%s' # A string present in each email log entry (for counting them) email_count_string = 'Email template' NOTIFICATIONS_INTERCEPT = [] + class TestPublicApplicationEmails(DoajTestCase): def setUp(self): super(TestPublicApplicationEmails, self).setUp() diff --git a/doajtest/unit/test_formrender.py b/doajtest/unit/test_formrender.py index 1d655420af..f57ee83a61 100644 --- a/doajtest/unit/test_formrender.py +++ b/doajtest/unit/test_formrender.py @@ -6,11 +6,16 @@ # Form context for basic test ################################################################ + class TestForm(Form): + __test__ = False # Prevent collection by PyTest one = StringField("One") two = StringField("Two") + class TestRenderer(Renderer): + __test__ = False # Prevent collection by PyTest + def __init__(self): super(TestRenderer, self).__init__() self.FIELD_GROUPS = { @@ -20,7 +25,10 @@ def __init__(self): ] } + class TestContext(FormContext): + __test__ = False # Prevent collection by PyTest + def data2form(self): self.form = TestForm(formdata=self.form_data) diff --git a/portality/bll/services/background_task_status.py b/portality/bll/services/background_task_status.py index 486fdb1d84..ae0c6b7908 100644 --- a/portality/bll/services/background_task_status.py +++ b/portality/bll/services/background_task_status.py @@ -95,7 +95,7 @@ def create_queues_status(self, queue_name) -> dict: # prepare for err_msgs limited_sec = app.config.get('BG_MONITOR_LAST_COMPLETED', {}).get(queue_name) if limited_sec is None: - app.logger.warn(f'BG_MONITOR_LAST_COMPLETED for {queue_name} not found ') + app.logger.warning(f'BG_MONITOR_LAST_COMPLETED for {queue_name} not found ') err_msgs = [] if limited_sec is not None and last_completed_date: diff --git a/portality/core.py b/portality/core.py index 0a09313632..cc14ff79af 100644 --- a/portality/core.py +++ b/portality/core.py @@ -227,11 +227,11 @@ def initialise_index(app, conn, only_mappings=None): :return: """ if not app.config['INITIALISE_INDEX']: - app.logger.warn('INITIALISE_INDEX config var is not True, initialise_index command cannot run') + app.logger.warning('INITIALISE_INDEX config var is not True, initialise_index command cannot run') return if app.config.get("READ_ONLY_MODE", False) and app.config.get("SCRIPTS_READ_ONLY_MODE", False): - app.logger.warn("System is in READ-ONLY mode, initialise_index command cannot run") + app.logger.warning("System is in READ-ONLY mode, initialise_index command cannot run") return # get the app mappings diff --git a/portality/dao.py b/portality/dao.py index 14c5ad125f..40f14adc7c 100644 --- a/portality/dao.py +++ b/portality/dao.py @@ -136,7 +136,7 @@ def save(self, retries=0, back_off_factor=1, differentiate=False, blocking=False :return: """ if app.config.get("READ_ONLY_MODE", False) and app.config.get("SCRIPTS_READ_ONLY_MODE", False): - app.logger.warn("System is in READ-ONLY mode, save command cannot run") + app.logger.warning("System is in READ-ONLY mode, save command cannot run") return if retries > app.config.get("ES_RETRY_HARD_LIMIT", 1000): # an arbitrary large number @@ -220,7 +220,7 @@ def save(self, retries=0, back_off_factor=1, differentiate=False, blocking=False def delete(self): if app.config.get("READ_ONLY_MODE", False) and app.config.get("SCRIPTS_READ_ONLY_MODE", False): - app.logger.warn("System is in READ-ONLY mode, delete command cannot run") + app.logger.warning("System is in READ-ONLY mode, delete command cannot run") return # r = requests.delete(self.target() + self.id) @@ -313,7 +313,7 @@ def bulk(cls, documents: List[dict], idkey='id', refresh=False, action='index', """ # ~~->ReadOnlyMode:Feature~~ if app.config.get("READ_ONLY_MODE", False) and app.config.get("SCRIPTS_READ_ONLY_MODE", False): - app.logger.warn("System is in READ-ONLY mode, bulk command cannot run") + app.logger.warning("System is in READ-ONLY mode, bulk command cannot run") return if action not in ['index', 'update', 'delete']: @@ -363,7 +363,7 @@ def refresh(cls): :return: """ if app.config.get("READ_ONLY_MODE", False) and app.config.get("SCRIPTS_READ_ONLY_MODE", False): - app.logger.warn("System is in READ-ONLY mode, refresh command cannot run") + app.logger.warning("System is in READ-ONLY mode, refresh command cannot run") return # r = requests.post(cls.target() + '_refresh', headers=CONTENT_TYPE_JSON) @@ -449,7 +449,7 @@ def send_query(cls, qobj, retry=50, **kwargs): @classmethod def remove_by_id(cls, id): if app.config.get("READ_ONLY_MODE", False) and app.config.get("SCRIPTS_READ_ONLY_MODE", False): - app.logger.warn("System is in READ-ONLY mode, delete_by_id command cannot run") + app.logger.warning("System is in READ-ONLY mode, delete_by_id command cannot run") return # r = requests.delete(cls.target() + id) @@ -461,7 +461,7 @@ def remove_by_id(cls, id): @classmethod def delete_by_query(cls, query): if app.config.get("READ_ONLY_MODE", False) and app.config.get("SCRIPTS_READ_ONLY_MODE", False): - app.logger.warn("System is in READ-ONLY mode, delete_by_query command cannot run") + app.logger.warning("System is in READ-ONLY mode, delete_by_query command cannot run") return #r = requests.delete(cls.target() + "_query", data=json.dumps(query)) @@ -472,7 +472,7 @@ def delete_by_query(cls, query): @classmethod def destroy_index(cls): if app.config.get("READ_ONLY_MODE", False) and app.config.get("SCRIPTS_READ_ONLY_MODE", False): - app.logger.warn("System is in READ-ONLY mode, destroy_index command cannot run") + app.logger.warning("System is in READ-ONLY mode, destroy_index command cannot run") return # if app.config['ELASTIC_SEARCH_INDEX_PER_TYPE']: diff --git a/portality/tasks/async_workflow_notifications.py b/portality/tasks/async_workflow_notifications.py index 4bea0b1104..b0236af7ec 100644 --- a/portality/tasks/async_workflow_notifications.py +++ b/portality/tasks/async_workflow_notifications.py @@ -333,7 +333,7 @@ def associate_editor_notifications(emails_dict, limit=None): assoc_email = assoc.email except AttributeError: # There isn't an account for that id - app.logger.warn("No account found for ID {0}".format(assoc_id)) + app.logger.warning("No account found for ID {0}".format(assoc_id)) continue text = render_template('email/workflow_reminder_fragments/assoc_ed_age_frag', num_idle=idle, x_days=X_DAYS, num_very_idle=very_idle, y_weeks=Y_WEEKS, url=url) diff --git a/portality/tasks/harvester_helpers/epmc/client.py b/portality/tasks/harvester_helpers/epmc/client.py index fb742b0714..2957e9fe62 100644 --- a/portality/tasks/harvester_helpers/epmc/client.py +++ b/portality/tasks/harvester_helpers/epmc/client.py @@ -37,9 +37,9 @@ def check_epmc_version(resp_json): received_ver = resp_json['version'] configured_ver = app.config.get("EPMC_TARGET_VERSION") if received_ver != configured_ver: - app.logger.warn("Mismatching EPMC API version; recommend checking for changes. Expected '{0}' Found '{1}'".format(configured_ver, received_ver)) + app.logger.warning("Mismatching EPMC API version; recommend checking for changes. Expected '{0}' Found '{1}'".format(configured_ver, received_ver)) except KeyError: - app.logger.warn("Couldn't check EPMC API version; did not find 'version' key in response. Proceed with caution as the EPMC API may have changed.") + app.logger.warning("Couldn't check EPMC API version; did not find 'version' key in response. Proceed with caution as the EPMC API may have changed.") def to_keywords(s): diff --git a/portality/tasks/helpers/background_helper.py b/portality/tasks/helpers/background_helper.py index 2790475729..66a15343e8 100644 --- a/portality/tasks/helpers/background_helper.py +++ b/portality/tasks/helpers/background_helper.py @@ -26,7 +26,7 @@ def get_queue_id_by_task_queue(task_queue: RedisHuey): elif task_queue.name == main_queue.name: return constants.BGJOB_QUEUE_ID_MAIN else: - app.logger.warn(f'unknown task_queue[{task_queue}]') + app.logger.warning(f'unknown task_queue[{task_queue}]') return constants.BGJOB_QUEUE_ID_UNKNOWN @@ -141,7 +141,7 @@ def _load_bgtask_safe(_mi): return _mi.module_finder.find_spec(_mi.name).loader.load_module(_mi.name) except RuntimeError as e: if 'No configuration for scheduled action' in str(e): - app.logger.warn(f'config for {_mi.name} not found') + app.logger.warning(f'config for {_mi.name} not found') return None raise e diff --git a/portality/view/admin.py b/portality/view/admin.py index 9e39473b03..010907c3b1 100644 --- a/portality/view/admin.py +++ b/portality/view/admin.py @@ -68,7 +68,7 @@ def journals_list(): try: query = json.loads(request.values.get("q")) except: - app.logger.warn("Bad Request at admin/journals: " + str(request.values.get("q"))) + app.logger.warning("Bad Request at admin/journals: " + str(request.values.get("q"))) abort(400) # get the total number of journals to be affected @@ -89,7 +89,7 @@ def journals_list(): try: query = json.loads(request.data) except: - app.logger.warn("Bad Request at admin/journals: " + str(request.data)) + app.logger.warning("Bad Request at admin/journals: " + str(request.data)) abort(400) # get only the query part @@ -123,7 +123,7 @@ def articles_list(): try: query = json.loads(request.data) except: - app.logger.warn("Bad Request at admin/journals: " + str(request.data)) + app.logger.warning("Bad Request at admin/journals: " + str(request.data)) abort(400) # get only the query part From 2dce792b5b7b4f70fc296ae2791ec458b1812304 Mon Sep 17 00:00:00 2001 From: Dom Mitchell Date: Tue, 29 Aug 2023 16:17:01 +0200 Subject: [PATCH 32/56] Added 208-215 --- portality/templates/publisher/help.html | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/portality/templates/publisher/help.html b/portality/templates/publisher/help.html index ebd3d91571..660f379067 100644 --- a/portality/templates/publisher/help.html +++ b/portality/templates/publisher/help.html @@ -204,10 +204,16 @@

Failed XML uploads explained

A journal may have two ISSNs: an ISSN for the print version and an ISSN for the electronic version. Sometimes the ISSNs of the journal have changed.
The print and online ISSNs you have supplied are identical. If you supply 2 ISSNs they must be different: an ISSN for the print version and an ISSN for the electronic version. +
+ ISSNs provided don't match any journal. We do not have a record of one or both of those ISSNs in DOAJ. Check that all the Article ISSNs in the file are correct

+ Check that the journal to which you are trying to upload article metadata is indexed in DOAJ.
+
+ Check that the ISSNs in the metadata are both seen on the DOAJ journal record.
+
If you need to have the ISSNs of your DOAJ record updated, please contact us and we will check that the ISSNs are registered at the ISSN Portal and will then update the record accordingly.

If you believe all the ISSNs for the articles are correct, please contact us with the relevant details. From a3372afae3341cce524c21e8ab7457675ed03388 Mon Sep 17 00:00:00 2001 From: Dom Mitchell Date: Tue, 29 Aug 2023 16:18:43 +0200 Subject: [PATCH 33/56] Update help.html --- portality/templates/publisher/help.html | 1 + 1 file changed, 1 insertion(+) diff --git a/portality/templates/publisher/help.html b/portality/templates/publisher/help.html index 660f379067..0bda54892f 100644 --- a/portality/templates/publisher/help.html +++ b/portality/templates/publisher/help.html @@ -206,6 +206,7 @@

Failed XML uploads explained

The print and online ISSNs you have supplied are identical. If you supply 2 ISSNs they must be different: an ISSN for the print version and an ISSN for the electronic version.
ISSNs provided don't match any journal. We do not have a record of one or both of those ISSNs in DOAJ. +
Check that all the Article ISSNs in the file are correct
From 6072f1eca06e4d3a3ee613c8b4f5b9b92e626910 Mon Sep 17 00:00:00 2001 From: Dom Mitchell Date: Tue, 29 Aug 2023 16:20:52 +0200 Subject: [PATCH 34/56] Update help.html --- portality/templates/publisher/help.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/portality/templates/publisher/help.html b/portality/templates/publisher/help.html index 0bda54892f..9f4daddeb4 100644 --- a/portality/templates/publisher/help.html +++ b/portality/templates/publisher/help.html @@ -205,7 +205,7 @@

Failed XML uploads explained


The print and online ISSNs you have supplied are identical. If you supply 2 ISSNs they must be different: an ISSN for the print version and an ISSN for the electronic version.
- ISSNs provided don't match any journal. We do not have a record of one or both of those ISSNs in DOAJ. + ISSNs provided don't match any journal. We do not have a record of one or both of those ISSNs in DOAJ.

From 26551e3ba0c56609176e1925a6ebb8d2b5bf5abc Mon Sep 17 00:00:00 2001 From: Dom Mitchell Date: Wed, 30 Aug 2023 16:40:52 +0200 Subject: [PATCH 35/56] Changed J's title --- cms/data/team.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cms/data/team.yml b/cms/data/team.yml index ce175c0667..cc72fec212 100644 --- a/cms/data/team.yml +++ b/cms/data/team.yml @@ -33,7 +33,7 @@ - name: Dominic Mitchell role: Operations Manager photo: dominic.jpg - bio: 'Dominic has over 25 years experience working with publisher and library communities. He is responsible for operations and development of the DOAJ platform. He acts as Committee chair for the Think. Check. Submit. initiative, of which DOAJ is a founding organisation. He represents DOAJ in Project JASPER, a cross-industry project working to ensure that journals are preserved for the long term. He also sits on the OASPA Board of Directors and serves as Secretary. Outside of work, he is reluctantly becoming an expert in the playparks of Stockholm with his twin sons.' + bio: 'Dominic has over 25 years of experience working with publisher and library communities. He is responsible for operations and development of the DOAJ platform. He acts as Committee chair for the Think. Check. Submit. initiative, of which DOAJ is a founding organisation. He represents DOAJ in Project JASPER, a cross-industry project working to ensure that journals are preserved for the long term. He also sits on the OASPA Board of Directors and serves as Secretary. Outside of work, he is reluctantly becoming an expert in the playparks of Stockholm with his twin sons.' coi: 2016: https://drive.google.com/file/d/0ByRf6PVViI-mWmU0UHZqZm1xcDQ/view?usp=sharing&resourcekey=0-BmQKwWn6Vb9ot73Xie66aA 2018: https://drive.google.com/file/d/13XX_GUrw2xRmXARjRrTxegULPT8Redka/view?usp=sharing @@ -43,7 +43,7 @@ - name: Gala García Reátegui role: Managing Editor photo: gala.jpg - bio: 'Gala holds a Masters Degree in Information and Documentation from Lyon 3 University in France. Prior to joining DOAJ, she worked for the Digital Strategy and Data Directorate at The French National Research Agency (ANR) and for the Open Archive HAL at the Center for Direct Scientific Communication (CCSD). Gala is Peruvian but lived for more than ten years in France. Today, she is based in Denmark. She loves meeting people from other cultures, trying local dishes or experiences: currently Gala goes winter bathing in the Limfjord, Denmark! She also loves running.' + bio: 'Gala holds a Master's Degree in Information and Documentation from Lyon 3 University in France. Prior to joining DOAJ, she worked for the Digital Strategy and Data Directorate at The French National Research Agency (ANR) and for the Open Archive HAL at the Center for Direct Scientific Communication (CCSD). Gala is Peruvian but lived for more than ten years in France. Today, she is based in Denmark. She loves meeting people from other cultures, trying local dishes or experiences. Currently Gala goes winter bathing in the Limfjord, Denmark! She also loves running.' coi: 2023: https://drive.google.com/file/d/1R7XquFauefdmtjPIsfGfAWQoAw7NLIci/view?usp=sharing @@ -63,9 +63,9 @@ 2022: https://drive.google.com/file/d/1-3xzwkHMclREgLhj_XNF5n6Nr4q2_bnw/view?usp=sharing - name: Judith Barnsby - role: Senior Managing Editor + role: Head of Editorial photo: judith.jpg - bio: 'Judith has 25 years experience in the scholarly publishing industry, working for a range of non-profit society publishers and service providers before joining DOAJ. She has a keen interest in publishing standards and protocols, and has served on the board of CLOCKSS and as chair of the PALS (publisher and library solutions) working group in the UK. Judith loves books, especially detective fiction, and volunteers in her local library.' + bio: 'Judith has 25 years of experience in the scholarly publishing industry, working for a range of non-profit society publishers and service providers before joining DOAJ. She has a keen interest in publishing standards and protocols, and has served on the board of CLOCKSS and as chair of the PALS (publisher and library solutions) working group in the UK. Judith loves books, especially detective fiction, and volunteers in her local library.' coi: 2016: https://drive.google.com/file/d/0B0fPCpIPjZlmb3JmVkFYbjN5aTh1OUhLd2lZaEV0ZlFwbTZV/view?usp=sharing&resourcekey=0-o_PXKLk5UFbPk_-4B61jVA 2018: https://drive.google.com/file/d/0ByRf6PVViI-mV2lfMjByQjYxUkpMcXhuc2l5Q3ZDWlpiYUtZ/view?usp=sharing&resourcekey=0-6eiGIRal00eXvgJUTeN_lw From 6b5723513c07b690ff917336ee71dd292473f2b5 Mon Sep 17 00:00:00 2001 From: Dom Mitchell Date: Wed, 30 Aug 2023 16:49:15 +0200 Subject: [PATCH 36/56] Update team.yml --- cms/data/team.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cms/data/team.yml b/cms/data/team.yml index cc72fec212..7f04ca9b83 100644 --- a/cms/data/team.yml +++ b/cms/data/team.yml @@ -58,7 +58,7 @@ - name: Joanna Ball role: Managing Director photo: joba.jpg - bio: 'Joanna has had over 25 years experience of working within research libraries in the UK and Denmark, most recently as Head of Roskilde University Library, before joining DOAJ in 2022. She has also been involved with UKSG as Chair of Insights Editorial Board and a Trustee, and is currently Vice Chair. Joanna lives with her family in Roskilde and enjoys running in her spare time.' + bio: 'Joanna has over 25 years of experience working within research libraries in the UK and Denmark, most recently as Head of Roskilde University Library, before joining DOAJ in 2022. She has also been involved with UKSG as Chair of Insights Editorial Board and a Trustee, and is currently Vice Chair. Joanna lives with her family in Roskilde and enjoys running in her spare time.' coi: 2022: https://drive.google.com/file/d/1-3xzwkHMclREgLhj_XNF5n6Nr4q2_bnw/view?usp=sharing From 669c452abc72a0b2702c7abad7f7c974cd93ae9e Mon Sep 17 00:00:00 2001 From: Steve Eardley Date: Wed, 30 Aug 2023 16:49:41 +0100 Subject: [PATCH 37/56] use double quotes for bio in team --- cms/data/team.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cms/data/team.yml b/cms/data/team.yml index 7f04ca9b83..781c84737e 100644 --- a/cms/data/team.yml +++ b/cms/data/team.yml @@ -43,7 +43,7 @@ - name: Gala García Reátegui role: Managing Editor photo: gala.jpg - bio: 'Gala holds a Master's Degree in Information and Documentation from Lyon 3 University in France. Prior to joining DOAJ, she worked for the Digital Strategy and Data Directorate at The French National Research Agency (ANR) and for the Open Archive HAL at the Center for Direct Scientific Communication (CCSD). Gala is Peruvian but lived for more than ten years in France. Today, she is based in Denmark. She loves meeting people from other cultures, trying local dishes or experiences. Currently Gala goes winter bathing in the Limfjord, Denmark! She also loves running.' + bio: "Gala holds a Master's Degree in Information and Documentation from Lyon 3 University in France. Prior to joining DOAJ, she worked for the Digital Strategy and Data Directorate at The French National Research Agency (ANR) and for the Open Archive HAL at the Center for Direct Scientific Communication (CCSD). Gala is Peruvian but lived for more than ten years in France. Today, she is based in Denmark. She loves meeting people from other cultures, trying local dishes or experiences. Currently Gala goes winter bathing in the Limfjord, Denmark! She also loves running." coi: 2023: https://drive.google.com/file/d/1R7XquFauefdmtjPIsfGfAWQoAw7NLIci/view?usp=sharing @@ -85,7 +85,7 @@ - name: Katrine Sundsbø role: Community Manager photo: katrine.jpeg - bio: 'Katrine holds a Master’s degree in Cognitive Neuroscience, and has five years of experience in the field of scholarly communications. She has been an advocate for open access and visibility of research through various working groups, projects and through gamification of scholarly communications. Though Katrine is half Danish and half Norwegian, her son is named after a Swedish singer - and her British husband suggested the name!' + bio: "Katrine holds a Master’s degree in Cognitive Neuroscience, and has five years of experience in the field of scholarly communications. She has been an advocate for open access and visibility of research through various working groups, projects and through gamification of scholarly communications. Though Katrine is half Danish and half Norwegian, her son is named after a Swedish singer - and her British husband suggested the name!" coi: 2023: https://drive.google.com/file/d/1yqK-Znq62T_QR_JjtcpQl6W_Ian2Ti4F/view?usp=share_link From d35fcda29bcd51256f36edb1ede8f314a2507f24 Mon Sep 17 00:00:00 2001 From: Steve Eardley Date: Thu, 31 Aug 2023 17:07:47 +0100 Subject: [PATCH 38/56] Update OAI contact email https://github.com/DOAJ/doajPM/issues/3693 --- portality/settings.py | 2 ++ portality/view/oaipmh.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/portality/settings.py b/portality/settings.py index f3536a3e01..a02020f8ea 100644 --- a/portality/settings.py +++ b/portality/settings.py @@ -947,6 +947,8 @@ # OAI-PMH SETTINGS # ~~->OAIPMH:Feature~~ +OAI_ADMIN_EMAIL = 'helpdesk+oai@doaj.org' + # ~~->OAIAriticleXML:Crosswalk~~ # ~~->OAIJournalXML:Crosswalk~~ OAI_DC_METADATA_FORMAT = { diff --git a/portality/view/oaipmh.py b/portality/view/oaipmh.py index 5006c13f02..73a5158dfe 100644 --- a/portality/view/oaipmh.py +++ b/portality/view/oaipmh.py @@ -305,7 +305,7 @@ def get_record(dao, base_url, specified_oai_endpoint, identifier=None, metadata_ def identify(dao, base_url): repo_name = app.config.get("SERVICE_NAME") - admin_email = app.config.get("ADMIN_EMAIL") + admin_email = app.config.get("OAI_ADMIN_EMAIL", app.config.get("ADMIN_EMAIL")) idobj = Identify(base_url, repo_name, admin_email) idobj.earliest_datestamp = dao.earliest_datestamp() return idobj From a2a310f75d646fc4a6bcfc458ece0feaff1a5961 Mon Sep 17 00:00:00 2001 From: Steve Eardley Date: Thu, 31 Aug 2023 17:43:00 +0100 Subject: [PATCH 39/56] 404 when there's no PDD or when it's mis-requested --- portality/view/doaj.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/portality/view/doaj.py b/portality/view/doaj.py index 5ac2f67f69..1292728293 100644 --- a/portality/view/doaj.py +++ b/portality/view/doaj.py @@ -198,8 +198,13 @@ def public_data_dump_redirect(record_type): if not current_user.has_role(constants.ROLE_PUBLIC_DATA_DUMP): abort(404) - target_data = models.Cache.get_public_data_dump().get(record_type, {}) - if target_data is None: + # Make sure the PDD exists + pdd = models.Cache.get_public_data_dump() + if pdd is None: + abort(404) + + target_data = pdd.get(record_type, {}) + if not target_data: abort(404) main_store = store.StoreFactory.get(constants.STORE__SCOPE__PUBLIC_DATA_DUMP) From c41f895c522c70ea0a48a2eb920b0f13b13e2695 Mon Sep 17 00:00:00 2001 From: Steve Eardley Date: Tue, 5 Sep 2023 11:47:31 +0100 Subject: [PATCH 40/56] update OAI test --- doajtest/unit/test_oaipmh.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doajtest/unit/test_oaipmh.py b/doajtest/unit/test_oaipmh.py index b65d319bd0..bab8102499 100644 --- a/doajtest/unit/test_oaipmh.py +++ b/doajtest/unit/test_oaipmh.py @@ -245,7 +245,7 @@ def test_06_identify(self): records = t.xpath('/oai:OAI-PMH/oai:Identify', namespaces=self.oai_ns) assert len(records) == 1 assert records[0].xpath('//oai:repositoryName', namespaces=self.oai_ns)[0].text == 'Directory of Open Access Journals' - assert records[0].xpath('//oai:adminEmail', namespaces=self.oai_ns)[0].text == 'sysadmin@cottagelabs.com' + assert records[0].xpath('//oai:adminEmail', namespaces=self.oai_ns)[0].text == 'helpdesk+oai@doaj.org' assert records[0].xpath('//oai:granularity', namespaces=self.oai_ns)[0].text == 'YYYY-MM-DDThh:mm:ssZ' def test_07_bad_verb(self): From b5212dc9a6f0b97f67bbb73c581a3e91cea4f9a9 Mon Sep 17 00:00:00 2001 From: Aga Date: Tue, 5 Sep 2023 13:42:54 +0100 Subject: [PATCH 41/56] fix script output file --- ...230609_find_articles_with_invalid_issns.py | 20 +++++++++---------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/portality/scripts/230609_find_articles_with_invalid_issns.py b/portality/scripts/230609_find_articles_with_invalid_issns.py index c45b8c5d63..d9cf550a04 100644 --- a/portality/scripts/230609_find_articles_with_invalid_issns.py +++ b/portality/scripts/230609_find_articles_with_invalid_issns.py @@ -19,18 +19,16 @@ if __name__ == "__main__": - # import argparse - # - # parser = argparse.ArgumentParser() - # parser.add_argument("-o", "--out", help="output file path") - # args = parser.parse_args() - # - # if not args.out: - # print("Please specify an output file path with the -o option") - # parser.print_help() - # exit() + import argparse - out = "out.csv" + parser = argparse.ArgumentParser() + parser.add_argument("-o", "--out", help="output file path") + args = parser.parse_args() + + if not args.out: + print("Please specify an output file path with the -o option") + parser.print_help() + exit() with open(out, "w", encoding="utf-8") as f: writer = csv.writer(f) From 24dce28fc773a67b8e777e4187a70dc5a3dc9b33 Mon Sep 17 00:00:00 2001 From: Aga Date: Tue, 5 Sep 2023 13:51:39 +0100 Subject: [PATCH 42/56] change 'value copied' to 'copied' --- portality/static/js/formulaic.js | 2 +- portality/templates/application_form/_field.html | 2 +- portality/templates/application_form/_list.html | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/portality/static/js/formulaic.js b/portality/static/js/formulaic.js index 58ddf4f403..c970e12c18 100644 --- a/portality/static/js/formulaic.js +++ b/portality/static/js/formulaic.js @@ -1252,7 +1252,7 @@ var formulaic = { let value_to_copy = form.convertValueToText(value); navigator.clipboard.writeText(value_to_copy) var confirmation = $("#copy-confirmation--" + this.fieldDef.name); - confirmation.text("Value copied: " + value_to_copy); + confirmation.text("Copied: " + value_to_copy); confirmation.show().delay(3000).fadeOut(); }; this.init(); diff --git a/portality/templates/application_form/_field.html b/portality/templates/application_form/_field.html index 40be86ce55..2e6ce601a5 100644 --- a/portality/templates/application_form/_field.html +++ b/portality/templates/application_form/_field.html @@ -10,7 +10,7 @@ {% endif %} {% if f.has_widget("click_to_copy") %} Copy value - + {% endif %} {% if f.optional %}(Optional){% endif %} diff --git a/portality/templates/application_form/_list.html b/portality/templates/application_form/_list.html index 2f8fc00597..02567676cc 100644 --- a/portality/templates/application_form/_list.html +++ b/portality/templates/application_form/_list.html @@ -9,7 +9,7 @@ {% endif %} {% if f.has_widget("click_to_copy") %} Copy value - + {% endif %} {% if f.optional %}(Optional){% endif %} From b15c11448fca8018d6463994a747ceeb2e213357 Mon Sep 17 00:00:00 2001 From: Steve Eardley Date: Thu, 7 Sep 2023 10:55:33 +0100 Subject: [PATCH 43/56] Route application form stuff to new editor server --- deploy/nginx/doaj | 45 ++++++++++++++++++++++++++++++++++----------- production.cfg | 6 ++---- 2 files changed, 36 insertions(+), 15 deletions(-) diff --git a/deploy/nginx/doaj b/deploy/nginx/doaj index b52db3aea0..d906851ae1 100644 --- a/deploy/nginx/doaj +++ b/deploy/nginx/doaj @@ -36,17 +36,27 @@ map $http_user_agent $block_ua { ~*curl 1; } +# the public server (deprecated, use failover) upstream doaj_apps { - server 10.131.191.139:5050; + server 10.131.191.139:5050; #doaj-public-app-1 } + +# Background server runs async tasks upstream doaj_bg_apps { - #server 10.131.56.133:5050; #old bg machine - server 10.131.12.33:5050; + server 10.131.12.33:5050; #doaj-background-app-1 +} + +# Editor and admin site components +upstream doaj_ed_failover { + server 10.131.56.133:5050; #doaj-editor-app-1 + server 10.131.12.33:5050; #doaj-background-app-1 } + +# For public site components, try all servers upstream doaj_apps_failover { - server 10.131.191.139:5050; - #server 10.131.56.133:5050 backup; #old bg machine - server 10.131.12.33:5050 backup; + server 10.131.191.139:5050; #doaj-public-app-1 + server 10.131.12.33:5050 backup; #doaj-background-app-1 + server 10.131.56.133:5050; #doaj-editor-app-1 } upstream doaj_index { server 10.131.191.132:9200; @@ -144,9 +154,7 @@ server { proxy_buffering off; } - # for now we are going to send all login functions to the bg machine - # technically ONLY the routes that require file upload need to go to the bg machine - # but we think it is handy to separate them out, and later we could send them to other machines + # technically only the routes that require file upload need to go to the bg machine, but separate for consistency location /account { limit_req zone=general burst=10 nodelay; proxy_pass http://doaj_bg_apps; @@ -157,6 +165,19 @@ server { proxy_set_header X-Forwarded-Proto $scheme; proxy_buffering off; } + + # prefer the editor machine for application form work + location /admin/application { + limit_req zone=general burst=10 nodelay; + proxy_pass http://doaj_ed_failover; + proxy_redirect off; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_buffering off; + } + location /admin { # there are admin bulk actions that MUST go to bg machine limit_req zone=general burst=10 nodelay; proxy_pass http://doaj_bg_apps; @@ -167,9 +188,10 @@ server { proxy_set_header X-Forwarded-Proto $scheme; proxy_buffering off; } + location /editor { limit_req zone=general burst=10 nodelay; - proxy_pass http://doaj_bg_apps; + proxy_pass http://doaj_ed_failover; proxy_redirect off; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; @@ -177,9 +199,10 @@ server { proxy_set_header X-Forwarded-Proto $scheme; proxy_buffering off; } + location /journal/readonly { limit_req zone=general burst=10 nodelay; - proxy_pass http://doaj_bg_apps; + proxy_pass http://doaj_ed_failover; proxy_redirect off; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; diff --git a/production.cfg b/production.cfg index 897ff34004..d9d501f253 100644 --- a/production.cfg +++ b/production.cfg @@ -5,11 +5,9 @@ ELASTIC_SEARCH_HOST = "http://10.131.191.132:9200" # doaj-ind ELASTICSEARCH_HOSTS = [{'host': '10.131.191.132', 'port': 9200}, {'host': '10.131.191.133', 'port': 9200}] INDEX_PER_TYPE_SUBSTITUTE = '_doc' - # doaj-public-app-1 doaj-background-app-1 -APP_MACHINES_INTERNAL_IPS = ['10.131.191.139:5050', '10.131.12.33:5050'] + # doaj-public-app-1 doaj-background-app-1 doaj-editor-app-1 +APP_MACHINES_INTERNAL_IPS = ['10.131.191.139:5050', '10.131.12.33:5050', '10.131.56.133:5050'] - # doaj-public-app-1 doaj-bg-app-1 doaj-background-app-1 -#APP_MACHINES_INTERNAL_IPS = ['10.131.191.139:5050', '10.131.56.133:5050', '10.131.12.33:5050'] # The app is served via nginx / cloudlflare - they handle SSL SSL = False From d71425f01b30338607cc0d48f03e040255d740b8 Mon Sep 17 00:00:00 2001 From: Steve Eardley Date: Thu, 7 Sep 2023 11:04:07 +0100 Subject: [PATCH 44/56] Version bump for click to copy release --- portality/settings.py | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/portality/settings.py b/portality/settings.py index a02020f8ea..2bffdbab8a 100644 --- a/portality/settings.py +++ b/portality/settings.py @@ -9,7 +9,7 @@ # Application Version information # ~~->API:Feature~~ -DOAJ_VERSION = "6.3.15" +DOAJ_VERSION = "6.3.16" API_VERSION = "3.0.1" ###################################### diff --git a/setup.py b/setup.py index b97eee8d8b..a83bf6daf2 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setup( name='doaj', - version='6.3.15', + version='6.3.16', packages=find_packages(), install_requires=[ "awscli==1.20.50", From 99ac2bc1840eebe4b3e6d29300398230c7ab28e6 Mon Sep 17 00:00:00 2001 From: Steve Eardley Date: Thu, 7 Sep 2023 12:24:21 +0100 Subject: [PATCH 45/56] Specifically /application/ for editor server --- deploy/nginx/doaj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/deploy/nginx/doaj b/deploy/nginx/doaj index d906851ae1..f09aeb805b 100644 --- a/deploy/nginx/doaj +++ b/deploy/nginx/doaj @@ -166,8 +166,8 @@ server { proxy_buffering off; } - # prefer the editor machine for application form work - location /admin/application { + # prefer the editor machine for application form work (but application_quick_reject goes to background async) + location ~* /admin/application/ { limit_req zone=general burst=10 nodelay; proxy_pass http://doaj_ed_failover; proxy_redirect off; From 159abc8e2f685de031fedb93c6a96b8c79cf07e3 Mon Sep 17 00:00:00 2001 From: Steve Eardley Date: Sun, 10 Sep 2023 11:15:34 +0100 Subject: [PATCH 46/56] ensure editor server is classified as backup for apps failover --- deploy/nginx/doaj | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/deploy/nginx/doaj b/deploy/nginx/doaj index f09aeb805b..4e6c3b0576 100644 --- a/deploy/nginx/doaj +++ b/deploy/nginx/doaj @@ -49,14 +49,14 @@ upstream doaj_bg_apps { # Editor and admin site components upstream doaj_ed_failover { server 10.131.56.133:5050; #doaj-editor-app-1 - server 10.131.12.33:5050; #doaj-background-app-1 + server 10.131.12.33:5050 backup; #doaj-background-app-1 } # For public site components, try all servers upstream doaj_apps_failover { - server 10.131.191.139:5050; #doaj-public-app-1 - server 10.131.12.33:5050 backup; #doaj-background-app-1 - server 10.131.56.133:5050; #doaj-editor-app-1 + server 10.131.191.139:5050; #doaj-public-app-1 + server 10.131.12.33:5050 backup; #doaj-background-app-1 + server 10.131.56.133:5050 backup; #doaj-editor-app-1 } upstream doaj_index { server 10.131.191.132:9200; @@ -131,6 +131,7 @@ server { proxy_set_header X-Forwarded-Proto $scheme; proxy_buffering off; } + location /search { if ($block_ua) {return 403;} limit_req zone=general burst=10 nodelay; @@ -210,7 +211,8 @@ server { proxy_set_header X-Forwarded-Proto $scheme; proxy_buffering off; } - location /publisher { # only /publisher/uploadfile MUST go to bg, and /publisher/uploadFile + + location /publisher { # only /publisher/uploadfile MUST go to background limit_req zone=general burst=10 nodelay; proxy_pass http://doaj_bg_apps; proxy_redirect off; @@ -220,7 +222,8 @@ server { proxy_set_header X-Forwarded-Proto $scheme; proxy_buffering off; } - location /service { + + location /service { # performs locks etc - handle on the background server limit_req zone=general burst=10 nodelay; proxy_pass http://doaj_bg_apps; proxy_redirect off; @@ -244,6 +247,7 @@ server { proxy_set_header X-Forwarded-Proto $scheme; proxy_buffering off; } + location /csv { limit_req zone=general burst=10 nodelay; proxy_pass http://doaj_bg_apps; @@ -258,6 +262,7 @@ server { location =/robots.txt { alias /home/cloo/doaj/src/doaj/deploy/robots-production.txt; } + location /static/ { alias /home/cloo/doaj/src/doaj/portality/static/; autoindex off; From 810181e5d9d9ac8532bd368d9448809f4965819f Mon Sep 17 00:00:00 2001 From: Dom Mitchell Date: Mon, 11 Sep 2023 10:15:44 +0200 Subject: [PATCH 47/56] Removed section Commented out 263-302 --- portality/templates/includes/contribution_rates.html | 2 ++ 1 file changed, 2 insertions(+) diff --git a/portality/templates/includes/contribution_rates.html b/portality/templates/includes/contribution_rates.html index 29f28a8426..821fefede9 100644 --- a/portality/templates/includes/contribution_rates.html +++ b/portality/templates/includes/contribution_rates.html @@ -260,6 +260,7 @@

For institutions that are members of:

The definition as to Large/Small is at the discretion of the consortium.

+ From e7e0abb1b6a3fcba49a85892976f6554213f2d31 Mon Sep 17 00:00:00 2001 From: Steve Eardley Date: Mon, 11 Sep 2023 12:49:27 +0100 Subject: [PATCH 48/56] necessary fixes for articles script --- .../230609_find_articles_with_invalid_issns.py | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/portality/scripts/230609_find_articles_with_invalid_issns.py b/portality/scripts/230609_find_articles_with_invalid_issns.py index d9cf550a04..8b857faa01 100644 --- a/portality/scripts/230609_find_articles_with_invalid_issns.py +++ b/portality/scripts/230609_find_articles_with_invalid_issns.py @@ -1,16 +1,13 @@ from portality import models from portality.bll.services import article as articlesvc from portality.bll import exceptions -from portality.core import es_connection -from portality.util import ipt_prefix -import esprit import csv IN_DOAJ = { "query": { "bool": { "must": [ - {"term" : {"admin.in_doaj":True}} + {"term": {"admin.in_doaj": True}} ] } } @@ -22,15 +19,10 @@ import argparse parser = argparse.ArgumentParser() - parser.add_argument("-o", "--out", help="output file path") + parser.add_argument("-o", "--out", help="output file path", required=True) args = parser.parse_args() - if not args.out: - print("Please specify an output file path with the -o option") - parser.print_help() - exit() - - with open(out, "w", encoding="utf-8") as f: + with open(args.out, "w", encoding="utf-8") as f: writer = csv.writer(f) writer.writerow(["ID", "PISSN", "EISSN", "Journals found with article's PISSN", "In doaj?", "Journals found with article's EISSN", "In doaj?", "Error"]) @@ -61,4 +53,4 @@ j_e_in_doaj.append(jobj.is_in_doaj()) else: j_e_in_doaj.append("n/a") - writer.writerow([id, pissn, eissn, j_p, j_p_in_doaj, j_e, j_e_in_doaj, str(e)]) \ No newline at end of file + writer.writerow([id, pissn, eissn, j_p, j_p_in_doaj, j_e, j_e_in_doaj, str(e)]) From 17b4b336fe3a8a4908f8013431f48602e2877ca6 Mon Sep 17 00:00:00 2001 From: Dom Mitchell Date: Wed, 13 Sep 2023 08:59:37 +0200 Subject: [PATCH 49/56] Update team.yml Removed 2016 COIs --- cms/data/team.yml | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/cms/data/team.yml b/cms/data/team.yml index 781c84737e..fe599e2120 100644 --- a/cms/data/team.yml +++ b/cms/data/team.yml @@ -6,7 +6,6 @@ photo: alejandra.png bio: 'Alejandra has a Bachelor’s degree in Information Science and a Master’s degree in Digital Media. She has around ten years of experience in information management, knowledge management and scholarly communication at national and international level.' coi: - 2016: https://drive.google.com/file/d/0ByRf6PVViI-mVFhjdkZWaFJZOTQ/view?usp=sharing&resourcekey=0-gMdxxHXyyJB9zFZIuh99QQ 2018: https://drive.google.com/file/d/0ByRf6PVViI-mRlRYTDBPRlZiWTRxQ3VMTUZpQnZ5ZkwyLVQ4/view?usp=sharing&resourcekey=0-mlQ6rSCEnr6RfpCwh_4SMw 2020: https://drive.google.com/file/d/1PF7Cc9vGAwWGqpqDo7nRULjxWIF2NR_Q/view?usp=sharing 2022: https://drive.google.com/file/d/1E45ycyctDfYkh65ZCM8PMtQsYym_eP6L/view?usp=sharing @@ -16,7 +15,6 @@ photo: cenyu.jpg bio: 'Cenyu holds a PhD in Information Systems Science at Hanken School of Economics in Finland. She has spent around seven years on Open Access research with a particular focus on gold OA publishing concerning journals and publishers outside the mainstream. She was one of three DOAJ ambassadors for China from 2016 to 2017.' coi: - 2016: https://drive.google.com/file/d/0B0fPCpIPjZlmNHZCQmxpUmN6bUEtYUx2VHZnbjVySS1fRTlr/view?usp=sharing&resourcekey=0-1TRIV1MEQMhdGbmCd7CbOA 2020: https://drive.google.com/file/d/1rm9fjOF3OHJ9lR9wEUyQBQTO2KdoNQcE/view?usp=sharing 2022: https://drive.google.com/file/d/1Mn_CR0twKxyFbbHxsLSrgeU984BNOLlS/view?usp=sharing @@ -25,7 +23,6 @@ photo: clara.jpg bio: 'Clara has 10 years experience in the scholarly publishing industry. She worked at Cambridge University Press as an Open Access Project Manager until 2015. She also works in science communication as a freelancer at the University Pompeu Fabra, Barcelona. Clara speaks Spanish, Catalan, English and some German and French. She loves cetaceans, freediving, cycling, and is an enthusiastic cook.' coi: - 2016: https://drive.google.com/file/d/0ByRf6PVViI-mbDFybndLbldEbFE/view?usp=sharing&resourcekey=0-lKZNFwvUNdVAGKatvnKiPg 2018: https://drive.google.com/file/d/1LHmZSZ6bwf6U71fNvIibJa6R1lquNhfR/view?usp=sharing 2020: https://drive.google.com/file/d/1v4duxnoTcNo4UbL_GBa5D1T8JtTl7oY1/view?usp=sharing 2022: https://drive.google.com/file/d/1hevYxG1102llDy-_i-onwKbDuOlBguA_/view?usp=sharing @@ -35,7 +32,6 @@ photo: dominic.jpg bio: 'Dominic has over 25 years of experience working with publisher and library communities. He is responsible for operations and development of the DOAJ platform. He acts as Committee chair for the Think. Check. Submit. initiative, of which DOAJ is a founding organisation. He represents DOAJ in Project JASPER, a cross-industry project working to ensure that journals are preserved for the long term. He also sits on the OASPA Board of Directors and serves as Secretary. Outside of work, he is reluctantly becoming an expert in the playparks of Stockholm with his twin sons.' coi: - 2016: https://drive.google.com/file/d/0ByRf6PVViI-mWmU0UHZqZm1xcDQ/view?usp=sharing&resourcekey=0-BmQKwWn6Vb9ot73Xie66aA 2018: https://drive.google.com/file/d/13XX_GUrw2xRmXARjRrTxegULPT8Redka/view?usp=sharing 2020: https://drive.google.com/file/d/1nxFOuAdXLb8A-LulhNpz9i5vSmr5DBwF/view?usp=sharing 2022: https://drive.google.com/file/d/1HBF9RLaIt3lFNG6WDcV08fQSMS_C6zwA/view?usp=sharing @@ -67,7 +63,6 @@ photo: judith.jpg bio: 'Judith has 25 years of experience in the scholarly publishing industry, working for a range of non-profit society publishers and service providers before joining DOAJ. She has a keen interest in publishing standards and protocols, and has served on the board of CLOCKSS and as chair of the PALS (publisher and library solutions) working group in the UK. Judith loves books, especially detective fiction, and volunteers in her local library.' coi: - 2016: https://drive.google.com/file/d/0B0fPCpIPjZlmb3JmVkFYbjN5aTh1OUhLd2lZaEV0ZlFwbTZV/view?usp=sharing&resourcekey=0-o_PXKLk5UFbPk_-4B61jVA 2018: https://drive.google.com/file/d/0ByRf6PVViI-mV2lfMjByQjYxUkpMcXhuc2l5Q3ZDWlpiYUtZ/view?usp=sharing&resourcekey=0-6eiGIRal00eXvgJUTeN_lw 2020: https://drive.google.com/file/d/18MWTsze4cDQQRPHJl2XrYgHQvlxhsPZa/view?usp=sharing 2023: https://drive.google.com/file/d/1hUsVIY09N6WceSx1edTM-h516CJGkHcu/view?usp=share_link @@ -77,7 +72,6 @@ photo: Kamel.jpg bio: 'Kamel is Full Professor of Chemistry at the University of Bejaia, Algeria (ORCID). He gained his PhD in Process Engineering and Chemistry of Materials Science at the University of Setif, Algeria. Kamel joined DOAJ in 2016 as an Ambassador for North Africa. He is currently Creative Commons Algeria Chapter lead, director of the Laboratory of Organic Materials at the University of Bejaia and editor-in-chief of Algerian Journal of Natural Products. His scientific activity is focused on chemistry of Natural Products, scholarly communications and new developments in academic publishing. Father of 3 daughters, he likes travelling, healthy local foods & home-made snacks.' coi: - 2016: https://drive.google.com/file/d/0B0fPCpIPjZlmVEN4X1Q0RDdCams1NXhveW1HQmtMYU56bDE4/view?usp=sharing&resourcekey=0-wA1CGAbjB6FAX33gCDQmrA 2018: https://drive.google.com/file/d/1JdF2kh-fLXz8kPGN_3ijDt5y9K6s0hOQ/view?usp=sharing 2020: https://drive.google.com/file/d/1iXrjwLTNBXwKD2TwrPD9ApKL7O6uZ8Z7/view?usp=sharing 2022: https://drive.google.com/file/d/1cl18h_mYnNogYs8Rk-fhBTW6WKOTC2IF/view?usp=sharing @@ -94,7 +88,6 @@ photo: lars.jpg bio: 'Lars worked at Danish university libraries for two decades and was Director of Libraries at Lund University, Sweden from 2001 to 2011. He founded the DOAJ in 2003, and was Managing Director from 2013-2021. He has vast experience in change management, re-engineering of academic libraries, and development of information services for research & higher education. For two decades Lars has been a strong advocate of open access and for providing services to the open access movement. He is co-founder of OpenDOAR, the Directory of Open Access Books and Think. Check. Submit. Lars lives outside Copenhagen, and is married with 4 children and 4 grandchildren. He enjoys vegetable gardening, growing cacti and succulents, and playing internet chess.' coi: - 2016: https://drive.google.com/file/d/0ByRf6PVViI-mbmo2aU9NWkx5dGs/view?usp=sharing&resourcekey=0-mpdRgVU9UlFjC614-woDvg 2018: https://drive.google.com/file/d/1mm1a8nbY5MQX9loqIs2ZQuVN-73RfPuN/view?usp=sharing 2021: https://drive.google.com/file/d/1bNj5sqUsu4sRLmm_YOuh3JCSMERzQ1Ro/view?usp=sharing 2022: https://drive.google.com/file/d/1fRJtvci2_j4vad0C5N1pfqm2sHZQkFz3/view?usp=sharing @@ -104,7 +97,6 @@ photo: leena.jpg bio: "Leena joined the DOAJ team in 2016 as an Ambassador for India before becoming a Managing Editor. Prior to joining DOAJ she worked as a science librarian at Nanyang Technological University, Singapore, where she developed a keen interest in scholarly communication & open science. A recent addition to her interests is artificial intelligence in scholarly communication. Leena holds a Master’s degree in Information Studies and lives in Singapore. She loves watching sci-fi shows and is enthusiastic about travelling to new places." coi: - 2016: https://drive.google.com/file/d/0B0fPCpIPjZlmTHZuaEtMSDNIeUpKT2Fid19jVjVFTkRoUmdj/view?usp=sharing&resourcekey=0-KqvRVa30bQEUfqO-YA1L-g 2018: https://drive.google.com/file/d/1tifEjAIlU3txBw9DjIcRW9cZL7YG7_nU/view?usp=sharing 2020: https://drive.google.com/file/d/1zU-lLB5W54E_QUm5uto5tqB6cZl83TAJ/view?usp=sharing 2022: https://drive.google.com/file/d/19rw-naMJqHkI5T7aDIDPUkwPutBdDpDm/view?usp=sharing @@ -151,7 +143,6 @@ photo: Rikard.jpg bio: 'Rikard has a Bachelor of Arts degree with a Major in Cultural Sciences and a specialization in publishing. He enjoys reading about philosophy and religion.' coi: - 2016: https://drive.google.com/file/d/0ByRf6PVViI-mdnJPdldOM0hUMFU/view?usp=sharing&resourcekey=0-8dJAtvm2n7vXV9NhqZYckw 2018: https://drive.google.com/file/d/1tOnW8L6TwolyLpIXwMKTITf9wGh_ukLb/view?usp=sharing 2020: https://drive.google.com/file/d/14c0RgpyD2Slzyh5s8LGvj5OwWbL4H8NX/view?usp=sharing 2023: https://drive.google.com/file/d/1HQIh1DlfhEutTWniXDGLYFVa9VxJ4OT9/view?usp=share_link @@ -161,7 +152,6 @@ photo: sonja.jpg bio: 'Sonja is a former Information Librarian from Lund University Library. She has a B.A. in English, Bulgarian and Russian from Lund University and specialises in applications for journals in the Slavonic languages.' coi: - 2016: https://drive.google.com/file/d/0ByRf6PVViI-mNUFoZWV4YnZ3bDg/view?usp=sharing&resourcekey=0-1JRid_DHRMKbgdzmVYL7NQ 2018: https://drive.google.com/file/d/1M5AGEDP79uk2olCcmVYjKCsmzL7tG2Vc/view?usp=sharing 2020: https://drive.google.com/file/d/1-4RJYScTs_zMBeD5zESNvCoIBCWTOWHR/view?usp=sharing 2022: https://drive.google.com/file/d/1soZtiW6gyVJPl7P_J60j2TL2Fqzl0QAs/view?usp=sharing @@ -178,7 +168,6 @@ photo: tom.jpg bio: 'Tom has a PhD in molecular microbiology and spent several years in Africa doing research on malaria, sleeping sickness and meningococcal epidemics. He has been actively advocating open access and open science since 2012 when he joined the Open Knowledge community and became a member of the DOAJ advisory board. His current research interests are development of quality systems for the assessment of scholarly journals and articles, and research in the area of soil microbiology in relation to soil health and human health.' coi: - 2016: https://drive.google.com/file/d/0ByRf6PVViI-mYUFZNDRISTZodUU/view?usp=sharing&resourcekey=0-g13FJaUJpdR_t2rMLEyzEQ 2018: https://drive.google.com/file/d/1x0w-a1TWQdJDKPtQpGhmDZSdA4BhFSpI/view?usp=sharing 2020: https://drive.google.com/file/d/1VyirUdc6FBNOujl938bHf1JCL1jLNwXV/view?usp=sharing 2022: https://drive.google.com/file/d/1ww7WHQEg1395bPn20Arb7LJn9lIROdBl/view?usp=sharing From 8c7e20574a8d04cedacd1d075bf20a97ed8f85fc Mon Sep 17 00:00:00 2001 From: Steve Eardley Date: Wed, 13 Sep 2023 13:45:22 +0100 Subject: [PATCH 50/56] Version bump for static pages --- portality/settings.py | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/portality/settings.py b/portality/settings.py index 2bffdbab8a..f2bdd5479f 100644 --- a/portality/settings.py +++ b/portality/settings.py @@ -9,7 +9,7 @@ # Application Version information # ~~->API:Feature~~ -DOAJ_VERSION = "6.3.16" +DOAJ_VERSION = "6.3.17" API_VERSION = "3.0.1" ###################################### diff --git a/setup.py b/setup.py index a83bf6daf2..fbcac71bc0 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setup( name='doaj', - version='6.3.16', + version='6.3.17', packages=find_packages(), install_requires=[ "awscli==1.20.50", From 63270b2a9782b3e240636f45eaf023e4bb9a5df4 Mon Sep 17 00:00:00 2001 From: Steve Eardley Date: Fri, 15 Sep 2023 14:55:26 +0100 Subject: [PATCH 51/56] Updates to backups alert script --- deploy/lambda/alert_backups_missing.py | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/deploy/lambda/alert_backups_missing.py b/deploy/lambda/alert_backups_missing.py index 38a9edbc2e..566a361b9a 100644 --- a/deploy/lambda/alert_backups_missing.py +++ b/deploy/lambda/alert_backups_missing.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -""" Steven Eardley 2020-02-07 for DOAJ - uploaded manually (todo: we should upload this in the release script) """ +""" Steven Eardley 2023-09-15 for DOAJ - uploaded manually (todo: we should upload this in the release script) """ # ~~BackupsMissing:Monitoring->Lambda:Technology~~ @@ -8,23 +8,25 @@ import json from datetime import datetime, timezone, timedelta -from portality.lib.dates import FMT_DATETIME_STD - s3 = boto3.client('s3') # Check the doaj elasticsearch snapshot bucket has been updated today (should happen daily at 0600 via background job) -buckets = ['doaj-index-backups'] +buckets = ['doaj-index-ipt-backups'] + # Check the doaj-nginx logs bucket has been updated today (should happen daily at 0630 via cron logrotate) -buckets += ['doaj-nginx-logs'] +# buckets += ['doaj-nginx-logs'] def lambda_handler(event, context): """ The main function executed by Lambda""" + start = datetime.utcnow() summary = {'success': [], 'fail': []} for b in buckets: + print('Checking bucket {0} was updated today'.format(b)) + # First check the bucket actually exists try: s3.head_bucket(Bucket=b) @@ -32,11 +34,13 @@ def lambda_handler(event, context): error_code = int(e.response['Error']['Code']) if error_code == 404: send_alert_email(b, last_mod=None) + raise # Then check the expected entry exists in the bucket's objects. files = list_bucket_keys(bucket_name=b) old_to_new = sorted(files, key=lambda f: f['LastModified']) newest = old_to_new[-1] + print('Latest backup is', newest) # If the newest file is older than 1 day old, our backups are not up to date. if datetime.now(timezone.utc) - newest['LastModified'] > timedelta(days=1): @@ -47,6 +51,8 @@ def lambda_handler(event, context): summary['success'].append(b) print(summary) # For the CloudWatch logs + print('Completed in', str(datetime.utcnow() - start)) + return str(summary) @@ -86,8 +92,8 @@ def send_alert_email(bucket, last_mod): msg = 'AWS backup error: bucket {b} is missing.'.format(b=bucket) else: msg = 'AWS backup error: bucket {b} has not been updated today - it was last modified on {t}.' \ - '\nYou may wish to check the corresponding logs.'.format(b=bucket, - t=last_mod.strftime(FMT_DATETIME_STD)) + '\nYou may wish to check the corresponding logs.'.format(b=bucket, t=last_mod.strftime( + '%Y-%m-%dT%H:%M:%SZ')) r = botocore.vendored.requests.post('https://api.mailgun.net/v3/doaj.org/messages', auth=('api', credentials.get('ERROR_MAIL_API_KEY', '')), From 55799fdb136461a0bf54ea5b5250da962891bbdd Mon Sep 17 00:00:00 2001 From: Steve Eardley Date: Mon, 18 Sep 2023 17:12:07 +0100 Subject: [PATCH 52/56] Add URL to plausible logging --- portality/lib/plausible.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/portality/lib/plausible.py b/portality/lib/plausible.py index 2aa602d986..44f844ba4f 100644 --- a/portality/lib/plausible.py +++ b/portality/lib/plausible.py @@ -62,7 +62,7 @@ def send_event(goal: str, on_completed=None, **props_kwargs): def _send(): resp = requests.post(plausible_api_url, json=payload, headers=headers) if resp.status_code >= 300: - logger.warning(f'send plausible event api fail. [{resp.status_code}][{resp.text}]') + logger.warning(f'Send plausible event API fail: [{resp.url}][{resp.status_code}][{resp.text}]') if on_completed: on_completed(resp) From 097933b6b2b01b0afb5bbbc74065148ec237ba58 Mon Sep 17 00:00:00 2001 From: Steve Eardley Date: Mon, 18 Sep 2023 17:37:14 +0100 Subject: [PATCH 53/56] Add headers and payload to plausible logging --- portality/lib/plausible.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/portality/lib/plausible.py b/portality/lib/plausible.py index 44f844ba4f..90b1b8f46b 100644 --- a/portality/lib/plausible.py +++ b/portality/lib/plausible.py @@ -62,7 +62,7 @@ def send_event(goal: str, on_completed=None, **props_kwargs): def _send(): resp = requests.post(plausible_api_url, json=payload, headers=headers) if resp.status_code >= 300: - logger.warning(f'Send plausible event API fail: [{resp.url}][{resp.status_code}][{resp.text}]') + logger.warning(f'Send plausible event API fail. snd: [{resp.url}] [{headers}] [{payload}] rcv: [{resp.status_code}] [{resp.text}]') if on_completed: on_completed(resp) From c8e6e919285ddd5ff4414f020e6322d3e83d066c Mon Sep 17 00:00:00 2001 From: Steve Eardley Date: Thu, 21 Sep 2023 11:32:58 +0100 Subject: [PATCH 54/56] Validate supplied owner in csv ingest --- portality/forms/application_processors.py | 10 +++++----- portality/scripts/journals_update_via_csv.py | 11 ++++++++++- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/portality/forms/application_processors.py b/portality/forms/application_processors.py index 13a294d14d..1cd426c1f6 100644 --- a/portality/forms/application_processors.py +++ b/portality/forms/application_processors.py @@ -198,8 +198,11 @@ def _patch_target_note_id(self): for note in self.target.notes: note_date = dates.parse(note['date']) if not note.get('author_id') and note_date > dates.before_now(60): - note['author_id'] = current_user.id - + try: + note['author_id'] = current_user.id + except AttributeError: + # Skip if we don't have a current_user + pass class NewApplication(ApplicationProcessor): @@ -307,7 +310,6 @@ def patch_target(self): if (self.target.owner is None or self.target.owner == "") and (self.source.owner is not None): self.target.set_owner(self.source.owner) - def finalise(self, account, save_target=True, email_alert=True): """ account is the administrator account carrying out the action @@ -326,7 +328,6 @@ def finalise(self, account, save_target=True, email_alert=True): elif not j.is_in_doaj(): raise Exception(Messages.EXCEPTION_EDITING_WITHDRAWN_JOURNAL) - # if we are allowed to finalise, kick this up to the superclass super(AdminApplication, self).finalise() @@ -813,7 +814,6 @@ def patch_target(self): if (self.target.owner is None or self.target.owner == "") and (self.source.owner is not None): self.target.set_owner(self.source.owner) - def finalise(self): # FIXME: this first one, we ought to deal with outside the form context, but for the time being this # can be carried over from the old implementation diff --git a/portality/scripts/journals_update_via_csv.py b/portality/scripts/journals_update_via_csv.py index 298b7c817b..c696068a85 100644 --- a/portality/scripts/journals_update_via_csv.py +++ b/portality/scripts/journals_update_via_csv.py @@ -82,6 +82,7 @@ reader = csv.DictReader(g, fieldnames=header_row) # verify header row with current CSV headers, report errors + # TODO: Include 'Owner' field - but we should probably base this process off the AdminCSV too. expected_headers = JournalFixtureFactory.csv_headers() # Always perform a match check on supplied headers, not counting order @@ -155,6 +156,14 @@ if len(updates) > 0: [print(upd) for upd in updates] + # Check we have the expected owner (if supplied) before proceeding to create an update request + own = row.get('Owner') + if own is not None: + if own.strip().lower() != j.owner.strip().lower(): + print('ABORTING - supplied owner {0} mismatches journal owner {1}.'.format(own, j.owner)) + writer.writerow([j.id, ' | '.join(updates), 'COULD NOT UPDATE - Owner mismatch. Expected {0} Got {1}'.format(own, j.owner)]) + continue + # Create an update request for this journal update_req = None jlock = None @@ -204,7 +213,7 @@ # Add note to UR if supplied if note: - fc.target.add_note(note) + fc.target.add_note(note, author_id=sys_acc.id) if not args.manual_review: # This is the update request, in 'update request' state From 0b197f8fc86b3e13d86ddfd9f5970f19ed1478f2 Mon Sep 17 00:00:00 2001 From: Steve Eardley Date: Thu, 21 Sep 2023 14:51:44 +0100 Subject: [PATCH 55/56] Reduce frequency of journalcsv to every 2 hours --- portality/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/portality/settings.py b/portality/settings.py index f2bdd5479f..7301701e2b 100644 --- a/portality/settings.py +++ b/portality/settings.py @@ -427,7 +427,7 @@ HUEY_SCHEDULE = { "sitemap": {"month": "*", "day": "*", "day_of_week": "*", "hour": "8", "minute": "0"}, "reporting": {"month": "*", "day": "1", "day_of_week": "*", "hour": "0", "minute": "0"}, - "journal_csv": {"month": "*", "day": "*", "day_of_week": "*", "hour": "*", "minute": "35"}, + "journal_csv": {"month": "*", "day": "*", "day_of_week": "*", "hour": "*/2", "minute": "20"}, "read_news": {"month": "*", "day": "*", "day_of_week": "*", "hour": "*", "minute": "30"}, "article_cleanup_sync": {"month": "*", "day": "2", "day_of_week": "*", "hour": "0", "minute": "0"}, "async_workflow_notifications": {"month": "*", "day": "*", "day_of_week": "1", "hour": "5", "minute": "0"}, From e5cdcbdf4b97cc14a9eea95e342425cca0e8bfb9 Mon Sep 17 00:00:00 2001 From: Steven Eardley Date: Sun, 24 Sep 2023 18:51:21 +0100 Subject: [PATCH 56/56] Reduce gunicorn worker count --- deploy/doaj_gunicorn_config.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/deploy/doaj_gunicorn_config.py b/deploy/doaj_gunicorn_config.py index f9425de5e5..a08dd6ef62 100644 --- a/deploy/doaj_gunicorn_config.py +++ b/deploy/doaj_gunicorn_config.py @@ -1,7 +1,7 @@ import multiprocessing bind = "0.0.0.0:5050" -workers = multiprocessing.cpu_count() * 8 + 1 +workers = multiprocessing.cpu_count() * 6 + 1 proc_name = 'doaj' max_requests = 1000 @@ -13,4 +13,4 @@ max_requests_jitter = 100 timeout = 40 -graceful_timeout = 40 \ No newline at end of file +graceful_timeout = 40