diff --git a/arches/app/datatypes/datatypes.py b/arches/app/datatypes/datatypes.py index eadbe2cbab5..eb8d4d5d121 100644 --- a/arches/app/datatypes/datatypes.py +++ b/arches/app/datatypes/datatypes.py @@ -1651,8 +1651,8 @@ def transform_value_for_tile(self, value, **kwargs): Accepts a comma delimited string of file paths as 'value' to create a file datatype value with corresponding file record in the files table for each path. Only the basename of each path is used, so the accuracy of the full path is not important. However the name of each file must match the name of a file in - the directory from which Arches will request files. By default, this is the 'uploadedfiles' directory - in a project. + the directory from which Arches will request files. By default, this is the directory in a project as defined + in settings.UPLOADED_FILES_DIR. """ @@ -1671,7 +1671,7 @@ def transform_value_for_tile(self, value, **kwargs): tile_file["name"] = os.path.basename(file_path) tile_file["type"] = mime.guess_type(file_path)[0] tile_file["type"] = "" if tile_file["type"] is None else tile_file["type"] - file_path = "uploadedfiles/" + str(tile_file["name"]) + file_path = "%s/%s" % (settings.UPLOADED_FILES_DIR, str(tile_file["name"])) tile_file["file_id"] = str(uuid.uuid4()) if source_path: source_file = os.path.join(source_path, tile_file["name"]) @@ -1679,7 +1679,7 @@ def transform_value_for_tile(self, value, **kwargs): try: with default_storage.open(source_file) as f: current_file, created = models.File.objects.get_or_create(fileid=tile_file["file_id"]) - filename = fs.save(os.path.join("uploadedfiles", os.path.basename(f.name)), File(f)) + filename = fs.save(os.path.join(settings.UPLOADED_FILES_DIR, os.path.basename(f.name)), File(f)) current_file.path = os.path.join(filename) current_file.save() tile_file["size"] = current_file.path.size @@ -1705,7 +1705,7 @@ def pre_tile_save(self, tile, nodeid): if file["file_id"]: if file["url"] == f'{settings.MEDIA_URL}{file["file_id"]}': val = uuid.UUID(file["file_id"]) # to test if file_id is uuid - file_path = "uploadedfiles/" + file["name"] + file_path = "%s/%s" % (settings.UPLOADED_FILES_DIR, file["name"]) try: file_model = models.File.objects.get(pk=file["file_id"]) except ObjectDoesNotExist: @@ -1723,7 +1723,7 @@ def pre_tile_save(self, tile, nodeid): logger.warning(_("This file's fileid is not a valid UUID")) def transform_export_values(self, value, *args, **kwargs): - return ",".join([settings.MEDIA_URL + "uploadedfiles/" + str(file["name"]) for file in value]) + return ",".join([settings.MEDIA_URL + settings.UPLOADED_FILES_DIR + "/" + str(file["name"]) for file in value]) def is_a_literal_in_rdf(self): return False diff --git a/arches/app/etl_modules/base_data_editor.py b/arches/app/etl_modules/base_data_editor.py index a05498c40a3..54018b28a0a 100644 --- a/arches/app/etl_modules/base_data_editor.py +++ b/arches/app/etl_modules/base_data_editor.py @@ -8,8 +8,11 @@ from django.utils.decorators import method_decorator from django.utils.translation import gettext as _ from arches.app.datatypes.datatypes import DataTypeFactory -from arches.app.models.models import GraphModel, Node +from arches.app.models.models import GraphModel, Node, ETLModule from arches.app.models.system_settings import settings +from arches.app.search.elasticsearch_dsl_builder import Bool, Exists, FiltersAgg, Nested, NestedAgg, Query, Wildcard +from arches.app.search.mappings import RESOURCES_INDEX +from arches.app.search.search_engine_factory import SearchEngineFactory import arches.app.tasks as tasks from arches.app.etl_modules.decorators import load_data_async from arches.app.etl_modules.save import save_to_tiles @@ -114,12 +117,13 @@ def create_load_event(self, cursor, load_details): return result - def stage_data(self, cursor, graph_id, node_id, resourceids, operation, text_replacing, language_code, case_insensitive): + def stage_data(self, cursor, module_id, graph_id, node_id, resourceids, operation, text_replacing, language_code, case_insensitive): result = {"success": False} + update_limit = ETLModule.objects.get(pk=module_id).config["updateLimit"] try: cursor.execute( - """SELECT * FROM __arches_stage_string_data_for_bulk_edit(%s, %s, %s, %s, %s, %s, %s, %s, %s)""", - (self.loadid, graph_id, node_id, self.moduleid, (resourceids), operation, text_replacing, language_code, case_insensitive), + """SELECT * FROM __arches_stage_string_data_for_bulk_edit(%s, %s, %s, %s, %s, %s, %s, %s, %s, %s)""", + (self.loadid, graph_id, node_id, self.moduleid, (resourceids), operation, text_replacing, language_code, case_insensitive, update_limit), ) result["success"] = True except Exception as e: @@ -164,73 +168,90 @@ def edit_staged_data(self, cursor, graph_id, node_id, operation, language_code, result["message"] = _("Unable to edit staged data: {}").format(str(e)) return result - def get_preview_data(self, graph_id, node_id, resourceids, language_code, old_text, case_insensitive): - node_id_query = " AND nodeid = %(node_id)s" if node_id else "" - graph_id_query = " AND graphid = %(graph_id)s" if graph_id else "" - resourceids_query = " AND resourceinstanceid IN %(resourceids)s" if resourceids else "" - like_operator = "ilike" if case_insensitive == "true" else "like" - old_text_like = "%" + old_text + "%" if old_text else "" - text_query = ( - " AND t.tiledata -> %(node_id)s -> %(language_code)s ->> 'value' " + like_operator + " %(old_text)s" if old_text else "" - ) + def get_preview_data(self, node_id, search_url, language_code, operation, old_text, case_insensitive): + request = HttpRequest() + request.user = self.request.user + request.method = "GET" + request.GET["paging-filter"] = 1 + request.GET["tiles"] = True + if language_code is None: language_code = "en" - request_parmas_dict = { - "node_id": node_id, - "language_code": language_code, - "graph_id": graph_id, - "resourceids": resourceids, - "old_text": old_text_like, + if search_url: + params = parse_qs(urlsplit(search_url).query) + for k, v in params.items(): + request.GET.__setitem__(k, v[0]) + + search_url_query = search_results(request, returnDsl=True).dsl["query"] + case_insensitive = True if case_insensitive == "true" else False + + if old_text: + search_query = Wildcard( + field=f"tiles.data.{node_id}.{language_code}.value.keyword", + query=f"*{old_text}*", + case_insensitive=case_insensitive, + ) + search_bool_agg = Bool() + search_bool_agg.must(search_query) + + else: + if operation.startswith("upper"): + regexp = "(.*[a-z].*)" + elif operation.startswith("lower"): + regexp = "(.*[A-Z].*)" + elif operation.startswith("capitalize"): + regexp = "([a-z].*)|([A-Z][a-zA-Z]*[A-Z].*)|((.+[ ]+)[a-z].*)|((.+[ ]+)[A-Z][a-zA-Z]*[A-Z].*)" + elif operation.startswith("trim"): + regexp = "[ \t].*|.*[ \t]" + case_search_query = { + "regexp": { + f"tiles.data.{str(node_id)}.{language_code}.value.keyword": { + "value": regexp + } + } + } + search_query = Bool() + search_query.must(case_search_query) + search_bool_agg = Bool() + search_bool_agg.must(case_search_query) + + string_search_nested = Nested(path="tiles", query=search_query) + inner_hits_query = { + "inner_hits": { + "_source": False, + "docvalue_fields": [ f"tiles.data.{node_id}.{language_code}.value.keyword" ] + } } + string_search_nested.dsl["nested"].update(inner_hits_query) - sql_query = ( - """ - SELECT t.tiledata -> %(node_id)s -> %(language_code)s ->> 'value' FROM tiles t, nodes n - WHERE t.nodegroupid = n.nodegroupid - """ - + node_id_query - + graph_id_query - + resourceids_query - + text_query - + " LIMIT 5;" - ) + search_bool_query = Bool() + search_bool_query.must(string_search_nested) - tile_count_query = ( - """ - SELECT count(t.tileid) FROM tiles t, nodes n - WHERE t.nodegroupid = n.nodegroupid - """ - + node_id_query - + graph_id_query - + resourceids_query - + text_query - ) + search_url_query["bool"]["must"].append(search_bool_query.dsl) - resource_count_query = ( - """ - SELECT count(DISTINCT t.resourceinstanceid) FROM tiles t, nodes n - WHERE t.nodegroupid = n.nodegroupid - """ - + node_id_query - + graph_id_query - + resourceids_query - + text_query - ) + search_filter_agg = FiltersAgg(name="string_search") + search_filter_agg.add_filter(search_bool_agg) - with connection.cursor() as cursor: - cursor.execute(sql_query, request_parmas_dict) - row = [value[0] for value in cursor.fetchall()] + nested_agg = NestedAgg(path="tiles", name="tile_agg") + nested_agg.add_aggregation(search_filter_agg) + + se = SearchEngineFactory().create() + query = Query(se, limit=5) - cursor.execute(tile_count_query, request_parmas_dict) - count = cursor.fetchall() - (number_of_tiles,) = count[0] + query.add_query(search_url_query) + query.add_aggregation(nested_agg) - cursor.execute(resource_count_query, request_parmas_dict) - count = cursor.fetchall() - (number_of_resources,) = count[0] + results = query.search(index=RESOURCES_INDEX) + values = [] + for hit in results['hits']['hits']: + for tile in hit['inner_hits']['tiles']['hits']['hits']: + values.append(tile['fields'][f"tiles.data.{node_id}.{language_code}.value.keyword"][0]) - return row, number_of_tiles, number_of_resources + number_of_resources = results['hits']['total']['value'] + number_of_tiles = results["aggregations"]["tile_agg"]["string_search"]["buckets"][0]["doc_count"] + + return values[:5], number_of_tiles, number_of_resources def preview(self, request): graph_id = request.POST.get("graph_id", None) @@ -251,13 +272,13 @@ def preview(self, request): if resourceids: resourceids = tuple(resourceids) - if case_insensitive == "true" and operation == "replace": + if case_insensitive and operation == "replace": operation = "replace_i" if also_trim == "true": operation = operation + "_trim" first_five_values, number_of_tiles, number_of_resources = self.get_preview_data( - graph_id, node_id, resourceids, language_code, old_text, case_insensitive + node_id, search_url, language_code, operation, old_text, case_insensitive ) return_list = [] with connection.cursor() as cursor: @@ -320,7 +341,7 @@ def write(self, request): } first_five_values, number_of_tiles, number_of_resources = self.get_preview_data( - graph_id, node_id, resourceids, language_code, old_text, case_insensitive + node_id, search_url, language_code, operation, old_text, case_insensitive ) load_details = { @@ -340,7 +361,7 @@ def write(self, request): if use_celery_bulk_edit: response = self.run_load_task_async(request, self.loadid) else: - response = self.run_load_task(self.loadid, graph_id, node_id, operation, language_code, old_text, new_text, resourceids) + response = self.run_load_task(self.userid, self.loadid, self.moduleid, graph_id, node_id, operation, language_code, old_text, new_text, resourceids) else: self.log_event(cursor, "failed") return {"success": False, "data": event_created["message"]} @@ -371,7 +392,7 @@ def run_load_task_async(self, request): operation = operation + "_trim" edit_task = tasks.edit_bulk_string_data.apply_async( - (self.loadid, graph_id, node_id, operation, language_code, old_text, new_text, resourceids, self.userid), + (self.userid, self.loadid, self.moduleid, graph_id, node_id, operation, language_code, old_text, new_text, resourceids), ) with connection.cursor() as cursor: cursor.execute( @@ -379,7 +400,7 @@ def run_load_task_async(self, request): (edit_task.task_id, self.loadid), ) - def run_load_task(self, loadid, graph_id, node_id, operation, language_code, old_text, new_text, resourceids): + def run_load_task(self, userid, loadid, module_id, graph_id, node_id, operation, language_code, old_text, new_text, resourceids): if resourceids: resourceids = [uuid.UUID(id) for id in resourceids] case_insensitive = False @@ -387,7 +408,7 @@ def run_load_task(self, loadid, graph_id, node_id, operation, language_code, old case_insensitive = True with connection.cursor() as cursor: - data_staged = self.stage_data(cursor, graph_id, node_id, resourceids, operation, old_text, language_code, case_insensitive) + data_staged = self.stage_data(cursor, module_id, graph_id, node_id, resourceids, operation, old_text, language_code, case_insensitive) if data_staged["success"]: data_updated = self.edit_staged_data(cursor, graph_id, node_id, operation, language_code, old_text, new_text) @@ -397,7 +418,7 @@ def run_load_task(self, loadid, graph_id, node_id, operation, language_code, old if data_updated["success"]: self.loadid = loadid # currently redundant, but be certain - data_updated = save_to_tiles(loadid, finalize_import=False) + data_updated = save_to_tiles(userid, loadid, finalize_import=False) return {"success": True, "data": "done"} else: with connection.cursor() as cursor: diff --git a/arches/app/etl_modules/base_import_module.py b/arches/app/etl_modules/base_import_module.py index 294d9463f29..75d8b4c41a8 100644 --- a/arches/app/etl_modules/base_import_module.py +++ b/arches/app/etl_modules/base_import_module.py @@ -15,6 +15,7 @@ from arches.app.etl_modules.save import save_to_tiles from arches.app.models.models import Node +from arches.app.models.system_settings import settings from arches.app.utils.decorators import user_created_transaction_match from arches.app.utils.file_validator import FileValidator from arches.app.utils.transaction import reverse_edit_log_entries @@ -156,7 +157,7 @@ def get_node_lookup(self, nodes): lookup[node.alias] = {"nodeid": str(node.nodeid), "datatype": node.datatype, "config": node.config} return lookup - def run_load_task(self, files, summary, result, temp_dir, loadid): + def run_load_task(self, userid, files, summary, result, temp_dir, loadid): with connection.cursor() as cursor: for file in files.keys(): self.stage_excel_file(file, summary, cursor) @@ -173,7 +174,7 @@ def run_load_task(self, files, summary, result, temp_dir, loadid): result["validation"] = self.validate(loadid) if len(result["validation"]["data"]) == 0: self.loadid = loadid # currently redundant, but be certain - save_to_tiles(loadid, multiprocessing=False) + save_to_tiles(userid, loadid, multiprocessing=False) else: cursor.execute( """UPDATE load_event SET status = %s, load_end_time = %s WHERE loadid = %s""", @@ -204,7 +205,7 @@ def read(self, request): self.loadid = request.POST.get("load_id") self.cumulative_excel_files_size = 0 content = request.FILES["file"] - self.temp_dir = os.path.join("uploadedfiles", "tmp", self.loadid) + self.temp_dir = os.path.join(settings.UPLOADED_FILES_DIR, "tmp", self.loadid) try: self.delete_from_default_storage(self.temp_dir) except (FileNotFoundError): @@ -254,7 +255,7 @@ def read(self, request): def start(self, request): self.loadid = request.POST.get("load_id") - self.temp_dir = os.path.join("uploadedfiles", "tmp", self.loadid) + self.temp_dir = os.path.join(settings.UPLOADED_FILES_DIR, "tmp", self.loadid) result = {"started": False, "message": ""} with connection.cursor() as cursor: try: @@ -270,7 +271,7 @@ def start(self, request): def write(self, request): self.loadid = request.POST.get("load_id") - self.temp_dir = os.path.join("uploadedfiles", "tmp", self.loadid) + self.temp_dir = os.path.join(settings.UPLOADED_FILES_DIR, "tmp", self.loadid) self.file_details = request.POST.get("load_details", None) result = {} if self.file_details: @@ -281,10 +282,10 @@ def write(self, request): if summary["cumulative_excel_files_size"] / 1000000 > use_celery_file_size_threshold_in_MB: response = self.run_load_task_async(request, self.loadid) else: - response = self.run_load_task(files, summary, result, self.temp_dir, self.loadid) + response = self.run_load_task(self.userid, files, summary, result, self.temp_dir, self.loadid) return response - + class FileValidationError(Exception): def __init__(self, message=_("Unable to read file"), code=400): self.title = _("Invalid Uploaded File") diff --git a/arches/app/etl_modules/branch_excel_importer.py b/arches/app/etl_modules/branch_excel_importer.py index 281c1571116..a2cf7fdd530 100644 --- a/arches/app/etl_modules/branch_excel_importer.py +++ b/arches/app/etl_modules/branch_excel_importer.py @@ -12,6 +12,7 @@ from arches.app.datatypes.datatypes import DataTypeFactory from arches.app.etl_modules.decorators import load_data_async from arches.app.models.models import TileModel +from arches.app.models.system_settings import settings import arches.app.tasks as tasks from arches.app.utils.betterJSONSerializer import JSONSerializer from arches.management.commands.etl_template import create_workbook @@ -34,7 +35,7 @@ def __init__(self, request=None, loadid=None, temp_dir=None): @load_data_async def run_load_task_async(self, request): self.loadid = request.POST.get("load_id") - self.temp_dir = os.path.join("uploadedfiles", "tmp", self.loadid) + self.temp_dir = os.path.join(settings.UPLOADED_FILES_DIR, "tmp", self.loadid) self.file_details = request.POST.get("load_details", None) result = {} if self.file_details: @@ -64,7 +65,7 @@ def create_tile_value(self, cell_values, data_node_lookup, node_lookup, row_deta datatype_instance = self.datatype_factory.get_instance(datatype) source_value = row_details[key] config = node_details["config"] - config["path"] = os.path.join("uploadedfiles", "tmp", self.loadid) + config["path"] = os.path.join(settings.UPLOADED_FILES_DIR, "tmp", self.loadid) config["loadid"] = self.loadid try: config["nodeid"] = nodeid @@ -124,7 +125,7 @@ def process_worksheet(self, worksheet, cursor, node_lookup, nodegroup_lookup): operation = "insert" if user_tileid: - if nodegroup_cardinality == "n": + if nodegroup_cardinality == "n": operation = "update" # db will "insert" if tileid does not exist elif nodegroup_cardinality == "1": if TileModel.objects.filter(pk=cell_values[1]).exists(): @@ -183,7 +184,7 @@ def get_graphid(self, workbook): def stage_excel_file(self, file, summary, cursor): if file.endswith("xlsx"): summary["files"][file]["worksheets"] = [] - uploaded_file_path = os.path.join("uploadedfiles", "tmp", self.loadid, file) + uploaded_file_path = os.path.join(settings.UPLOADED_FILES_DIR, "tmp", self.loadid, file) workbook = load_workbook(filename=default_storage.open(uploaded_file_path)) graphid = self.get_graphid(workbook) nodegroup_lookup, nodes = self.get_graph_tree(graphid) diff --git a/arches/app/etl_modules/import_single_csv.py b/arches/app/etl_modules/import_single_csv.py index 5506b973d7d..306a8134d84 100644 --- a/arches/app/etl_modules/import_single_csv.py +++ b/arches/app/etl_modules/import_single_csv.py @@ -69,7 +69,7 @@ def read(self, request): """ content = request.FILES.get("file") - temp_dir = os.path.join("uploadedfiles", "tmp", self.loadid) + temp_dir = os.path.join(settings.UPLOADED_FILES_DIR, "tmp", self.loadid) try: self.delete_from_default_storage(temp_dir) except (FileNotFoundError): @@ -147,7 +147,7 @@ def write(self, request): ) return {"success": False, "data": error_message} - temp_dir = os.path.join("uploadedfiles", "tmp", self.loadid) + temp_dir = os.path.join(settings.UPLOADED_FILES_DIR, "tmp", self.loadid) csv_file_path = os.path.join(temp_dir, csv_file_name) csv_size = default_storage.size(csv_file_path) # file size in byte use_celery_threshold = 500 # 500 bytes @@ -155,11 +155,11 @@ def write(self, request): if csv_size > use_celery_threshold: response = self.run_load_task_async(request, self.loadid) else: - response = self.run_load_task(self.loadid, graphid, has_headers, fieldnames, csv_mapping, csv_file_name, id_label) + response = self.run_load_task(self.userid, self.loadid, graphid, has_headers, fieldnames, csv_mapping, csv_file_name, id_label) return response - def run_load_task(self, loadid, graphid, has_headers, fieldnames, csv_mapping, csv_file_name, id_label): + def run_load_task(self, userid, loadid, graphid, has_headers, fieldnames, csv_mapping, csv_file_name, id_label): self.populate_staging_table(loadid, graphid, has_headers, fieldnames, csv_mapping, csv_file_name, id_label) @@ -171,7 +171,7 @@ def run_load_task(self, loadid, graphid, has_headers, fieldnames, csv_mapping, c ("validated", loadid), ) self.loadid = loadid # currently redundant, but be certain - response = save_to_tiles(loadid, multiprocessing=False) + response = save_to_tiles(userid, loadid, multiprocessing=False) return response else: with connection.cursor() as cursor: @@ -204,7 +204,8 @@ def run_load_task_async(self, request): def start(self, request): graphid = request.POST.get("graphid") csv_mapping = request.POST.get("fieldMapping") - mapping_details = {"mapping": json.loads(csv_mapping), "graph": graphid} + csv_file_name = request.POST.get("csvFileName") + mapping_details = {"mapping": json.loads(csv_mapping), "graph": graphid, "file_name": csv_file_name} with connection.cursor() as cursor: cursor.execute( """INSERT INTO load_event (loadid, complete, status, etl_module_id, load_details, load_start_time, user_id) VALUES (%s, %s, %s, %s, %s, %s, %s)""", @@ -214,7 +215,7 @@ def start(self, request): return {"success": True, "data": message} def populate_staging_table(self, loadid, graphid, has_headers, fieldnames, csv_mapping, csv_file_name, id_label): - temp_dir = os.path.join("uploadedfiles", "tmp", loadid) + temp_dir = os.path.join(settings.UPLOADED_FILES_DIR, "tmp", loadid) csv_file_path = os.path.join(temp_dir, csv_file_name) with default_storage.open(csv_file_path, mode="r") as csvfile: reader = csv.reader(csvfile) # if there is a duplicate field, DictReader will not work diff --git a/arches/app/etl_modules/save.py b/arches/app/etl_modules/save.py index e9c5af984c7..c5b20cc90e6 100644 --- a/arches/app/etl_modules/save.py +++ b/arches/app/etl_modules/save.py @@ -1,8 +1,9 @@ from datetime import datetime import json from django.db.utils import IntegrityError, ProgrammingError -from django.utils.translation import gettext as _ +from django.contrib.auth.models import User from django.db import connection +from django.utils.translation import gettext as _ from arches.app.models.system_settings import settings from arches.app.utils.index_database import index_resources_by_transaction import logging @@ -10,7 +11,7 @@ logger = logging.getLogger(__name__) -def save_to_tiles(loadid, finalize_import=True, multiprocessing=True): +def save_to_tiles(userid, loadid, finalize_import=True, multiprocessing=True): with connection.cursor() as cursor: try: cursor.execute("""CALL __arches_prepare_bulk_load();""") @@ -86,6 +87,21 @@ def save_to_tiles(loadid, finalize_import=True, multiprocessing=True): ) try: index_resources_by_transaction(loadid, quiet=True, use_multiprocessing=False, recalculate_descriptors=True) + user = User.objects.get(id=userid) + user_email = getattr(user, "email", "") + user_firstname = getattr(user, "first_name", "") + user_lastname = getattr(user, "last_name", "") + user_username = getattr(user, "username", "") + cursor.execute( + """ + UPDATE edit_log e + SET (resourcedisplayname, userid, user_firstname, user_lastname, user_email, user_username) = (r.name ->> %s, %s, %s, %s, %s, %s) + FROM resource_instances r + WHERE e.resourceinstanceid::uuid = r.resourceinstanceid + AND transactionid = %s + """, + (settings.LANGUAGE_CODE, userid, user_firstname, user_lastname, user_email, user_username, loadid), + ) cursor.execute( """UPDATE load_event SET (status, indexed_time, complete, successful) = (%s, %s, %s, %s) WHERE loadid = %s""", ("indexed", datetime.now(), True, True, loadid), diff --git a/arches/app/etl_modules/tile_excel_importer.py b/arches/app/etl_modules/tile_excel_importer.py index 0181f0136a7..d6ae6e46c98 100644 --- a/arches/app/etl_modules/tile_excel_importer.py +++ b/arches/app/etl_modules/tile_excel_importer.py @@ -13,6 +13,7 @@ from arches.app.datatypes.datatypes import DataTypeFactory from arches.app.etl_modules.decorators import load_data_async from arches.app.models.models import Node, TileModel +from arches.app.models.system_settings import settings from arches.app.utils.betterJSONSerializer import JSONSerializer from arches.app.etl_modules.base_import_module import BaseImportModule, FileValidationError from arches.app.etl_modules.base_import_module import BaseImportModule @@ -34,7 +35,7 @@ def __init__(self, request=None, loadid=None, temp_dir=None): @load_data_async def run_load_task_async(self, request): self.loadid = request.POST.get("load_id") - self.temp_dir = os.path.join("uploadedfiles", "tmp", self.loadid) + self.temp_dir = os.path.join(settings.UPLOADED_FILES_DIR, "tmp", self.loadid) self.file_details = request.POST.get("load_details", None) result = {} if self.file_details: @@ -63,7 +64,7 @@ def create_tile_value(self, cell_values, data_node_lookup, node_lookup, nodegrou datatype_instance = self.datatype_factory.get_instance(datatype) source_value = row_details[key] config = node_details["config"] - config["path"] = os.path.join("uploadedfiles", "tmp", self.loadid) + config["path"] = os.path.join(settings.UPLOADED_FILES_DIR, "tmp", self.loadid) config["loadid"] = self.loadid try: config["nodeid"] = nodeid @@ -96,8 +97,11 @@ def process_worksheet(self, worksheet, cursor, node_lookup, nodegroup_lookup): row_count = 0 nodegroupid_column = int(worksheet.max_column) - nodegroup_alias = nodegroup_lookup[worksheet.cell(row=2,column=nodegroupid_column).value]['alias'] - data_node_lookup[nodegroup_alias] = [val.value for val in worksheet[1][3:-3]] + maybe_nodegroup = worksheet.cell(row=2,column=nodegroupid_column).value + if maybe_nodegroup: + nodegroup_alias = nodegroup_lookup[maybe_nodegroup]['alias'] + data_node_lookup[nodegroup_alias] = [val.value for val in worksheet[1][3:-3]] + # else: empty worksheet (no tiles) for row in worksheet.iter_rows(min_row=2): cell_values = [cell.value for cell in row] @@ -127,7 +131,7 @@ def process_worksheet(self, worksheet, cursor, node_lookup, nodegroup_lookup): nodegroup_cardinality = nodegroup_lookup[row_details["nodegroup_id"]]["cardinality"] operation = 'insert' if user_tileid: - if nodegroup_cardinality == "n": + if nodegroup_cardinality == "n": operation = "update" # db will "insert" if tileid does not exist elif nodegroup_cardinality == "1": if TileModel.objects.filter(pk=tileid).exists(): @@ -161,7 +165,7 @@ def process_worksheet(self, worksheet, cursor, node_lookup, nodegroup_lookup): [self.loadid], ) return {"name": worksheet.title, "rows": row_count} - + def validate_uploaded_file(self, workbook): graphid = None for worksheet in workbook.worksheets: @@ -189,7 +193,7 @@ def get_graphid(self, workbook): def stage_excel_file(self, file, summary, cursor): if file.endswith("xlsx"): summary["files"][file]["worksheets"] = [] - uploaded_file_path = os.path.join("uploadedfiles", "tmp", self.loadid, file) + uploaded_file_path = os.path.join(settings.UPLOADED_FILES_DIR, "tmp", self.loadid, file) workbook = load_workbook(filename=default_storage.open(uploaded_file_path)) graphid = self.get_graphid(workbook) nodegroup_lookup, nodes = self.get_graph_tree(graphid) diff --git a/arches/app/media/css/abstracts/_breakpoint-settings.scss b/arches/app/media/css/abstracts/_breakpoint-settings.scss index 6d628b71ab8..a5d5df3eb09 100644 --- a/arches/app/media/css/abstracts/_breakpoint-settings.scss +++ b/arches/app/media/css/abstracts/_breakpoint-settings.scss @@ -2,8 +2,12 @@ // across team members. It will improve communication between // stakeholders, designers, developers, and testers. +// ----mobile-small|----mobile-portrait|-----mobile|---mobile-max|tablet-----tablet-max|small-desktop-----small-desktop-max|desktop-lg------|wide---- + $breakpoints: ( - mobile: (min-width: 320px), + mobile-small: (max-width: 320px), + mobile-portrait: (max-width: 385px), + mobile: (max-width: 450px), mobile-max: (max-width: 740px), tablet: (min-width: 740px), tablet-max: (max-width: 915px), diff --git a/arches/app/media/css/arches.scss b/arches/app/media/css/arches.scss index c17d1f53d04..20df078c249 100644 --- a/arches/app/media/css/arches.scss +++ b/arches/app/media/css/arches.scss @@ -7,8 +7,9 @@ @import url(../node_modules/ionicons/css/ionicons.min.css); @import url(../node_modules/lt-themify-icons/themify-icons.css); @import url(../node_modules/chosen-js/chosen.css); -@import url(../node_modules/select2/select2.css); -@import url(../node_modules/select2/select2-bootstrap.css); +// @import url(../node_modules/select2/select2.css); +// @import url(../node_modules/select2/select2-bootstrap.css); +@import url(../node_modules/select-woo/dist/css/selectWoo.min.css); @import url(../node_modules/mapbox-gl/dist/mapbox-gl.css); @import url(../node_modules/nouislider/distribute/nouislider.min.css); @import url(../node_modules/codemirror/lib/codemirror.css); @@ -861,19 +862,11 @@ h4.branch-xl-title { max-width: 600px; } -.workflow-step-body div .new-provisional-edit-card-container .card form div div .widget-wrapper .form-group .resource-instance-wrapper .select2-container { - max-width: 600px !important; -} - .workflow-step-body div .new-provisional-edit-card-container .card form div div .widget-wrapper .form-group div .columns { border: 1px solid #ddd; padding: 20px; } -.new-provisional-edit-card-container .card form div div .widget-wrapper .form-group div .select2-container { - max-width: 600px !important; -} - .wf-multi-tile-step-container { display: flex; flex-direction: row; @@ -2365,189 +2358,11 @@ span.icon-wrap.icon-circle.bg-gray-dark:hover { margin: 3px; } -.select2-container { - width: 100% !important; - border: 1px solid #ddd; -} - .form-group div input { max-width: 600px; border: 1px solid #eee; } -.resource-instance-wrapper .select2-container { - max-width: 600px !important; - border: 1px solid #eee; -} - -.select2-container.select2-container-active.select2-dropdown-open { - border: 1px solid steelblue; -} - -.select2-choice { - border: 1px solid #E9E9E9 !important; - border-radius: 2px !important; - background-image: none !important; - height: 36px !important; - padding: 4px 0 0 16px !important; -} - -.select2-container .select2-choice { - display: flex; - height: 26px; - padding: 0 0 0 8px; - overflow: hidden; - position: relative; - // border: 1px solid #aaa; - border: none !important; - white-space: nowrap; - line-height: 26px; - color: #444; - text-decoration: none; - border-radius: 4px; - background-clip: padding-box; - -webkit-touch-callout: none; - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; - background-color: #fff; - background-image: -webkit-gradient(linear, left bottom, left top, color-stop(0, #eee), color-stop(0.5, #fff)); - background-image: -webkit-linear-gradient(center bottom, #eee 0, #fff 50%); - background-image: -moz-linear-gradient(center bottom, #eee 0, #fff 50%); - // filter: progid: DXImageTransform.Microsoft.gradient(startColorstr='#ffffff', endColorstr='#eeeeee', GradientType=0); - background-image: linear-gradient(to top, #eee 0, #fff 50%); -} - -a.select2-choice { - background: #42a5f5; -} - -.select2-dropdown-open .select2-choice { - background-color: #fff !important; -} - -.select2-arrow { - border: none !important; - background: none !important; - background-image: none !important; - padding-top: 2px; -} - -.select2-container .select2-choice .select2-arrow b:before { - content: ""; -} - -.select2-container.select2-container-disabled .select2-choice { - background: #eee; -} - -.select2-container-multi .select2-choices { - border: 1px solid #e1e5ea; - background-image: none; -} - -.select2-container-multi.select2-container-active .select2-choices { - border: 1px solid #e1e5ea; - box-shadow: none; -} - -.select2-drop { - border-radius: 0px; - color: inherit; -} - -.select2-drop-active { - border: 1px solid steelblue; - border-top: none; - box-shadow: none; -} - -.select2-result.disabled { - background-color: #eee; - color: #999; - pointer-events: none; -} - -.select2-results { - padding: 0px; - margin: 0px; -} - -.select2-results li { - /*padding: 4px 6px;*/ - padding: 0px; - line-height: 22px; - color: #595959; -} - -.select2-results .select2-no-results, -.select2-results .select2-searching, -.select2-results .select2-results .select2-ajax-error { - background: #f4f4f4; - line-height: 30px; - padding-left: 5px; - padding-top: 2px; - padding-bottom: 2px; -} - -.select2-results .select2-ajax-error { - color: #9e1515; -} - -.select2-results .select2-no-results { - font-size: 1.3rem; -} - -.select2-container-multi .select2-choices .select2-search-choice { - padding: 3px 10px 5px 18px; - margin: 4px 0 0px 5px; - background: #42a5f5; - position: relative; - line-height: 13px; - color: #fff; - cursor: default; - border: 1px solid #3b8dd5; - border-radius: 2px; - -webkit-box-shadow: none; - -webkit-touch-callout: none; - -moz-user-select: none; - -ms-user-select: none; - background-image: none; - -webkit-touch-callout: none; - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; - .fa-minus { - margin: 0px 2px 0px 7px; - } -} - -.filter-flag { - background: #30ad24 !important; - font-size: 1.3rem; -} - -.select2-container-multi .select2-choices li { - float: left; - list-style: none; -} - -.select2-container-multi .select2-search-choice-close { - left: 3px; - color: #fff; -} - -a.select2-search-choice-close { - background-color: #fff; - border-radius: 3px; -} - -.select2-search-choice div { - margin-top: 1px; -} - .btn-display-toggle { height: 35px; } @@ -6962,21 +6777,6 @@ div .switch label:hover, cursor: pointer; } -.select2-search, -.dropdown-shim, -.dropdown-shim, -.dropdown-shim { - margin-top: 10px; -} - -.select2-drop.select2-drop-above .select2-search input { - margin-bottom: 10px; -} - -.select2-results { - margin-top: 10px; -} - .relative, .slide, .relative, @@ -7236,12 +7036,6 @@ div.hide-file-list>div>div>div>div>form>div>div:nth-child(3) { max-width: 300px; } -.select2-container.select2-allowclear .select2-choice abbr { - margin-top: 0px; - padding: 6px 7px 6px 6px; - border: 1px solid #ccc; -} - .widget-preview { border: 1px solid transparent; } @@ -8686,7 +8480,6 @@ a#close-map-tools.map-widget-icon:hover { padding-left: 12px; border-radius: 3px; margin-bottom: 5px; - height: 75px; } .buffer-control .h5 { @@ -9494,9 +9287,8 @@ a.search-query-link-captions:focus { } .search-inline-filters { - /*display: flex; - flex-direction: row; - justify-content: right;*/ + display: flex; + justify-content: right; margin-top: 10px; margin-left: -5px; margin-bottom: 5px; @@ -10257,59 +10049,6 @@ table.table.dataTable { border: none; } -.resource_search_widget_dropdown ul .select2-disabled { - background: #eee; - height: 35px; -} - -.resource_search_widget_dropdown ul .select2-disabled .group { - padding: 0px; - border-top: 1px solid #C1D4F3; - width: 383px; -} - -.resource_search_widget_dropdown ul .select2-disabled div span span button { - width: 195px; - height: 35px; -} - -.resource_search_widget_dropdown ul .select2-disabled div span span button.active { - background: #8EAFE3; -} - -.resource_search_widget_dropdown ul .select2-disabled div span span button.term-search-btn:not(.active) { - background: #BBD1EA; - color: #658CC9; -} - -.resource_search_widget_dropdown ul .select2-disabled div span span button:not(.active):hover { - background: #B9D0F4; - color: #4330A4; -} - -.resource_search_widget_dropdown ul .select2-disabled div span span button:first-child { - border-right: 1px solid steelblue; -} - -.resource_search_widget_dropdown .select2-results { - background: #fdfdfd; - z-index: 10; - margin-top: 0px; - border-top: 1px solid steelblue; -} - -.resource_search_widget_dropdown.select2-drop-active { - border-color: steelblue; -} - -.resource_search_widget_dropdown ul li:not(.select2-no-results) { - color: #0A449F; -} - -.resource_search_widget_dropdown ul .select2-highlighted { - background: #E5EFFD; -} - .term-search-btn.active { color: #4330A4; } @@ -10448,166 +10187,6 @@ table.table.dataTable { margin-left: -8px; } -.facets-container { - width: 275px; - border-inline-start: 1px solid #ddd; -} - -.facets-search-container { - width: calc(100% - 275px); - height: calc(100vh - 115px); - overflow-y: auto; - padding: 2px; - background: white; - border-inline-start: 1px #e0e0e0 solid; -} - -.faceted-search-card-container { - border: 1px solid #ddd; - padding: 20px; - margin: 15px; - background: #f9f9f9; -} - -.search-facets { - height: calc(100vh - 115px); - overflow-y: auto; - background: #fbfbfb; -} - -.list-group.search-facets { - margin: 0; -} - -.search-facet-item { - position: relative; - display: block; - padding: 10px 15px; - margin-bottom: -1px; - background-color: #fff; - border: 1px solid #ddd; - border-right-width: 0px; - border-left-width: 0px; -} - -.search-facet-item:first-of-type { - border-top-width: 1px; -} - -a.search-facet-item:not(.active):hover { - cursor: pointer; - background: #fbfbfb; -} - -a.search-facet-item:hover, -a.search-facet-item:focus { - background-color: #f8f8f8; -} - -.search-facet-item.header { - background: #f2f2f2; - padding-top: 5px; - border-top: none; - position: sticky; - top: 0px; - z-index: 10; -} - -.search-facet-item.header .search-facet-item-heading { - font-weight: 600; - margin-bottom: 5px; -} - -div.search-facet-item.disabled { - border-bottom: 1px solid #ddd; - padding-left: 10px; - padding-right: 10px; -} - -.search-facet-item-heading { - font-weight: 400; - font-size: 1.3rem; -} - -.search-facet-item.header input { - border-color: #bbb; -} - -a.search-facet-item .search-facet-item-heading { - color: #666; -} - -a.search-facet-item { - color: #777; -} - -.search-facet-item.disabled { - background: #f6f6f6; - color: #666; - cursor: pointer; -} - -a.search-facet-item.disabled { - cursor: default; -} - -.facet-name { - font-size: 1.5rem; - color: #333; -} - -.facet-search-criteria { - position: relative; - padding: 10px 0px 0px 0px; -} - -.facet-search-button { - margin: 10px; - display: flex; - justify-content: flex-end; -} - -.facet-btn-group { - display: block; - margin: 10px 15px 50px 15px; -} - -.facet-btn { - width: 50%; - height: 40px; -} - -.facet-btn:focus, -.facet-btn.selected { - background: #ee9818; -} - -.facet-label { - margin-left: 5px; - margin-bottom: 5px; -} - -.facet-body { - padding-top: 5px; - padding-bottom: 45px; - margin-left: 10px; -} - -.facet-body .col-md-4.col-lg-3 { - padding-right: 5px; -} -.facet-body .col-md-3.col-lg-2 { - padding-right: 5px; -} - -.facet-body div div .select2-container { - border: none; -} - -.facet-body .chosen-container-single .chosen-single { - height: 36px; -} - .related-resources-header .resource-instance-wrapper { padding: 0; } @@ -10619,10 +10198,6 @@ a.search-facet-item.disabled { margin-left: -5px; } -#widget-crud-settings div.row.widget-wrapper div div .select2-container { - height: 32px; -} - .resource-instance-search .row.widget-wrapper { padding-top: 0; padding-left: 0; @@ -10659,12 +10234,6 @@ a.search-facet-item.disabled { border-radius: 2px; } -.select2-search-choice .sm-icon-wrap { - padding: 0px; - padding-left: 5px; - padding-right: 2px; -} - a.filter-tools { margin-left: 0px; padding: 3px 6px; @@ -10965,21 +10534,6 @@ a.filter-tools:hover { padding: 8px 6px } -ul.select2-choices { - padding-right: 30px !important; -} - -ul.select2-choices:after { - content: ""; - position: absolute; - right: 10px; - top: 50%; - transform: translateY(-50%); - border-top: 5px solid #333; - border-left: 5px solid transparent; - border-right: 5px solid transparent; -} - .sidepanel-draggable { background-color: #f7f7f7; border-left: solid 1px gainsboro; @@ -11811,36 +11365,6 @@ ul.select2-choices:after { left: 45px; } -.select2-drop.split-controls-drop { - max-width: 238px; - border: 2px solid #ddd; - border-top: none; -} - -.split-controls-drop .select2-result-label { - display: flex; - align-items: center; -} - -.split-controls-drop .select2-result-label > div { - margin-right: 5px; -} - -.split-controls-drop .select2-result-label .image img { - width: 50px; -} - -.split-controls-drop .select2-result-label .title { - width: 100%; - text-overflow: ellipsis; - overflow: hidden; -} - -.select2-container.select2-container-active.select2-dropdown-open.split-controls-drop{ - border: 2px solid #ddd; - border-radius: none; -} - .iiif-image-tools .layout .mode-selector { display: flex } @@ -13199,7 +12723,7 @@ ul.select2-choices:after { cursor: pointer; } -.etl-error-report-link { +.etl-status-link { color: steelblue; cursor: pointer; } diff --git a/arches/app/media/css/components/_dropdown.scss b/arches/app/media/css/components/_dropdown.scss new file mode 100644 index 00000000000..c54947cd21a --- /dev/null +++ b/arches/app/media/css/components/_dropdown.scss @@ -0,0 +1,66 @@ +.select2-container { + font-size: 1.4rem; + width: 100% !important; + max-width: 600px !important; +} + +.select2-container--default .select2-selection--single, +.select2-container--default .select2-selection--multiple { + border: 1px solid #ddd; + border-radius: 0px; + min-height: 36px; + + .select2-selection__rendered { + line-height: 34px; + } + + .select2-selection__arrow { + height: 34px; + } + + .select2-selection__choice { + background-color: #42a5f5; + + border: none; + //background: transparent; + color: white; + vertical-align: top; + height: 26px; + + & .fa-minus { + margin: 0px 2px 0px 7px; + } + + &__remove { + color: #595959; + background-color: #fff; + border-radius: 3px; + line-height: 12px; + font-size: 18px; + margin-top: 6px; + padding: 1px; + } + } + + .select2-selection__placeholder { + color: #595959; + } +} + +.select2-container--default .select2-selection--multiple .select2-selection__rendered { + line-height: 26px; +} + +.select2-container--default .select2-search--inline .select2-search__field { + line-height: 24px; + padding: 0px 5px; +} + +.select2-selection__clear { + margin: 0 8px; + font-size: 1.5em; + + &:hover { + color: #2986b8 + } +} diff --git a/arches/app/media/css/components/_index.scss b/arches/app/media/css/components/_index.scss index d65dec8b160..10851edd777 100644 --- a/arches/app/media/css/components/_index.scss +++ b/arches/app/media/css/components/_index.scss @@ -1,4 +1,5 @@ @import "buttons"; +@import "dropdown"; @import "datatypes"; @import "icon-selector"; @import "links"; diff --git a/arches/app/media/css/components/search/_advanced-search.scss b/arches/app/media/css/components/search/_advanced-search.scss index e57efe45e5a..023858074e6 100644 --- a/arches/app/media/css/components/search/_advanced-search.scss +++ b/arches/app/media/css/components/search/_advanced-search.scss @@ -31,4 +31,165 @@ } } } +} + +.facets-container { + width: 275px; + border-inline-start: 1px solid #ddd; +} + +.facets-search-container { + width: calc(100% - 275px); + height: calc(100vh - 115px); + overflow-y: auto; + padding: 2px; + background: white; + border-inline-start: 1px #e0e0e0 solid; +} + +.faceted-search-card-container { + border: 1px solid #ddd; + padding: 20px; + margin: 15px; + background: #f9f9f9; +} + +.search-facets { + height: calc(100vh - 115px); + overflow-y: auto; + background: #fbfbfb; +} + +.list-group.search-facets { + margin: 0; +} + +.search-facet-item { + position: relative; + display: block; + padding: 10px 15px; + margin-bottom: -1px; + background-color: #fff; + border: 1px solid #ddd; + border-right-width: 0px; + border-left-width: 0px; +} + +.search-facet-item:first-of-type { + border-top-width: 1px; +} + +a.search-facet-item:not(.active):hover { + cursor: pointer; + background: #fbfbfb; +} + +a.search-facet-item:hover, +a.search-facet-item:focus { + background-color: #f8f8f8; +} + +.search-facet-item.header { + background: #f2f2f2; + padding-top: 5px; + border-top: none; + position: sticky; + top: 0px; + z-index: 10; +} + +.search-facet-item.header .search-facet-item-heading { + font-weight: 600; + margin-bottom: 5px; +} + +div.search-facet-item.disabled { + border-bottom: 1px solid #ddd; + padding-left: 10px; + padding-right: 10px; +} + +.search-facet-item-heading { + font-weight: 400; + font-size: 1.3rem; +} + +.search-facet-item.header input { + border-color: #bbb; +} + +a.search-facet-item .search-facet-item-heading { + color: #666; + margin: 0px; +} + +a.search-facet-item { + color: #777; +} + +.search-facet-item.disabled { + background: #f6f6f6; + color: #666; + cursor: pointer; +} + +a.search-facet-item.disabled { + cursor: default; +} + +.facet-name { + font-size: 1.5rem; + color: #333; +} + +.facet-search-criteria { + position: relative; + padding: 10px 0px 0px 0px; +} + +.facet-search-button { + margin: 10px; + display: flex; + justify-content: flex-end; +} + +.facet-btn-group { + display: block; + margin: 10px 15px 50px 15px; +} + +.facet-btn { + width: 50%; + height: 40px; +} + +.facet-btn:focus, +.facet-btn.selected { + background: #ee9818; +} + +.facet-label { + margin-left: 5px; + margin-bottom: 5px; +} + +.facet-body { + padding-top: 5px; + padding-bottom: 45px; + margin-left: 10px; +} + +.facet-body .col-md-4.col-lg-3 { + padding-right: 5px; +} +.facet-body .col-md-3.col-lg-2 { + padding-right: 5px; +} + +.facet-body div div .select2-container { + border: none; +} + +.facet-body .chosen-container-single .chosen-single { + height: 36px; } \ No newline at end of file diff --git a/arches/app/media/css/components/search/_index.scss b/arches/app/media/css/components/search/_index.scss index 737f6a65a78..774b5b9a459 100644 --- a/arches/app/media/css/components/search/_index.scss +++ b/arches/app/media/css/components/search/_index.scss @@ -1,4 +1,5 @@ @import "advanced-search"; @import "time-filter"; +@import "term-search"; @import "related-resources"; -@import "search-results"; \ No newline at end of file +@import "search-results"; diff --git a/arches/app/media/css/components/search/_search-results.scss b/arches/app/media/css/components/search/_search-results.scss index ef157ebe37b..aef3001ae3a 100644 --- a/arches/app/media/css/components/search/_search-results.scss +++ b/arches/app/media/css/components/search/_search-results.scss @@ -1,3 +1,7 @@ .search-listing-footer { font-size: 1.2rem; + @include break-at(mobile-small){ + flex-direction: column; + height: auto; + } } \ No newline at end of file diff --git a/arches/app/media/css/components/search/_term-search.scss b/arches/app/media/css/components/search/_term-search.scss new file mode 100644 index 00000000000..8f816745f8d --- /dev/null +++ b/arches/app/media/css/components/search/_term-search.scss @@ -0,0 +1,26 @@ +.term-search-filter .select2-container--default .select2-selection--multiple { + .select2-selection__rendered { + line-height: 24px; + } + + .select2-selection__choice { + line-height: 24px; + + .filter-flag { + background-color: #30ad24; + } + + button.search-tag { + border: none; + background: transparent; + color: white; + vertical-align: top; + height: 26px; + + .fa-minus { + margin: 0px 2px 0px 7px; + } + } + + } +} diff --git a/arches/app/media/css/pages/_search.scss b/arches/app/media/css/pages/_search.scss index bfabc742298..6a9d6c74d32 100644 --- a/arches/app/media/css/pages/_search.scss +++ b/arches/app/media/css/pages/_search.scss @@ -12,9 +12,15 @@ article.main-search-container { max-width: calc(100vw - 50px); } .search-tools-container { + display: flex; + justify-content: space-between; + flex-wrap: wrap; @include break-at(small-desktop-max) { width: 100%; } + @include break-at(mobile-small) { + height: auto; + } .clear-filter { @include break-at(mobile-max) { width: auto; @@ -62,11 +68,22 @@ article.main-search-container { flex-direction: column; height: 52px; } + @include break-at(mobile) { + height: auto; + flex-direction: row; + } + @include break-at(mobile-portrait){ + flex-direction: column; + } .search-type-btn-panel { @include break-at(small-desktop-max) { margin-left: 5px; height: auto; } + @include break-at(mobile-portrait) { + display: flex; + justify-content: center; + } .search-type-btn { @include break-at(small-desktop-max) { height: 20px; @@ -75,10 +92,27 @@ article.main-search-container { margin: 5px 2px; min-width: 69px; } - .fa { + @include break-at(mobile) { + height: 42px; + min-width: 42px; + margin: 0; + top: 1px; + } + i { @include break-at(small-desktop-max) { display: none; } + @include break-at(mobile) { + display: block; + } + } + span { + @include break-at(mobile-max) { + display: block; + } + @include break-at(mobile) { + display: none; + } } } } @@ -92,6 +126,13 @@ article.main-search-container { @include break-at(mobile-max) { top: -2px; } + @include break-at(mobile) { + top: 0px; + } + @include break-at(mobile-portrait) { + display: flex; + justify-content: center; + } .search-type-btn-popup { @include break-at(small-desktop-max) { min-width: 90px; @@ -99,23 +140,35 @@ article.main-search-container { border: 1px solid #d8d8d8; margin: 0 4px; } + @include break-at(mobile) { + height: 42px; + min-width: 42px; + width: auto; + margin: 1px; + } &.active { @include break-at(small-desktop-max) { line-height: inherit; } } - .fa { + i { @include break-at(small-desktop-max) { display: none; } + @include break-at(mobile) { + display: block; + } } p { - @include break-at(small-desktop) { + @include break-at(desktop-lg) { display: none; } - @include break-at(small-desktop-max) { + @include break-at(mobile-max) { display: block; } + @include break-at(mobile) { + display: none; + } } } } diff --git a/arches/app/media/js/bindings/select2-query.js b/arches/app/media/js/bindings/select2-query.js index 2bbc4767428..db0705d0a48 100644 --- a/arches/app/media/js/bindings/select2-query.js +++ b/arches/app/media/js/bindings/select2-query.js @@ -2,7 +2,7 @@ define([ 'jquery', 'knockout', 'underscore', - 'select2' + 'select-woo' ], function($, ko, _) { ko.bindingHandlers.select2Query = { init: function(el, valueAccessor, allBindingsAccessor) { @@ -12,69 +12,101 @@ define([ select2Config = _.defaults(select2Config, { clickBubble: true, multiple: false, - allowClear: true, + allowClear: false, + minimumResultsForSearch: 8 }); var value = select2Config.value; + value.extend({ rateLimit: 250 }); + // select2Config.value = value(); ko.utils.domNodeDisposal.addDisposeCallback(el, function() { - $(el).select2('destroy'); + $(el).selectWoo('destroy'); + $(el).off("select2:selecting"); + $(el).off("select2:opening"); + $(el).off("change"); }); var placeholder = select2Config.placeholder; if (ko.isObservable(placeholder)) { placeholder.subscribe(function(newItems) { select2Config.placeholder = newItems; - $(el).select2("destroy").select2(select2Config); - }); + $(el).selectWoo(select2Config); + }, this); select2Config.placeholder = select2Config.placeholder(); if (select2Config.allowClear) { select2Config.placeholder = select2Config.placeholder === "" ? " " : select2Config.placeholder; } } - //select2Config.value = value(); - $(el).select2(select2Config); - - if (value) { - $(el).select2("val", value()); - value.subscribe(function(newVal) { - select2Config.value = newVal; - $(el).select2("val", newVal); - }, this); - $(el).on("change", function(val) { - if (val.val === "") { - val.val = null; - } - return value(val.val); + var disabled = select2Config.disabled; + if (ko.isObservable(disabled)) { + disabled.subscribe(function(isdisabled) { + select2Config.disabled = isdisabled; + $(el).selectWoo("destroy").selectWoo(select2Config); }); + select2Config.disabled = select2Config.disabled(); } - if (ko.unwrap(select2Config.disabled)) { - $(el).select2("disable"); - select2Config.disabled.subscribe(function(val){ - if (val === false) { - $(el).select2("enable"); - } - }); - } + // this initializes the placeholder for the single select element + // we shouldn't have to do this but there is some issue with selectwoo + // specifically rendering the placeholder for resource instance widgets in adv. search + var renderPlaceholder = function() { + var renderedEle = $(el).siblings().first().find('.select2-selection__rendered'); + var placeholderEle = renderedEle.find('.select2-selection__placeholder'); + if (placeholderEle[0]?.innerText === "" && !select2Config.multiple){ + placeholderEle.remove(); + var placeholderHtml = document.createElement("span"); + var placeholderText = document.createTextNode(select2Config.placeholder); + placeholderHtml.classList.add('select2-selection__placeholder'); + placeholderHtml.appendChild(placeholderText); + renderedEle.append(placeholderHtml); + } + }; - $(el).on("select2-opening", function() { + $(document).ready(function() { + select2Config.data = ko.unwrap(select2Config.data); + $(el).selectWoo(select2Config); + + if (value) { + // initialize the dropdown with the value + $(el).val(value()); + $(el).trigger('change.select2'); + + // update the dropdown if something else changes the value + value.subscribe(function(newVal) { + console.log(newVal); + // select2Config.value = newVal; + $(el).val(newVal); + $(el).trigger('change.select2'); + + if(!newVal){ + renderPlaceholder(); + } + }, this); + } + renderPlaceholder(); + }); + + + $(el).on("change", function(e) { + let val = $(el).val(); + if (val === "") { + val = null; + } + value(val); + }); + + $(el).on("select2:opening", function() { if (select2Config.clickBubble) { $(el).parent().trigger('click'); } }); - + if (typeof select2Config.onSelect === 'function') { - $(el).on("select2-selecting", function(e) { - select2Config.onSelect(e.choice); - }); - } - if (typeof select2Config.onClear === 'function') { - $(el).on("select2-clearing", function(e) { - select2Config.onClear(e.choice); + $(el).on("select2:selecting", function(e) { + select2Config.onSelect(e.params.args.data); }); } - } }; diff --git a/arches/app/media/js/bindings/select2v4.js b/arches/app/media/js/bindings/select2v4.js index 32b3256de57..625023d3e92 100644 --- a/arches/app/media/js/bindings/select2v4.js +++ b/arches/app/media/js/bindings/select2v4.js @@ -71,7 +71,7 @@ // Provide a hook for binding to the select2 "data" property; this property is read-only in select2 so not subscribing. if (ko.isWriteableObservable(allBindings[dataBindingName])) { dataChangeHandler = function() { - if (!$(element).data('select2')) return; + if (!$(element).data('select-woo')) return; allBindings[dataBindingName]($(element).select2('data')); }; $(element).on('change', dataChangeHandler); diff --git a/arches/app/media/js/bindings/term-search.js b/arches/app/media/js/bindings/term-search.js index b16fe57eb5a..765ca208de6 100644 --- a/arches/app/media/js/bindings/term-search.js +++ b/arches/app/media/js/bindings/term-search.js @@ -3,7 +3,7 @@ define([ 'underscore', 'knockout', 'arches', - 'select2' + 'select-woo' ], function($, _, ko, arches) { ko.bindingHandlers.termSearch = { init: function(el, valueAccessor, allBindingsAccessor, viewmodel, bindingContext) { @@ -11,104 +11,102 @@ define([ var terms = valueAccessor().terms; var tags = valueAccessor().tags; var language = valueAccessor().language; + var placeholder = valueAccessor().placeholder; - var notifyValueChange = function(value){ - var val = terms().concat(tags()); - searchbox.select2('data', val); //.trigger('change'); - }; - - terms.subscribe(function(value) { - notifyValueChange(value); - }); - - tags.subscribe(function(value) { - notifyValueChange(value); - }); + tags.subscribe(function(tags) { + // first clear any existing tags + searchbox.tags.forEach(tag => { + tag.remove(); + }); + searchbox.tags = []; - language.subscribe((value) => { - notifyValueChange(value); + tags.forEach(item => { + var option = new Option(item.text, item.id, true, true); + option.data = item; + searchbox.append(option); + searchbox.tags.push(option); + }); + searchbox.trigger('change'); }); + var self = this; + this.stripMarkup = function(m){ + return m.replace(/(<([^>]+)>)/gi, ""); + }; - var searchbox = $(el).select2({ - dropdownCssClass: 'resource_search_widget_dropdown', + var searchbox = $(el).selectWoo({ + dropdownCssClass: ':all:', + placeholder: placeholder, multiple: true, minimumInputLength: 2, + data:ko.unwrap(terms).concat(ko.unwrap(tags)), // initial selection ajax: { url: arches.urls.search_terms, dataType: 'json', quietMillis: 500, - data: function(term, page) { + data: function(requestParams) { + let term = requestParams.term || ''; return { q: term, // search term - lang: language().code, - page_limit: 30 + lang: language().code }; }, - results: function(data, page) { - var value = $(el).parent().find('.select2-input').val(); - - // this result is being hidden by a style in arches.css - // .select2-results li:first-child{ - // display:none; - // } - var results = []; - searchbox.groups = []; + processResults: function(data, params) { + window.setTimeout(function() { + // handles tabbing into the tags themselves to allow for removal + $('.select2-results').on('keydown', (e) => { + if (e.keyCode == 9) { + e.preventDefault(); + var elem = $('.select2-results button'); + elem.focus(); + } + }); + }, 2000); _.each(data, function(value, searchType) { - if (value.length > 0) { - searchbox.groups.unshift(searchType); - } _.each(value, function(val) { val.inverted = ko.observable(false); - results.push(val); + val.id = val.type + val.value + val.context_label; }, this); }, this); - //res = _.groupBy(results, 'type'); var res = []; res.push({ - inverted: ko.observable(false), - type: 'group', - context: '', - context_label: '', - id: '', - text: searchbox.groups, - value: '', - disabled: true - }); - _.each(_.groupBy(results, 'type'), function(value, group){ - res = res.concat(value); - }); - res.unshift({ inverted: ko.observable(false), type: 'string', context: '', context_label: '', - id: value, - text: value, - value: value + id: params.term, + text: params.term, + value: params.term }); + if(data.terms.length > 0){ + res.push({"text": "Terms", "children": data.terms}); + } + if(data.concepts.length > 0){ + res.push({"text": "Concepts", "children": data.concepts}); + } return { results: res }; } }, - id: function(item) { - return item.type + item.value + item.context_label; - }, - formatResult: function(result, container, query, escapeMarkup) { + templateResult: function(result, container) { + if (result.loading || result.children) { + return result.text; + } var markup = []; var indent = result.type === 'concept' || result.type === 'term' ? 'term-search-item indent' : (result.type === 'string' ? 'term-search-item' : 'term-search-group'); if (result.type === 'group') { _.each(result.text, function(searchType, i) { var label = searchType === 'concepts' ? arches.translations.termSearchConcept : arches.translations.termSearchTerm; var active = i === 0 ? 'active' : ''; - markup.push(''); + markup.push(''); }); } else { - window.Select2.util.markMatch(result.text, query.term, markup, escapeMarkup); + markup.push(self.stripMarkup(result.text)); + //window.selectWoo.util.markMatch(result.text, query.term, markup, stripMarkup); } var context = result.context_label != '' ? '(' + _.escape(result.context_label) + ')' : ''; var formatedresult = '' + markup.join("") + '' + context + ''; - container[0].className = container[0].className + ' ' + result.type; + container.className = container.className + ' ' + result.type; $(container).click(function(event){ var btn = event.target.closest('button'); if(!!btn && btn.id === 'termsgroup') { @@ -124,35 +122,64 @@ define([ }); return formatedresult; }, - formatSelection: function(result, container) { + templateSelection: function(result, container) { + if(result.element.data){ + result = { + ...result, + ...result.element.data + }; + } + + result.text = self.stripMarkup(result.text); + var context = result.context_label != '' ? '(' + _.escape(result.context_label) + ')' : ''; - var markup = '' + result.text + '' + context; + var markup = ''; if (result.inverted()) { - markup = '' + result.text + '' + context; + markup = ''; } if (result.type !== 'string' && result.type !== 'concept' && result.type !== 'term') { - $(container.prevObject).addClass('filter-flag'); + $(container).addClass('filter-flag'); + }else{ + $(container).addClass('term-flag'); } + $(container).click(function(event){ + var btn = event.target.closest('button'); + if(!!btn && btn.className === 'search-tag') { + let params = {el: $(btn), result: result}; + searchbox.trigger('choice-selected', params); + } + }); + $(container).find('span.select2-selection__choice__remove').eq(0) + .attr('tabindex', '0').attr('aria-label', 'remove item') + .on('keydown', function(evt) { + if(evt.keyCode === 13){ + $(evt.currentTarget).click(); + } + }); + return markup; }, escapeMarkup: function(m) { return m; } - }).on('change', function(e, el) { - if (e.added) { - terms.push(e.added); + }).on('select2:select', function(e) { + terms.push(e.params.data); + }).on('select2:unselect', function(e) { + if(e.params.data.element.data){ + e.params.data = { + ...e.params.data, + ...e.params.data.element.data + }; } - if (e.removed) { - terms.remove(function(item) { - return item.id === e.removed.id && item.context_label === e.removed.context_label; - }); - tags.remove(function(item) { - return item.id === e.removed.id && item.context_label === e.removed.context_label; - }); - } - }).on('choice-selected', function(e, el) { - var selectedTerm = $(el).data('select2-data'); - var terms = searchbox.select2('data'); + terms.remove(function(item) { + return item.id === e.params.data.id && item.context_label === e.params.data.context_label; + }); + tags.remove(function(item) { + return item.id === e.params.data.id && item.context_label === e.params.data.context_label; + }); + }).on('choice-selected', function(e, params) { + let el = params.el; + let selectedTerm = params.result; if (selectedTerm.id !== "Advanced Search") { if(!selectedTerm.inverted()){ @@ -162,20 +189,9 @@ define([ } selectedTerm.inverted(!selectedTerm.inverted()); } - - //terms(terms); - - }).on('select2-loaded', function(e, el) { - if (searchbox.groups.length > 0) { - if (searchbox.groups[0] === 'concepts'){ - $('.term').hide(); - } else { - $('.concept').hide(); - } - } - }); - searchbox.select2('data', ko.unwrap(terms).concat(ko.unwrap(tags))).trigger('change'); + + searchbox.tags = []; } }; diff --git a/arches/app/media/js/utils/ontology.js b/arches/app/media/js/utils/ontology.js index ce48b504e46..b3ff713f0fd 100644 --- a/arches/app/media/js/utils/ontology.js +++ b/arches/app/media/js/utils/ontology.js @@ -1,4 +1,4 @@ -define(['knockout', 'arches'], function(ko, arches) { +define(['jquery', 'knockout', 'arches'], function($, ko, arches) { var ontologyUtils = { /** * makeFriendly - makes a shortened name from an fully qalified name @@ -24,10 +24,8 @@ define(['knockout', 'arches'], function(ko, arches) { closeOnSelect: true, allowClear: allowClear || false, ajax: { - url: function() { - return arches.urls.ontology_properties; - }, - data: function(term, page) { + url: arches.urls.ontology_properties, + data: function(requestParams) { var data = { 'domain_ontology_class': domain, 'range_ontology_class': range, @@ -37,25 +35,36 @@ define(['knockout', 'arches'], function(ko, arches) { }, dataType: 'json', quietMillis: 250, - results: function(data, page, query) { + processResults: function(data, params) { var ret = data; - if(query.term !== ""){ + if(!!params.term && params.term !== ""){ ret = data.filter(function(item){ - return item.toUpperCase().includes(query.term.toUpperCase()); + return item.toUpperCase().includes(params.term.toUpperCase()); }); } + ret = ret.map((item) => { + return {id: item, text: item}; + }); return { results: ret }; } }, - id: function(item) { - return item; + templateResult: function(item) { + return ontologyUtils.makeFriendly(item.text); + }, + templateSelection: function(item) { + return ontologyUtils.makeFriendly(item.text); }, - formatResult: ontologyUtils.makeFriendly, - formatSelection: ontologyUtils.makeFriendly, initSelection: function(el, callback) { - callback(value()); + if(!!value()){ + var data = {id: value(), text: value()}; + var option = new Option(data.text, data.id, true, true); + $(el).append(option); + callback([data]); + }else{ + callback([]); + } } }; } diff --git a/arches/app/media/js/viewmodels/card-constraints.js b/arches/app/media/js/viewmodels/card-constraints.js index cd6bd28827a..48143c33d8f 100644 --- a/arches/app/media/js/viewmodels/card-constraints.js +++ b/arches/app/media/js/viewmodels/card-constraints.js @@ -11,7 +11,7 @@ define([ return { clickBubble: true, disabled: false, - data: {results: nodeOptions}, + data: nodeOptions, value: this.constraint.nodes, multiple: params.multiple || true, closeOnSelect: false, diff --git a/arches/app/media/js/viewmodels/concept-select.js b/arches/app/media/js/viewmodels/concept-select.js index 2366acebfa5..b5ed21433eb 100644 --- a/arches/app/media/js/viewmodels/concept-select.js +++ b/arches/app/media/js/viewmodels/concept-select.js @@ -89,44 +89,57 @@ define([ url: arches.urls.paged_dropdown, dataType: 'json', quietMillis: 250, - data: function(term, page) { + data: function(requestParams) { + let term = requestParams.term || ''; + let page = requestParams.page || 1; return { conceptid: ko.unwrap(params.node.config.rdmCollection), query: term, page: page }; }, - results: function(data) { + processResults: function(data) { data.results.forEach(function(result) { if (result.collector) { delete result.id; } }); return { - results: data.results, - more: data.more + "results": data.results, + "pagination": { + "more": data.more + } }; } }, - id: function(item) { - return item.id; - }, - formatResult: function(item) { + templateResult: function(item) { var indentation = ''; for (var i = 0; i < item.depth-1; i++) { indentation += '    '; } return indentation + item.text; }, - formatSelection: function(item) { + templateSelection: function(item) { return item.text; }, - clear: function() { - self.value(''); - }, isEmpty: ko.computed(function() { return self.value() === '' || !self.value(); }), + // init: function(el){ + // valueData.forEach(function(data) { + // var option = new Option(data.text, data.id, true, true); + // $(el).append(option).trigger('change'); + + // // manually trigger the `select2:select` event + // $(el).trigger({ + // type: 'select2:select', + // params: { + // data: data + // } + // }); + // }); + // }, + initComplete: false, initSelection: function(el, callback) { var valueList = self.valueList(); @@ -158,31 +171,42 @@ define([ valueData.reverse(); } - } - else { + if(!self.select2Config.initComplete){ + valueData.forEach(function(data) { + var option = new Option(data.text, data.id, true, true); + $(el).append(option); + }); + self.select2Config.initComplete = true; + } + } else { valueData = { id: data, text: NAME_LOOKUP[data], }; } - if (valueData) { callback(valueData); } + callback(valueData); }; - valueList.forEach(function(value) { - if (ko.unwrap(value)) { - if (NAME_LOOKUP[value]) { - setSelectionData(value); - } else { - $.ajax(arches.urls.concept_value + '?valueid=' + ko.unwrap(value), { - dataType: "json" - }).done(function(data) { - NAME_LOOKUP[value] = data.value; + if (valueList.length > 0) { + valueList.forEach(function(value) { + if (ko.unwrap(value)) { + if (NAME_LOOKUP[value]) { setSelectionData(value); - }); + } else { + $.ajax(arches.urls.concept_value + '?valueid=' + ko.unwrap(value), { + dataType: "json" + }).done(function(data) { + NAME_LOOKUP[value] = data.value; + setSelectionData(value); + }); + } } - } - }); + }); + }else{ + callback([]); + } + } }; diff --git a/arches/app/media/js/viewmodels/excel-file-import.js b/arches/app/media/js/viewmodels/excel-file-import.js index cb200883c39..d91b008b0fe 100644 --- a/arches/app/media/js/viewmodels/excel-file-import.js +++ b/arches/app/media/js/viewmodels/excel-file-import.js @@ -21,6 +21,7 @@ define([ this.loadStatus = ko.observable('ready'); this.downloadMode = ko.observable(false); this.selectedLoadEvent = params.selectedLoadEvent || ko.observable(); + this.editHistoryUrl = `${arches.urls.edit_history}?transactionid=${ko.unwrap(params.selectedLoadEvent)?.loadid}`; this.validationErrors = params.validationErrors || ko.observable(); this.validated = params.validated || ko.observable(); this.getErrorReport = params.getErrorReport; diff --git a/arches/app/media/js/viewmodels/node-value-select.js b/arches/app/media/js/viewmodels/node-value-select.js index c357981d601..8f54fe24c54 100644 --- a/arches/app/media/js/viewmodels/node-value-select.js +++ b/arches/app/media/js/viewmodels/node-value-select.js @@ -102,9 +102,10 @@ define([ data: function(term) { return {nodeid: params.node.config.nodeid(), term:term}; }, - results: function(data) { + processResults: function(data) { var options = []; data.tiles.forEach(function(tile) { + tile.id = tile.tileid; options.push(tile); }); return { results: options }; @@ -140,10 +141,7 @@ define([ } }, escapeMarkup: function(m) { return m; }, - id: function(tile) { - return tile.tileid; - }, - formatResult: function(tile) { + templateResult: function(tile) { var nodeid = params.node.config.nodeid(); var nodeDisplayValue = _.find(tile.display_values, function(displayValue) { return nodeid === displayValue.nodeid; @@ -163,12 +161,12 @@ define([ return markup; } }, - formatSelection: function(tile) { + templateSelection: function(tile) { var nodeid = params.node.config.nodeid(); var displayValue = _.find(tile.display_values, function(dv) { return nodeid === dv.nodeid; }); - return displayValue.value; + return displayValue?.value; } }; }; diff --git a/arches/app/media/js/viewmodels/resource-instance-select.js b/arches/app/media/js/viewmodels/resource-instance-select.js index f0122f32fae..93c65aa2027 100644 --- a/arches/app/media/js/viewmodels/resource-instance-select.js +++ b/arches/app/media/js/viewmodels/resource-instance-select.js @@ -1,12 +1,13 @@ define([ + 'jquery', 'knockout', 'underscore', - 'jquery', 'arches', 'viewmodels/widget', 'utils/ontology', + 'select-woo', 'views/components/resource-report-abstract', -], function(ko, _, $, arches, WidgetViewModel, ontologyUtils) { +], function($, ko, _, arches, WidgetViewModel, ontologyUtils) { var resourceLookup = {}; var graphCache = {}; @@ -43,6 +44,7 @@ define([ params.configKeys = ['placeholder', 'defaultResourceInstance']; this.preview = arches.graphs.length > 0; this.renderContext = params.renderContext; + this.label = params.label; this.relationship = ko.observable(); /* shoehorn logic to piggyback off of search context functionality. @@ -52,10 +54,7 @@ define([ self.renderContext = 'search'; } - this.allowInstanceCreation = params.allowInstanceCreation === false ? false : true; - if (self.renderContext === 'search') { - this.allowInstanceCreation = params.allowInstanceCreation === true ? true : false; - } + this.allowInstanceCreation = typeof params.allowInstanceCreation === 'undefined' ? true : !!params.allowInstanceCreation; if (!!params.configForm) { this.allowInstanceCreation = false; } @@ -341,64 +340,62 @@ define([ allowClear: self.renderContext === 'search' ? true : false, onSelect: function(item) { self.selectedItem(item); - if (!(self.renderContext === 'search') || self.allowInstanceCreation) { - if (item._source) { - if (self.renderContext === 'search'){ - self.value(item._id); - } else { - var ret = self.makeObject(item._id, item._source); - self.setValue(ret); - window.setTimeout(function() { - if(self.displayOntologyTable){ - self.resourceToAdd(""); - } - }, 250); - } + if (item._source) { + if (self.renderContext === 'search'){ + self.value(item._id); } else { - // This section is used when creating a new resource Instance - if(!self.preview){ - var params = { - graphid: item._id, - complete: ko.observable(false), - resourceid: ko.observable(), - tileid: ko.observable() - }; - self.newResourceInstance(params); - var clearNewInstance = function() { - self.newResourceInstance(null); - window.setTimeout(function() { - self.resourceToAdd(""); - }, 250); - }; - let resourceCreatorPanel = document.querySelector('#resource-creator-panel'); - resourceCreatorPanel.addEventListener("transitionend", () => $(resourceCreatorPanel).find('.resource-instance-card-menu-item.selected').focus()); // focus on the resource creator panel for keyboard readers - params.complete.subscribe(function() { - if (params.resourceid()) { - if (self.renderContext === 'search'){ - self.value(params.resourceid()); - clearNewInstance(); - } else { - window.fetch(arches.urls.search_results + "?id=" + params.resourceid()) - .then(function(response){ - if(response.ok) { - return response.json(); - } - throw("error"); - }) - .then(function(json) { - var item = json.results.hits.hits[0]; - var ret = self.makeObject(params.resourceid(), item._source); - self.setValue(ret); - }) - .finally(function(){ - clearNewInstance(); - }); - } - } else { + var ret = self.makeObject(item._id, item._source); + self.setValue(ret); + window.setTimeout(function() { + if(self.displayOntologyTable){ + self.resourceToAdd(""); + } + }, 250); + } + } else { + // This section is used when creating a new resource Instance + if(!self.preview){ + var params = { + graphid: item._id, + complete: ko.observable(false), + resourceid: ko.observable(), + tileid: ko.observable() + }; + self.newResourceInstance(params); + var clearNewInstance = function() { + self.newResourceInstance(null); + window.setTimeout(function() { + self.resourceToAdd(""); + }, 250); + }; + let resourceCreatorPanel = document.querySelector('#resource-creator-panel'); + resourceCreatorPanel.addEventListener("transitionend", () => $(resourceCreatorPanel).find('.resource-instance-card-menu-item.selected').focus()); // focus on the resource creator panel for keyboard readers + params.complete.subscribe(function() { + if (params.resourceid()) { + if (self.renderContext === 'search'){ + self.value(params.resourceid()); clearNewInstance(); + } else { + window.fetch(arches.urls.search_results + "?id=" + params.resourceid()) + .then(function(response){ + if(response.ok) { + return response.json(); + } + throw("error"); + }) + .then(function(json) { + var item = json.results.hits.hits[0]; + var ret = self.makeObject(params.resourceid(), item._source); + self.setValue(ret); + }) + .finally(function(){ + clearNewInstance(); + }); } - }); - } + } else { + clearNewInstance(); + } + }); } } }, @@ -408,7 +405,9 @@ define([ }, dataType: 'json', quietMillis: 250, - data: function(term, page) { + data: function(requestParams) { + let term = requestParams.term || ''; + let page = requestParams.page || 1; //TODO This regex isn't working, but it would nice fix it so that we can do more robust url checking // var expression = /[-a-zA-Z0-9@:%_\+.~#?&//=]{2,256}\.[a-z]{2,4}\b(\/[-a-zA-Z0-9@:%_\+.~#?&//=]*)?/gi; // var regex = new RegExp(expression); @@ -460,7 +459,7 @@ define([ return queryString.toString(); } }, - results: function(data, page) { + processResults: function(data) { if (!data['paging-filter'].paginator.has_next && self.allowInstanceCreation) { self.resourceTypesToDisplayInDropDown().forEach(function(graphid) { var graph = self.graphLookup[graphid]; @@ -475,32 +474,42 @@ define([ } }); } + data.results.hits.hits.forEach(function(hit){ + hit.id = hit._id; + }); return { - results: data.results.hits.hits, - more: data['paging-filter'].paginator.has_next + "results": data.results.hits.hits, + "pagination": { + "more": data['paging-filter'].paginator.has_next + } }; } }, - id: function(item) { - return item._id; - }, - formatResult: function(item) { - if (item._source) { - const iconClass = self.graphLookup[item._source.graph_id]?.iconclass; - return ` ${item._source.displayname}`; - } else { - const graph = self.graphLookup[item._id]; - if (self.allowInstanceCreation && graph.publication_id) { - return ' ' + arches.translations.riSelectCreateNew.replace('${graphName}', item.name) + ' . . . '; + templateResult: function(item) { + let res = ''; + if(!item.loading){ + if (item._source) { + const iconClass = self.graphLookup[item._source.graph_id]?.iconclass; + res = ` ${item._source.displayname}`; + } else { + const graph = self.graphLookup[item._id]; + if (self.allowInstanceCreation && graph.publication_id) { + res = ' ' + arches.translations.riSelectCreateNew.replace('${graphName}', item.name) + ' . . . '; + } } } + return $(res); }, - formatSelection: function(item) { + templateSelection: function(item) { + let ret = ''; if (item._source) { - return ` ${item._source.displayname}`; + var graph = self.graphLookup[item._source.graph_id]; + var iconClass = graph?.iconclass || ''; + ret = ` ${item._source.displayname}`; } else { - return item.name; + ret = item.name; } + return $(ret); }, initSelection: function(ele, callback) { if(self.renderContext === "search" && self.value() !== "" && !self.graphIds().includes(self.value())) { @@ -526,20 +535,23 @@ define([ if (resourceInstance) { lookups.push(resourceInstance); } }); - Promise.all(lookups).then(function(arr){ + var ret = []; if (arr.length) { - var ret = arr.map(function(item) { + ret = arr.map(function(item) { return {"_source":{"displayname": item["_source"].displayname, "iconclass": self.graphLookup[item._source.graph_id]?.iconclass || 'fa fa-question'}, "_id":item["_id"]}; }); if(self.multiple === false) { ret = ret[0]; } - callback(ret); } + callback(ret); }); } else if (self.graphIds().includes(self.value())){ self.value(null); + callback([]); + } else { + callback([]); } } }; diff --git a/arches/app/media/js/viewmodels/widget.js b/arches/app/media/js/viewmodels/widget.js index 7c7c379fa01..a85b27bd78d 100644 --- a/arches/app/media/js/viewmodels/widget.js +++ b/arches/app/media/js/viewmodels/widget.js @@ -87,7 +87,7 @@ define([ subscribeConfigObservable(obs, key); }); - if (ko.isObservable(this.defaultValue)) { + if (ko.isObservable(this.value) && ko.isObservable(this.defaultValue)) { var defaultValue = this.defaultValue(); if (this.tile && !this.tile.noDefaults && ko.unwrap(this.tile.tileid) == "" && defaultValue != null && defaultValue != "") { this.value(defaultValue); diff --git a/arches/app/media/js/views/components/datatypes/concept.js b/arches/app/media/js/views/components/datatypes/concept.js index 761bc4a5162..83da934d186 100644 --- a/arches/app/media/js/views/components/datatypes/concept.js +++ b/arches/app/media/js/views/components/datatypes/concept.js @@ -13,10 +13,12 @@ define([ this.search = params.search; if (this.search) { var filter = params.filterValue(); - params.config = ko.observable({options:[]}); + params.config = ko.observable({ + options:[], + placeholder: arches.translations.selectAnOption + }); - this.op = ko.observable(filter.op || ''); - this.placeholder = ko.observable('Select a concept'); + this.op = ko.observable(filter.op || 'eq'); this.multiple = ko.observable(false); this.searchValue = ko.observable(filter.val || ''); this.node = params.node; diff --git a/arches/app/media/js/views/components/datatypes/date.js b/arches/app/media/js/views/components/datatypes/date.js index d7c2e0ad16e..3114c3f8103 100644 --- a/arches/app/media/js/views/components/datatypes/date.js +++ b/arches/app/media/js/views/components/datatypes/date.js @@ -8,16 +8,16 @@ define(['knockout', 'templates/views/components/datatypes/date.htm'], function(k this.dateFormat = params.config.dateFormat; this.dateFormatOptions = ko.observableArray([{ 'id': 'YYYY-MM-DD HH:mm:ssZ', - 'name': 'ISO 8601 Time (YYYY-MM-DD HH:mm:ssZ)' + 'text': 'ISO 8601 Time (YYYY-MM-DD HH:mm:ssZ)' }, { 'id': 'YYYY-MM-DD', - 'name': 'ISO 8601 (YYYY-MM-DD)' + 'text': 'ISO 8601 (YYYY-MM-DD)' }, { 'id': 'YYYY-MM', - 'name': 'ISO 8601 Month (YYYY-MM)' + 'text': 'ISO 8601 Month (YYYY-MM)' }, { 'id': 'YYYY', - 'name': 'CE Year (YYYY)' + 'text': 'CE Year (YYYY)' }]); this.onDateFormatSelection = function(val, e) { @@ -29,7 +29,7 @@ define(['knockout', 'templates/views/components/datatypes/date.htm'], function(k var config = params.node.config || params.datatype.defaultconfig; var filter = params.filterValue(); this.dateFormat = config.dateFormat; - this.op = ko.observable(filter.op || ''); + this.op = ko.observable(filter.op || 'eq'); this.searchValue = ko.observable(filter.val || ''); this.filterValue = ko.computed(function() { return { diff --git a/arches/app/media/js/views/components/datatypes/domain-value.js b/arches/app/media/js/views/components/datatypes/domain-value.js index 43eb16564c4..27e1ce196b6 100644 --- a/arches/app/media/js/views/components/datatypes/domain-value.js +++ b/arches/app/media/js/views/components/datatypes/domain-value.js @@ -13,7 +13,7 @@ define([ this.options = params.node.config.options; this.options.unshift({id:"", selected:true, text:"Select an Option"}); var filter = params.filterValue(); - this.op = ko.observable(filter.op || ''); + this.op = ko.observable(filter.op || 'eq'); this.searchValue = ko.observable(filter.val || ''); this.filterValue = ko.computed(function() { return { diff --git a/arches/app/media/js/views/components/datatypes/edtf.js b/arches/app/media/js/views/components/datatypes/edtf.js index 2f8832f30fd..875af072b69 100644 --- a/arches/app/media/js/views/components/datatypes/edtf.js +++ b/arches/app/media/js/views/components/datatypes/edtf.js @@ -7,7 +7,7 @@ define(['knockout', 'templates/views/components/datatypes/edtf.htm'], function(k this.search = params.search; if (this.search) { var filter = params.filterValue(); - this.op = ko.observable(filter.op || ''); + this.op = ko.observable(filter.op || 'overlaps'); this.searchValue = ko.observable(filter.val || ''); this.filterValue = ko.computed(function() { return { diff --git a/arches/app/media/js/views/components/datatypes/number.js b/arches/app/media/js/views/components/datatypes/number.js index aafe88edbc9..eb335a040f3 100644 --- a/arches/app/media/js/views/components/datatypes/number.js +++ b/arches/app/media/js/views/components/datatypes/number.js @@ -9,7 +9,7 @@ define([ if (this.search) { var filter = params.filterValue(); - this.op = ko.observable(filter.op || ''); + this.op = ko.observable(filter.op || 'eq'); this.searchValue = ko.observable(filter.val || ''); this.filterValue = ko.computed(function() { return { diff --git a/arches/app/media/js/views/components/datatypes/string.js b/arches/app/media/js/views/components/datatypes/string.js index a56c0bd0af8..211249aad34 100644 --- a/arches/app/media/js/views/components/datatypes/string.js +++ b/arches/app/media/js/views/components/datatypes/string.js @@ -13,14 +13,12 @@ define([ this.op = ko.observable(filter.op || '~'); this.languages = ko.observableArray(); this.languages(arches.languages); - this.language = ko.observable( - arches.languages.find(lang => lang.code == arches.activeLanguage) - ); + this.language = ko.observable(arches.activeLanguage); this.searchValue = ko.observable(filter.val || ''); this.filterValue = ko.computed(function() { return { op: self.op(), - lang: self.language()?.code, + lang: self.language(), val: self.searchValue() }; }).extend({ throttle: 750 }); diff --git a/arches/app/media/js/views/components/etl_modules/base-bulk-string-editor.js b/arches/app/media/js/views/components/etl_modules/base-bulk-string-editor.js index b26a68a0e69..32e1c09cf76 100644 --- a/arches/app/media/js/views/components/etl_modules/base-bulk-string-editor.js +++ b/arches/app/media/js/views/components/etl_modules/base-bulk-string-editor.js @@ -28,10 +28,15 @@ define([ }; this.load_details = params.load_details; + this.editHistoryUrl = `${arches.urls.edit_history}?transactionid=${ko.unwrap(params.selectedLoadEvent)?.loadid}`; this.state = params.state; this.loading = params.loading || ko.observable(); this.alert = params.alert; this.moduleId = params.etlmoduleid; + this.selectedLoadEvent = params.selectedLoadEvent || ko.observable(); + this.formatTime = params.formatTime; + this.timeDifference = params.timeDifference; + this.config = params.config; this.loading(true); this.previewing = ko.observable(); this.languages = ko.observable(arches.languages); @@ -54,8 +59,8 @@ define([ this.searchUrl = ko.observable(); this.caseInsensitive = ko.observable(); this.trim = ko.observable(); - this.numberOfResources = ko.observable(); - this.numberOfTiles = ko.observable(); + this.numberOfResources = ko.observable(0); + this.numberOfTiles = ko.observable(0); this.selectedCaseOperation = ko.observable(); this.caseOperations = [ @@ -89,6 +94,27 @@ define([ return ready; }); + this.clearResults = ko.computed(() => { + // if any of these values change then clear the preview results + self.showPreview(false); + // we don't actually care about the results of the following + let clearResults = ''; + [self.selectedGraph(), + self.selectedCaseOperation(), + self.selectedNode(), + self.searchUrl(), + self.selectedLanguage(), + ((self.operation() == 'replace' && !!self.oldText() && !!self.newText() || self.operation() != 'replace')) + ].forEach(function(item){ + clearResults += item?.toString(); + }); + return clearResults; + }); + + this.allowEditOperation = ko.computed(() => { + return self.ready() && self.numberOfTiles() > 0 && self.showPreview(); + }); + this.addAllFormData = () => { if (self.operation() == 'case'){ self.formData.append('operation', self.selectedCaseOperation()); @@ -182,7 +208,7 @@ define([ }; this.write = function() { - if (!self.ready()) { + if (!self.allowEditOperation()) { return; } if (self.operation() === 'replace' && (!self.oldText() || !self.newText())){ diff --git a/arches/app/media/js/views/components/etl_modules/import-single-csv.js b/arches/app/media/js/views/components/etl_modules/import-single-csv.js index e27c9443aa3..2c305f255b4 100644 --- a/arches/app/media/js/views/components/etl_modules/import-single-csv.js +++ b/arches/app/media/js/views/components/etl_modules/import-single-csv.js @@ -46,6 +46,7 @@ define([ }); this.selectedLoadEvent = params.selectedLoadEvent || ko.observable(); + this.editHistoryUrl = `${arches.urls.edit_history}?transactionid=${ko.unwrap(params.selectedLoadEvent)?.loadid}`; this.validationErrors = params.validationErrors || ko.observable(); this.validated = params.validated || ko.observable(); this.getErrorReport = params.getErrorReport; diff --git a/arches/app/media/js/views/components/plugins/etl-manager.js b/arches/app/media/js/views/components/plugins/etl-manager.js index 0b7c1a9b933..222da4040a0 100644 --- a/arches/app/media/js/views/components/plugins/etl-manager.js +++ b/arches/app/media/js/views/components/plugins/etl-manager.js @@ -53,7 +53,7 @@ define([ this.activeTab.subscribe(val => { if (val == "import") { - self.fetchLoadEvent(); + setTimeout(this.fetchLoadEvent, 500); } }); diff --git a/arches/app/media/js/views/components/search/advanced-search.js b/arches/app/media/js/views/components/search/advanced-search.js index 06d5936041d..4d66cf9aa36 100644 --- a/arches/app/media/js/views/components/search/advanced-search.js +++ b/arches/app/media/js/views/components/search/advanced-search.js @@ -7,6 +7,7 @@ define([ 'views/components/search/base-filter', 'templates/views/components/search/advanced-search.htm', 'bindings/let', + 'bindings/key-events-click', ], function($, _, ko, koMapping, arches, BaseFilter, advancedSearchTemplate) { var componentName = 'advanced-search'; const viewModel = BaseFilter.extend({ diff --git a/arches/app/media/js/views/components/search/search-results.js b/arches/app/media/js/views/components/search/search-results.js index e38c6737bb8..8eabed860eb 100644 --- a/arches/app/media/js/views/components/search/search-results.js +++ b/arches/app/media/js/views/components/search/search-results.js @@ -4,7 +4,7 @@ define([ 'views/components/search/base-filter', 'bootstrap', 'arches', - 'select2', + 'select-woo', 'knockout', 'knockout-mapping', 'models/graph', diff --git a/arches/app/media/js/views/components/search/sort-results.js b/arches/app/media/js/views/components/search/sort-results.js index ee1772ac37c..131796535f8 100644 --- a/arches/app/media/js/views/components/search/sort-results.js +++ b/arches/app/media/js/views/components/search/sort-results.js @@ -14,7 +14,7 @@ define([ BaseFilter.prototype.initialize.call(this, options); - this.filter = ko.observable('asc'); + this.filter = ko.observable(''); this.filters[componentName](this); this.filter.subscribe(function(){ diff --git a/arches/app/media/js/views/components/search/term-filter.js b/arches/app/media/js/views/components/search/term-filter.js index 08e3b8c3529..9394606a4c5 100644 --- a/arches/app/media/js/views/components/search/term-filter.js +++ b/arches/app/media/js/views/components/search/term-filter.js @@ -11,14 +11,13 @@ define([ const viewModel = BaseFilter.extend({ initialize: function(options) { options.name = 'Term Filter'; - BaseFilter.prototype.initialize.call(this, options); this.filter.terms = ko.observableArray(); this.filter.tags = ko.observableArray(); - this.language = ko.observable(); + this.language = ko.observable("*"); this.languages = ko.observableArray(); const languages = arches.languages.slice(); languages.unshift({"code": "*", "name": "All"}); @@ -65,7 +64,7 @@ define([ var queryObj = this.query(); if (terms.length > 0){ queryObj[componentName] = ko.toJSON(terms); - queryObj['language'] = this.language()?.code; + queryObj['language'] = this.language(); } else { delete queryObj[componentName]; } @@ -86,15 +85,17 @@ define([ }, addTag: function(term, type, inverted){ - this.filter.tags.unshift({ - inverted: inverted, - type: type, - context: '', - context_label: '', - id: term, - text: term, - value: term - }); + if(!this.hasTag(term)){ + this.filter.tags.unshift({ + inverted: inverted, + type: type, + context: '', + context_label: '', + id: term, + text: term, + value: term + }); + } }, removeTag: function(term){ @@ -105,7 +106,7 @@ define([ hasTag: function(tag_text){ var has_tag = false; - this.filter.terms().forEach(function(term_item){ + this.filter.tags().forEach(function(term_item){ if (term_item.text == tag_text) { has_tag = true; } diff --git a/arches/app/media/js/views/components/widgets/map.js b/arches/app/media/js/views/components/widgets/map.js index 17359718f8b..9dbf455c9d6 100644 --- a/arches/app/media/js/views/components/widgets/map.js +++ b/arches/app/media/js/views/components/widgets/map.js @@ -9,7 +9,7 @@ define([ 'templates/views/components/map-widget-editor.htm', 'bindings/chosen', 'bindings/codemirror', - 'select2', + 'select-woo', 'bindings/select2v4', 'bindings/fadeVisible', 'bindings/mapbox-gl', diff --git a/arches/app/media/js/views/concept-search.js b/arches/app/media/js/views/concept-search.js index 01a1d16f327..34afaf1eb0c 100644 --- a/arches/app/media/js/views/concept-search.js +++ b/arches/app/media/js/views/concept-search.js @@ -1,4 +1,4 @@ -define(['jquery', 'underscore', 'backbone', 'select2', 'arches'], function($, _, Backbone, Select2, arches) { +define(['jquery', 'underscore', 'backbone', 'select-woo', 'arches'], function($, _, Backbone, Select2, arches) { return Backbone.View.extend({ initialize: function(options) { diff --git a/arches/app/media/js/views/rdm/modals/value-form.js b/arches/app/media/js/views/rdm/modals/value-form.js index d18986d4d6b..d159a61d9a8 100644 --- a/arches/app/media/js/views/rdm/modals/value-form.js +++ b/arches/app/media/js/views/rdm/modals/value-form.js @@ -1,4 +1,4 @@ -define(['jquery', 'backbone', 'bootstrap', 'select2'], function($, Backbone) { +define(['jquery', 'backbone', 'bootstrap', 'select-woo'], function($, Backbone) { return Backbone.View.extend({ initialize: function(options) { var self = this, diff --git a/arches/app/media/js/views/resource/related-resources-manager.js b/arches/app/media/js/views/resource/related-resources-manager.js index 4bb29e3f079..f793c10db14 100644 --- a/arches/app/media/js/views/resource/related-resources-manager.js +++ b/arches/app/media/js/views/resource/related-resources-manager.js @@ -371,7 +371,9 @@ define([ }, dataType: 'json', quietMillis: 250, - data: function(term, page) { + data: function(requestParams) { + let term = requestParams.term || ''; + let page = requestParams.page || 1; //TODO This regex isn't working, but it would nice fix it so that we can do more robust url checking // var expression = /[-a-zA-Z0-9@:%_\+.~#?&//=]{2,256}\.[a-z]{2,4}\b(\/[-a-zA-Z0-9@:%_\+.~#?&//=]*)?/gi; // var regex = new RegExp(expression); @@ -407,11 +409,18 @@ define([ return data; } }, - - results: function(data, page) { + processResults: function(data) { + data.results.hits.hits.forEach(function(hit){ + if (self.disableSearchResults(hit) === true) { + hit.disabled = true; + } + hit.id = hit._id; + }); return { - results: data.results.hits.hits, - more: data['paging-filter'].paginator.has_next + "results": data.results.hits.hits, + "pagination": { + "more": data['paging-filter'].paginator.has_next + } }; } }, @@ -425,35 +434,29 @@ define([ self.relationshipCandidateIds(null); }); }, - id: function(item) { - return item._id; - }, - formatResult: function(item) { - if (self.disableSearchResults(item) === false) { + templateResult: function(item) { + var ret = ''; + if(!item.id){ + return item.text; + } + if(item.disabled){ + ret = '' + item._source.displayname + ' Cannot be related'; + } else { if (item._source) { - return item._source.displayname; + ret = '' + item._source.displayname + ''; } else { - return ' Create a new ' + item.name + ' . . . '; + ret = ' Create a new ' + item.name + ' . . . '; } - } else { - return '' + item._source.displayname + ' Cannot be related'; - } - }, - formatResultCssClass: function(item) { - if (self.disableSearchResults(item) === false) { - return ''; - } else { - return 'disabled'; } + return $(ret); }, - formatSelection: function(item) { + templateSelection: function(item) { if (item._source) { return item._source.displayname; } else { return item.name; } - }, - initSelection: function(el, callback) { } + } }; }, diff --git a/arches/app/media/js/views/simple-search.js b/arches/app/media/js/views/simple-search.js index 8c6338197e7..53eca7f1242 100644 --- a/arches/app/media/js/views/simple-search.js +++ b/arches/app/media/js/views/simple-search.js @@ -1,4 +1,4 @@ -define(['jquery', 'backbone', 'select2'], function($, Backbone) { +define(['jquery', 'backbone', 'select-woo'], function($, Backbone) { return Backbone.View.extend({ initialize: function() { this.$el.find('.arches_simple_search').select2({ diff --git a/arches/app/media/plugins/knockout-select2.js b/arches/app/media/plugins/knockout-select2.js index 981d878629a..e34f0c2a1f8 100644 --- a/arches/app/media/plugins/knockout-select2.js +++ b/arches/app/media/plugins/knockout-select2.js @@ -1,4 +1,4 @@ -define(['jquery', 'knockout', 'underscore', 'select2'], function($, ko, _) { +define(['jquery', 'knockout', 'underscore', 'select-woo'], function($, ko, _, selectWoo) { ko.bindingHandlers.select2 = { init: function(el, valueAccessor, allBindingsAccessor, viewmodel, bindingContext) { var allBindings = allBindingsAccessor().select2; @@ -9,7 +9,7 @@ define(['jquery', 'knockout', 'underscore', 'select2'], function($, ko, _) { $(el).select2('destroy'); }); - select2Config.formatResult = function(item) { + select2Config.templateResult = function(item) { return ko.unwrap(item.text); }; @@ -83,7 +83,7 @@ define(['jquery', 'knockout', 'underscore', 'select2'], function($, ko, _) { return result; }; - select2Config.formatSelection = function(item) { + select2Config.templateSelection = function(item) { var path = []; var result = ko.unwrap(item.text); if (select2Config.showParents === true) { @@ -97,8 +97,8 @@ define(['jquery', 'knockout', 'underscore', 'select2'], function($, ko, _) { select2Config.value = value(); - $(el).select2(select2Config); - $(el).select2("val", value()); + $(el).selectWoo(select2Config); + $(el).selectWoo("val", value()); $(el).on("change", function(val) { if (val.val === "") { val.val = null; @@ -107,7 +107,7 @@ define(['jquery', 'knockout', 'underscore', 'select2'], function($, ko, _) { }); if (ko.unwrap(select2Config.disabled)) { - $(el).select2("disable"); + $(el).selectWoo("disable"); } $(el).on("select2-opening", function() { @@ -117,7 +117,7 @@ define(['jquery', 'knockout', 'underscore', 'select2'], function($, ko, _) { }); value.subscribe(function(newVal) { select2Config.value = newVal; - $(el).select2("val", newVal); + $(el).selectWoo("val", newVal); }, this); } }; diff --git a/arches/app/models/migrations/10012_alter_file_path.py b/arches/app/models/migrations/10012_alter_file_path.py new file mode 100644 index 00000000000..796db53b2bc --- /dev/null +++ b/arches/app/models/migrations/10012_alter_file_path.py @@ -0,0 +1,19 @@ +# Generated by Django 4.2.4 on 2023-09-13 10:19 + +import arches.app.utils.storage_filename_generator +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('models', '9979_update_stage_for_bulk_edit'), + ] + + operations = [ + migrations.AlterField( + model_name='file', + name='path', + field=models.FileField(upload_to=arches.app.utils.storage_filename_generator.generate_filename), + ), + ] diff --git a/arches/app/models/migrations/10041_limit_number_of_bulk_edit.py b/arches/app/models/migrations/10041_limit_number_of_bulk_edit.py new file mode 100644 index 00000000000..ae387bb1070 --- /dev/null +++ b/arches/app/models/migrations/10041_limit_number_of_bulk_edit.py @@ -0,0 +1,190 @@ +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("models", "10012_alter_file_path"), + ] + + add_limit_to_config = """ + update etl_modules + set config = jsonb_set(config, '{updateLimit}', to_jsonb(5000), true) + where etlmoduleid in ( + 'e4169b44-124a-4ff6-bd11-5521901f98a7', + '9079b83c-e22b-4fdc-a22e-74487ee7b7f3', + '80fc7aab-cbd8-4dc0-b55b-5facac4cd157' + ); + """ + + revert_config = """ + update etl_modules + set config = config - 'updateLimit' + where etlmoduleid in ( + 'e4169b44-124a-4ff6-bd11-5521901f98a7', + '9079b83c-e22b-4fdc-a22e-74487ee7b7f3', + '80fc7aab-cbd8-4dc0-b55b-5facac4cd157' + ); + """ + + update_staging_function = """ + DROP FUNCTION IF EXISTS __arches_stage_string_data_for_bulk_edit(uuid, uuid, uuid, uuid, uuid[], text, text, text, boolean); + CREATE OR REPLACE FUNCTION __arches_stage_string_data_for_bulk_edit( + load_id uuid, + graph_id uuid, + node_id uuid, + module_id uuid, + resourceinstance_ids uuid[], + operation text, + text_replacing text, + language_code text, + case_insensitive boolean, + update_limit integer + ) + RETURNS VOID + LANGUAGE 'plpgsql' + AS $$ + DECLARE + tile_id uuid; + tile_data jsonb; + nodegroup_id uuid; + parenttile_id uuid; + resourceinstance_id uuid; + text_replacing_like text; + BEGIN + IF text_replacing IS NOT NULL THEN + text_replacing_like = FORMAT('%%%s%%', text_replacing); + END IF; + INSERT INTO load_staging (tileid, value, nodegroupid, parenttileid, resourceid, loadid, nodegroup_depth, source_description, operation, passes_validation) + SELECT DISTINCT t.tileid, t.tiledata, t.nodegroupid, t.parenttileid, t.resourceinstanceid, load_id, 0, 'bulk_edit', 'update', true + FROM tiles t, nodes n + WHERE t.nodegroupid = n.nodegroupid + AND CASE + WHEN graph_id IS NULL THEN true + ELSE n.graphid = graph_id + END + AND CASE + WHEN node_id IS NULL THEN n.datatype = 'string' + ELSE n.nodeid = node_id + END + AND CASE + WHEN resourceinstance_ids IS NULL THEN true + ELSE t.resourceinstanceid = ANY(resourceinstance_ids) + END + AND CASE + WHEN text_replacing IS NULL THEN + CASE operation + WHEN 'trim' THEN + t.tiledata -> nodeid::text -> language_code ->> 'value' <> TRIM(t.tiledata -> nodeid::text -> language_code ->> 'value') + WHEN 'capitalize' THEN + t.tiledata -> nodeid::text -> language_code ->> 'value' <> INITCAP(t.tiledata -> nodeid::text -> language_code ->> 'value') + WHEN 'capitalize_trim' THEN + t.tiledata -> nodeid::text -> language_code ->> 'value' <> TRIM(INITCAP(t.tiledata -> nodeid::text -> language_code ->> 'value')) + WHEN 'upper' THEN + t.tiledata -> nodeid::text -> language_code ->> 'value' <> UPPER(t.tiledata -> nodeid::text -> language_code ->> 'value') + WHEN 'upper_trim' THEN + t.tiledata -> nodeid::text -> language_code ->> 'value' <> TRIM(UPPER(t.tiledata -> nodeid::text -> language_code ->> 'value')) + WHEN 'lower' THEN + t.tiledata -> nodeid::text -> language_code ->> 'value' <> LOWER(t.tiledata -> nodeid::text -> language_code ->> 'value') + WHEN 'lower_trim' THEN + t.tiledata -> nodeid::text -> language_code ->> 'value' <> TRIM(LOWER(t.tiledata -> nodeid::text -> language_code ->> 'value')) + END + WHEN language_code IS NOT NULL AND case_insensitive THEN + t.tiledata -> nodeid::text -> language_code ->> 'value' ilike text_replacing_like + WHEN language_code IS NOT NULL AND NOT case_insensitive THEN + t.tiledata -> nodeid::text -> language_code ->> 'value' like text_replacing_like + WHEN language_code IS NULL AND case_insensitive THEN + t.tiledata::text ilike text_replacing_like + WHEN language_code IS NULL AND NOT case_insensitive THEN + t.tiledata::text like text_replacing_like + END + LIMIT update_limit; + END; + $$; + """ + + revert_staging_function = """ + DROP FUNCTION IF EXISTS __arches_stage_string_data_for_bulk_edit(uuid, uuid, uuid, uuid, uuid[], text, text, text, boolean, integer); + CREATE OR REPLACE FUNCTION __arches_stage_string_data_for_bulk_edit( + load_id uuid, + graph_id uuid, + node_id uuid, + module_id uuid, + resourceinstance_ids uuid[], + operation text, + text_replacing text, + language_code text, + case_insensitive boolean + ) + RETURNS VOID + LANGUAGE 'plpgsql' + AS $$ + DECLARE + tile_id uuid; + tile_data jsonb; + nodegroup_id uuid; + parenttile_id uuid; + resourceinstance_id uuid; + text_replacing_like text; + BEGIN + IF text_replacing IS NOT NULL THEN + text_replacing_like = FORMAT('%%%s%%', text_replacing); + END IF; + INSERT INTO load_staging (tileid, value, nodegroupid, parenttileid, resourceid, loadid, nodegroup_depth, source_description, operation, passes_validation) + SELECT DISTINCT t.tileid, t.tiledata, t.nodegroupid, t.parenttileid, t.resourceinstanceid, load_id, 0, 'bulk_edit', 'update', true + FROM tiles t, nodes n + WHERE t.nodegroupid = n.nodegroupid + AND CASE + WHEN graph_id IS NULL THEN true + ELSE n.graphid = graph_id + END + AND CASE + WHEN node_id IS NULL THEN n.datatype = 'string' + ELSE n.nodeid = node_id + END + AND CASE + WHEN resourceinstance_ids IS NULL THEN true + ELSE t.resourceinstanceid = ANY(resourceinstance_ids) + END + AND CASE + WHEN text_replacing IS NULL THEN + CASE operation + WHEN 'trim' THEN + t.tiledata -> nodeid::text -> language_code ->> 'value' <> TRIM(t.tiledata -> nodeid::text -> language_code ->> 'value') + WHEN 'capitalize' THEN + t.tiledata -> nodeid::text -> language_code ->> 'value' <> INITCAP(t.tiledata -> nodeid::text -> language_code ->> 'value') + WHEN 'capitalize_trim' THEN + t.tiledata -> nodeid::text -> language_code ->> 'value' <> TRIM(INITCAP(t.tiledata -> nodeid::text -> language_code ->> 'value')) + WHEN 'upper' THEN + t.tiledata -> nodeid::text -> language_code ->> 'value' <> UPPER(t.tiledata -> nodeid::text -> language_code ->> 'value') + WHEN 'upper_trim' THEN + t.tiledata -> nodeid::text -> language_code ->> 'value' <> TRIM(UPPER(t.tiledata -> nodeid::text -> language_code ->> 'value')) + WHEN 'lower' THEN + t.tiledata -> nodeid::text -> language_code ->> 'value' <> LOWER(t.tiledata -> nodeid::text -> language_code ->> 'value') + WHEN 'lower_trim' THEN + t.tiledata -> nodeid::text -> language_code ->> 'value' <> TRIM(LOWER(t.tiledata -> nodeid::text -> language_code ->> 'value')) + END + WHEN language_code IS NOT NULL AND case_insensitive THEN + t.tiledata -> nodeid::text -> language_code ->> 'value' ilike text_replacing_like + WHEN language_code IS NOT NULL AND NOT case_insensitive THEN + t.tiledata -> nodeid::text -> language_code ->> 'value' like text_replacing_like + WHEN language_code IS NULL AND case_insensitive THEN + t.tiledata::text ilike text_replacing_like + WHEN language_code IS NULL AND NOT case_insensitive THEN + t.tiledata::text like text_replacing_like + END; + END; + $$; + """ + + + operations = [ + migrations.RunSQL( + add_limit_to_config, + revert_config, + ), + migrations.RunSQL( + update_staging_function, + revert_staging_function, + ), + ] diff --git a/arches/app/models/migrations/10097_add_editlog_index.py b/arches/app/models/migrations/10097_add_editlog_index.py new file mode 100644 index 00000000000..0f86dc5ba99 --- /dev/null +++ b/arches/app/models/migrations/10097_add_editlog_index.py @@ -0,0 +1,17 @@ +# Generated by Django 3.2.20 on 2023-09-29 18:29 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('models', '10041_limit_number_of_bulk_edit'), + ] + + operations = [ + migrations.AddIndex( + model_name='editlog', + index=models.Index(fields=['transactionid'], name='edit_log_transac_34aa75_idx'), + ), + ] diff --git a/arches/app/models/models.py b/arches/app/models/models.py index 69a9062b574..06c33dc4311 100644 --- a/arches/app/models/models.py +++ b/arches/app/models/models.py @@ -17,6 +17,7 @@ from arches.app.utils.module_importer import get_class_from_modulename from arches.app.models.fields.i18n import I18n_TextField, I18n_JSONField +from arches.app.utils import import_class_from_string from django.contrib.gis.db import models from django.db.models import JSONField from django.core.cache import caches @@ -281,6 +282,9 @@ def __init__(self, *args, **kwargs): class Meta: managed = True db_table = "edit_log" + indexes = [ + models.Index(fields=["transactionid"]), + ] class ExternalOauthToken(models.Model): @@ -322,7 +326,7 @@ class Meta: class File(models.Model): fileid = models.UUIDField(primary_key=True) - path = models.FileField(upload_to="uploadedfiles") + path = models.FileField(upload_to=import_class_from_string(settings.FILENAME_GENERATOR)) tile = models.ForeignKey("TileModel", db_column="tileid", null=True, on_delete=models.CASCADE) def __init__(self, *args, **kwargs): diff --git a/arches/app/search/components/advanced_search.py b/arches/app/search/components/advanced_search.py index acdc986ee39..8ca94e2f288 100644 --- a/arches/app/search/components/advanced_search.py +++ b/arches/app/search/components/advanced_search.py @@ -36,6 +36,11 @@ def append_dsl(self, search_results_object, permitted_nodegroups, include_provis node = models.Node.objects.get(pk=key) if self.request.user.has_perm("read_nodegroup", node.nodegroup): datatype = datatype_factory.get_instance(node.datatype) + try: + val["val"] = "" if val["val"] == None else val["val"] + except: + pass + if ("op" in val and (val["op"] == "null" or val["op"] == "not_null")) or ( "val" in val and (val["val"] == "null" or val["val"] == "not_null") ): diff --git a/arches/app/tasks.py b/arches/app/tasks.py index ede280869f1..48ec2a6036d 100644 --- a/arches/app/tasks.py +++ b/arches/app/tasks.py @@ -201,7 +201,7 @@ def on_chord_error(request, exc, traceback): def load_excel_data(import_module, importer_name, userid, files, summary, result, temp_dir, loadid): logger = logging.getLogger(__name__) try: - import_module.run_load_task(files, summary, result, temp_dir, loadid) + import_module.run_load_task(userid, files, summary, result, temp_dir, loadid) load_event = models.LoadEvent.objects.get(loadid=loadid) status = _("Completed") if load_event.status == "indexed" else _("Failed") @@ -276,7 +276,7 @@ def load_single_csv(userid, loadid, graphid, has_headers, fieldnames, csv_mappin try: ImportSingleCsv = import_single_csv.ImportSingleCsv() - ImportSingleCsv.run_load_task(loadid, graphid, has_headers, fieldnames, csv_mapping, csv_file_name, id_label) + ImportSingleCsv.run_load_task(userid, loadid, graphid, has_headers, fieldnames, csv_mapping, csv_file_name, id_label) load_event = models.LoadEvent.objects.get(loadid=loadid) status = _("Completed") if load_event.status == "indexed" else _("Failed") @@ -293,14 +293,14 @@ def load_single_csv(userid, loadid, graphid, has_headers, fieldnames, csv_mappin @shared_task -def edit_bulk_string_data(load_id, graph_id, node_id, operation, language_code, old_text, new_text, resourceids, userid): +def edit_bulk_string_data(userid, load_id, module_id, graph_id, node_id, operation, language_code, old_text, new_text, resourceids): from arches.app.etl_modules import base_data_editor logger = logging.getLogger(__name__) try: BulkStringEditor = base_data_editor.BulkStringEditor(loadid=load_id) - BulkStringEditor.run_load_task(load_id, graph_id, node_id, operation, language_code, old_text, new_text, resourceids) + BulkStringEditor.run_load_task(userid, load_id, module_id, graph_id, node_id, operation, language_code, old_text, new_text, resourceids) load_event = models.LoadEvent.objects.get(loadid=load_id) status = _("Completed") if load_event.status == "indexed" else _("Failed") diff --git a/arches/app/templates/base-manager.htm b/arches/app/templates/base-manager.htm index 617b64aedca..5058cc976c6 100644 --- a/arches/app/templates/base-manager.htm +++ b/arches/app/templates/base-manager.htm @@ -448,6 +448,15 @@

{% endif %} + {% if nav.report_view %} + +
+ +
+
+ {% endif %} + {% if nav.help %}
  • @@ -211,17 +221,21 @@

    - - + -
    - - -
    + + + + + + + @@ -230,4 +244,43 @@

    {% endblock etl_status %} {% block loading_status %} +
    +

    + +

    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    {% endblock loading_status %} diff --git a/arches/app/templates/views/components/etl_modules/base-import.htm b/arches/app/templates/views/components/etl_modules/base-import.htm index d2bb5d77ed0..a7e74eeb925 100644 --- a/arches/app/templates/views/components/etl_modules/base-import.htm +++ b/arches/app/templates/views/components/etl_modules/base-import.htm @@ -53,7 +53,12 @@

    - + + + + +

    @@ -104,7 +109,7 @@

    - {% trans "Full Error Report" %} + {% trans "Full Error Report" %} diff --git a/arches/app/templates/views/components/etl_modules/branch-excel-importer.htm b/arches/app/templates/views/components/etl_modules/branch-excel-importer.htm index ddd038cbf8b..38e0370e217 100644 --- a/arches/app/templates/views/components/etl_modules/branch-excel-importer.htm +++ b/arches/app/templates/views/components/etl_modules/branch-excel-importer.htm @@ -23,16 +23,17 @@

    - +
    diff --git a/arches/app/templates/views/components/etl_modules/import-single-csv.htm b/arches/app/templates/views/components/etl_modules/import-single-csv.htm index 590c6d46863..d331109765f 100644 --- a/arches/app/templates/views/components/etl_modules/import-single-csv.htm +++ b/arches/app/templates/views/components/etl_modules/import-single-csv.htm @@ -159,6 +159,10 @@

    +
    + + +
    diff --git a/arches/app/templates/views/components/etl_modules/tile-excel-importer.htm b/arches/app/templates/views/components/etl_modules/tile-excel-importer.htm index 26d7de65d24..a993eac9c60 100644 --- a/arches/app/templates/views/components/etl_modules/tile-excel-importer.htm +++ b/arches/app/templates/views/components/etl_modules/tile-excel-importer.htm @@ -23,16 +23,17 @@

    - +
    diff --git a/arches/app/templates/views/components/search/advanced-search.htm b/arches/app/templates/views/components/search/advanced-search.htm index e7651b9977b..71aa1bc60d3 100644 --- a/arches/app/templates/views/components/search/advanced-search.htm +++ b/arches/app/templates/views/components/search/advanced-search.htm @@ -29,17 +29,29 @@ -
    - -
    -
    +
    @@ -61,7 +73,9 @@
    -
    @@ -74,40 +88,62 @@
    -

    +
    -

    +
    - +
    -
    -

    + +
    +
    -

    +
    -
    + +
    diff --git a/arches/app/templates/views/components/search/map-filter.htm b/arches/app/templates/views/components/search/map-filter.htm index 252cbb9f24e..780f7ebf940 100644 --- a/arches/app/templates/views/components/search/map-filter.htm +++ b/arches/app/templates/views/components/search/map-filter.htm @@ -44,7 +44,7 @@
    - - + + +
    @@ -72,18 +79,17 @@
    diff --git a/arches/app/templates/views/components/search/provisional-filter.htm b/arches/app/templates/views/components/search/provisional-filter.htm index 70902d3491c..9dfa861a855 100755 --- a/arches/app/templates/views/components/search/provisional-filter.htm +++ b/arches/app/templates/views/components/search/provisional-filter.htm @@ -1,5 +1,5 @@ {% load i18n %} -
    +
    @@ -45,13 +44,12 @@
    -
    diff --git a/arches/app/templates/views/components/widgets/map.htm b/arches/app/templates/views/components/widgets/map.htm index 39b82986645..9a5447273e7 100644 --- a/arches/app/templates/views/components/widgets/map.htm +++ b/arches/app/templates/views/components/widgets/map.htm @@ -35,7 +35,7 @@
    - + attr: {'aria-label': $root.translations.availableGeometryTypes}"> +
    diff --git a/arches/app/templates/views/components/widgets/node-value-select.htm b/arches/app/templates/views/components/widgets/node-value-select.htm index 70cd53167a5..1b219cdf12d 100644 --- a/arches/app/templates/views/components/widgets/node-value-select.htm +++ b/arches/app/templates/views/components/widgets/node-value-select.htm @@ -11,13 +11,11 @@
    - + ">
    @@ -60,6 +58,11 @@ {% block report %}
    + + + + +
    {% endblock report %} diff --git a/arches/app/templates/views/components/widgets/resource-instance-select.htm b/arches/app/templates/views/components/widgets/resource-instance-select.htm index 3b0494312f9..7469a2f8b99 100644 --- a/arches/app/templates/views/components/widgets/resource-instance-select.htm +++ b/arches/app/templates/views/components/widgets/resource-instance-select.htm @@ -7,13 +7,13 @@
    - + attr: {'data-label': label, 'aria-label': label} + ">
    @@ -27,13 +27,13 @@
    - + attr: {'data-label': label, 'aria-label': label} + ">
    @@ -121,7 +121,7 @@ - + > @@ -157,7 +157,7 @@ - + > @@ -248,27 +248,29 @@
    -
    - +
    -
    +
    - +
    @@ -347,7 +349,7 @@ - + attr: {'aria-labeledby': resourceToRRLabel} "> + @@ -384,7 +385,7 @@ - + attr: {'aria-labeledby': rrToResourceLabel}"> + diff --git a/arches/app/templates/views/components/widgets/select.htm b/arches/app/templates/views/components/widgets/select.htm index e14b4c9054c..2ce0c665ff6 100644 --- a/arches/app/templates/views/components/widgets/select.htm +++ b/arches/app/templates/views/components/widgets/select.htm @@ -10,8 +10,8 @@
    - + }, attr: {'data-label': label} "> +
    @@ -47,8 +48,8 @@
    - +

    diff --git a/arches/app/templates/views/graph/graph-designer/card-configuration.htm b/arches/app/templates/views/graph/graph-designer/card-configuration.htm index d0ed9b6a886..94fd77adee9 100644 --- a/arches/app/templates/views/graph/graph-designer/card-configuration.htm +++ b/arches/app/templates/views/graph/graph-designer/card-configuration.htm @@ -119,10 +119,10 @@
    diff --git a/arches/app/templates/views/report-templates/tabbed.htm b/arches/app/templates/views/report-templates/tabbed.htm index 19529822789..25092c5f8d3 100644 --- a/arches/app/templates/views/report-templates/tabbed.htm +++ b/arches/app/templates/views/report-templates/tabbed.htm @@ -69,19 +69,6 @@ chosen: {disable_search_threshold: 10, width: '100%'} " > -
    diff --git a/arches/app/templates/views/resource/related-resources/related-resources-manager.htm b/arches/app/templates/views/resource/related-resources/related-resources-manager.htm index d622027dcba..79662ffee10 100644 --- a/arches/app/templates/views/resource/related-resources/related-resources-manager.htm +++ b/arches/app/templates/views/resource/related-resources/related-resources-manager.htm @@ -87,11 +87,11 @@

    - + }"> +
    @@ -157,11 +157,11 @@

    - + }"> + @@ -175,11 +175,11 @@

    - + }"> + @@ -194,11 +194,11 @@

    - + }"> + @@ -212,11 +212,11 @@

    - + }"> + @@ -249,7 +249,7 @@

    - placeholder: self.relationshipTypePlaceholder } }"> +

    diff --git a/arches/app/templates/views/search.htm b/arches/app/templates/views/search.htm index 8a89125ddcb..9e8f7e1b723 100644 --- a/arches/app/templates/views/search.htm +++ b/arches/app/templates/views/search.htm @@ -16,36 +16,34 @@
    -
    +
    -
    -
    - -
    -

    -
    +
    + +
    +

    -
    -
    - - - -
    -
    +
    +
    +
    + + + +
    diff --git a/arches/app/utils/skos.py b/arches/app/utils/skos.py index 9e25ba545ed..7c9e4599510 100644 --- a/arches/app/utils/skos.py +++ b/arches/app/utils/skos.py @@ -137,6 +137,12 @@ def save_concepts_from_skos(self, graph, overwrite_options="overwrite", staging_ if predicate == SKOS.hasTopConcept: top_concept_id = self.generate_uuid_from_subject(baseuuid, object) self.relations.append({"source": scheme_id, "type": "hasTopConcept", "target": top_concept_id}) + elif predicate == SKOS.narrower: + concept_id = self.generate_uuid_from_subject(baseuuid, object) + raise Exception(f"The Concept Scheme defines a \"narrower\" relation to a concept instead of using \"hasTopConcept\". First concept with incorrect relation: {object}") + + if len(self.relations) == 0: + raise Exception("The SKOS file doesn't appear to have any Top Concepts defined.") concept_scheme.addvalue( { diff --git a/arches/app/utils/storage_filename_generator.py b/arches/app/utils/storage_filename_generator.py new file mode 100644 index 00000000000..9ca1f4fb1a7 --- /dev/null +++ b/arches/app/utils/storage_filename_generator.py @@ -0,0 +1,5 @@ +import os +from arches.app.models.system_settings import settings + +def generate_filename(instance, filename): + return os.path.join(settings.UPLOADED_FILES_DIR, filename) diff --git a/arches/app/views/resource.py b/arches/app/views/resource.py index 02ee5505e3c..855344210dc 100644 --- a/arches/app/views/resource.py +++ b/arches/app/views/resource.py @@ -201,6 +201,7 @@ def add_i18n_to_widget_defaults(widgets): creator = instance_creator["creatorid"] user_created_instance = instance_creator["user_can_edit_instance_permissions"] + ontologyclass = None nodegroups = [] editable_nodegroups = [] @@ -342,6 +343,9 @@ def add_i18n_to_widget_defaults(widgets): context["nav"]["title"] = "" context["nav"]["menu"] = nav_menu + + if resourceid not in (None, ""): + context["nav"]["report_view"] = True if resourceid == settings.RESOURCE_INSTANCE_ID: context["nav"]["help"] = {"title": _("Managing System Settings"), "templates": ["system-settings-help"]} @@ -536,12 +540,16 @@ def getEditConceptValue(self, values): pass def get(self, request, resourceid=None, view_template="views/resource/edit-log.htm"): + transaction_id = request.GET.get("transactionid", None) if resourceid is None: - recent_edits = ( - models.EditLog.objects.all() - .exclude(resourceclassid=settings.SYSTEM_SETTINGS_RESOURCE_MODEL_ID) - .order_by("-timestamp")[:100] - ) + if transaction_id: + recent_edits = models.EditLog.objects.filter(transactionid=transaction_id).order_by("-timestamp") + else: + recent_edits = ( + models.EditLog.objects.all() + .exclude(resourceclassid=settings.SYSTEM_SETTINGS_RESOURCE_MODEL_ID) + .order_by("-timestamp")[:100] + ) edited_ids = list({edit.resourceinstanceid for edit in recent_edits}) resources = Resource.objects.filter(resourceinstanceid__in=edited_ids).select_related("graph") edit_type_lookup = { diff --git a/arches/install/arches-admin b/arches/install/arches-admin index 5e5bc7d7568..d4f7493cfd7 100644 --- a/arches/install/arches-admin +++ b/arches/install/arches-admin @@ -145,7 +145,7 @@ class ArchesProjectCommand(TemplateCommand): options["arches_version"] = "master" if complete_version[3] == 'final': options["arches_version"] = f"stable/{__version__}" - elif complete_version[3] == 'beta': + elif complete_version[3] in ['alpha','beta','rc']: options["arches_version"] = f"dev/{complete_version[0]}.{complete_version[1]}.x" super(ArchesProjectCommand, self).handle('project', project_name, target, **options) diff --git a/arches/install/arches-project b/arches/install/arches-project index 372a21447a8..f3d9e76c172 100644 --- a/arches/install/arches-project +++ b/arches/install/arches-project @@ -56,7 +56,7 @@ class ArchesCommand(TemplateCommand): options["arches_version"] = "master" if complete_version[3] == 'final': options["arches_version"] = f"stable/{arches.__version__}" - elif complete_version[3] == 'beta': + elif complete_version[3] in ['alpha','beta','rc']: options["arches_version"] = f"dev/{complete_version[0]}.{complete_version[1]}.x" options["arches_semantic_version"] = ".".join([str(arches.VERSION[0]), str(arches.VERSION[1]), str(arches.VERSION[2])]) super(ArchesCommand, self).handle('project', project_name, target, **options) diff --git a/arches/install/arches-templates/project_name/settings.py-tpl b/arches/install/arches-templates/project_name/settings.py-tpl index 1ceb362f284..3eccd27ce30 100644 --- a/arches/install/arches-templates/project_name/settings.py-tpl +++ b/arches/install/arches-templates/project_name/settings.py-tpl @@ -37,6 +37,8 @@ LOCALE_PATHS.append(os.path.join(APP_ROOT, 'locale')) FILE_TYPE_CHECKING = False FILE_TYPES = ["bmp", "gif", "jpg", "jpeg", "pdf", "png", "psd", "rtf", "tif", "tiff", "xlsx", "csv", "zip"] +FILENAME_GENERATOR = "arches.app.utils.storage_filename_generator.generate_filename" +UPLOADED_FILES_DIR = "uploadedfiles" # SECURITY WARNING: keep the secret key used in production secret! SECRET_KEY = '{{ secret_key }}' @@ -300,7 +302,7 @@ CELERY_BEAT_SCHEDULE = { # way of monitoring celery so you can detect the background task not being available. CELERY_CHECK_ONLY_INSPECT_BROKER = False -CANTALOUPE_DIR = os.path.join(ROOT_DIR, "uploadedfiles") +CANTALOUPE_DIR = os.path.join(ROOT_DIR, UPLOADED_FILES_DIR) CANTALOUPE_HTTP_ENDPOINT = "http://localhost:8182/" ACCESSIBILITY_MODE = False @@ -353,7 +355,7 @@ SHOW_LANGUAGE_SWITCH = len(LANGUAGES) > 1 try: from .package_settings import * except ImportError: - try: + try: from package_settings import * except ImportError as e: pass @@ -361,7 +363,7 @@ except ImportError: try: from .settings_local import * except ImportError as e: - try: + try: from settings_local import * except ImportError as e: pass diff --git a/arches/management/commands/packages.py b/arches/management/commands/packages.py index 20c6864739a..0c78c995621 100644 --- a/arches/management/commands/packages.py +++ b/arches/management/commands/packages.py @@ -763,7 +763,7 @@ def load_business_data(package_dir, prevent_indexing): self.import_business_data_relations(relation) uploaded_files = glob.glob(os.path.join(package_dir, "business_data", "files", "*")) - dest_files_dir = os.path.join(settings.MEDIA_ROOT, "uploadedfiles") + dest_files_dir = os.path.join(settings.MEDIA_ROOT, settings.UPLOADED_FILES_DIR) if os.path.exists(dest_files_dir) is False: os.makedirs(dest_files_dir) for f in uploaded_files: diff --git a/arches/settings.py b/arches/settings.py index 995c0ff3f95..d8c3d6c4482 100644 --- a/arches/settings.py +++ b/arches/settings.py @@ -598,6 +598,8 @@ FILE_TYPE_CHECKING = False FILE_TYPES = ["bmp", "gif", "jpg", "jpeg", "pdf", "png", "psd", "rtf", "tif", "tiff", "xlsx", "csv", "zip"] +FILENAME_GENERATOR = "arches.app.utils.storage_filename_generator.generate_filename" +UPLOADED_FILES_DIR = "uploadedfiles" MAPBOX_API_KEY = "" # Put your Mapbox key here! @@ -670,7 +672,7 @@ CLUSTER_DISTANCE_MAX = 5000 # meters GRAPH_MODEL_CACHE_TIMEOUT = None # seconds * hours * days = ~1mo -CANTALOUPE_DIR = os.path.join(ROOT_DIR, "uploadedfiles") +CANTALOUPE_DIR = os.path.join(ROOT_DIR, UPLOADED_FILES_DIR) CANTALOUPE_HTTP_ENDPOINT = "http://localhost:8182/" ACCESSIBILITY_MODE = False diff --git a/releases/7.5.0.md b/releases/7.5.0.md index 5ee3e69e5d0..67519053106 100644 --- a/releases/7.5.0.md +++ b/releases/7.5.0.md @@ -61,7 +61,7 @@ None 2. Replace any calls to `django.utils.translation.ugettext()` with `django.utils.translation.gettext()`. -3. In settings.py (or settings_local.py, etc.), remove `USE_L10N`. +3. In settings.py (or settings_local.py, etc.), remove `USE_L10N`. If present, replace `DEFAULT_FILE_STORAGE` with `STORAGES["default"]["BACKEND"]` and/or `STATICFILES_STORAGE` with `STORAGES["staticfiles"]["BACKEND"]`. (An example configuration can be found in the [Django documentation](https://docs.djangoproject.com/en/4.2/ref/settings/#storages).) 4. Within your project, with your Python 3 virtual environment activated: ```